using System; using System.Collections.Generic; using System.IO; using System.Text; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; /// 以同级临时文件生成六张 SVG、六张 PNG 和 CSV 后成组发布。 public sealed class SmoothingReportExporter { private readonly SmoothingFontResolver _fontResolver = new SmoothingFontResolver(); private readonly SmoothingFigureSetBuilder _figureSetBuilder = new SmoothingFigureSetBuilder(); private readonly SmoothingSvgRenderer _svgRenderer = new SmoothingSvgRenderer(); private readonly SmoothingPngRenderer _pngRenderer = new SmoothingPngRenderer(); private readonly SmoothingCsvWriter _csvWriter = new SmoothingCsvWriter(); public SmoothingReportExportResult Export(SmoothingReportExportRequest request) { if (request == null || request.Model == null || string.IsNullOrWhiteSpace(request.OutputDirectory) || !IsFileStem(request.FileStem)) return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.InvalidInput, "报告模型、输出目录或文件名无效。"); if (!_fontResolver.TryResolve(request.ChineseFontFamilyName, request.LatinFontFamilyName, out SmoothingFontResolution availableFonts, out string fontReason)) return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.FontUnavailable, fontReason); availableFonts.Dispose(); var pending = new List(); var svgPaths = new List(); var pngPaths = new List(); string csvPath = Path.Combine(request.OutputDirectory, request.FileStem + ".csv"); try { SmoothingFigureSet figures = _figureSetBuilder.Build(request.Model); for (int index = 0; index < figures.Figures.Count; index++) { SmoothingFigureDefinition figure = figures.Figures[index]; string svgPath = Path.Combine(request.OutputDirectory, figure.FileStem + ".svg"); string pngPath = Path.Combine(request.OutputDirectory, figure.FileStem + ".png"); svgPaths.Add(svgPath); pngPaths.Add(pngPath); pending.Add(new PendingFile(svgPath, Encoding.UTF8.GetBytes(_svgRenderer.Render(figure)))); if (!_fontResolver.TryResolve(request.ChineseFontFamilyName, request.LatinFontFamilyName, out SmoothingFontResolution renderFonts, out fontReason)) throw new InvalidOperationException(fontReason); byte[] png; using (renderFonts) png = _pngRenderer.Render(figure, renderFonts); pending.Add(new PendingFile(pngPath, png)); } pending.Add(new PendingFile(csvPath, _csvWriter.Write(request.Model))); Directory.CreateDirectory(request.OutputDirectory); for (int index = 0; index < pending.Count; index++) File.WriteAllBytes(pending[index].TemporaryPath, pending[index].Content); PublishAll(pending); DeleteIfExists(Path.Combine(request.OutputDirectory, "comparison.svg")); DeleteIfExists(Path.Combine(request.OutputDirectory, "comparison.png")); return SmoothingReportExportResult.Success(svgPaths, pngPaths, csvPath); } catch (Exception exception) { RestorePublishedFiles(pending); return SmoothingReportExportResult.Failure(SmoothingReportExportStatus.Failed, exception.Message); } finally { for (int index = 0; index < pending.Count; index++) { DeleteIfExists(pending[index].TemporaryPath); DeleteIfExists(pending[index].BackupPath); } } } private static void PublishAll(IReadOnlyList files) { string transaction = Guid.NewGuid().ToString("N"); for (int index = 0; index < files.Count; index++) { PendingFile file = files[index]; file.ExistedBeforePublish = File.Exists(file.FinalPath); file.BackupPath = file.ExistedBeforePublish ? file.FinalPath + ".backup-" + transaction : string.Empty; if (file.ExistedBeforePublish) File.Replace(file.TemporaryPath, file.FinalPath, file.BackupPath); else File.Move(file.TemporaryPath, file.FinalPath); file.Published = true; } for (int index = 0; index < files.Count; index++) DeleteIfExists(files[index].BackupPath); } private static void RestorePublishedFiles(IReadOnlyList files) { for (int index = files.Count - 1; index >= 0; index--) { PendingFile file = files[index]; if (!file.Published) continue; try { if (file.ExistedBeforePublish && File.Exists(file.BackupPath)) { if (File.Exists(file.FinalPath)) File.Replace(file.BackupPath, file.FinalPath, null); else File.Move(file.BackupPath, file.FinalPath); } else if (!file.ExistedBeforePublish) DeleteIfExists(file.FinalPath); } catch { } } } private static bool IsFileStem(string value) { return !string.IsNullOrWhiteSpace(value) && value.IndexOfAny(Path.GetInvalidFileNameChars()) < 0 && value.IndexOf(Path.DirectorySeparatorChar) < 0 && value.IndexOf(Path.AltDirectorySeparatorChar) < 0; } private static void DeleteIfExists(string path) { if (!string.IsNullOrEmpty(path) && File.Exists(path)) File.Delete(path); } private sealed class PendingFile { public PendingFile(string finalPath, byte[] content) { FinalPath = finalPath; Content = content; TemporaryPath = finalPath + ".tmp"; BackupPath = string.Empty; } public string FinalPath { get; } public byte[] Content { get; } public string TemporaryPath { get; } public string BackupPath { get; set; } public bool ExistedBeforePublish { get; set; } public bool Published { get; set; } } }