using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.IO; using System.Text.RegularExpressions; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.Mapping; using MultiWheelC.TrajectoryPlanning.PathSmoothing; using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Comparison; using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; using MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; using MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization; internal static class Program { private static readonly byte[] PngSignature = { 137, 80, 78, 71, 13, 10, 26, 10 }; private static int Main(string[] arguments) { try { if (arguments.Length == 3 && arguments[0] == "--verify-local-g2-diagnostic") { VerifyLocalG2Diagnostic(arguments[1], arguments[2]); Console.WriteLine("Local G2 diagnostic visualization verification completed."); return 0; } if (arguments.Length == 4 && arguments[0] == "--export-local-g2-diagnostic") { ExportLocalG2Diagnostic(arguments[1], arguments[2], arguments[3]); return 0; } if (arguments.Length == 3 && arguments[0] == "--export-fixtures") { ExportFixtureReports(arguments[1], arguments[2]); return 0; } if (arguments.Length == 2 && arguments[0] == "--export-end-to-end") { ExportEndToEndReports(arguments[1]); return 0; } Require(arguments.Length == 1 && File.Exists(arguments[0]), "A fixture path is required."); Verify(arguments[0]); Console.WriteLine("PNG verification host completed."); return 0; } catch (Exception exception) { Console.Error.WriteLine(exception.ToString()); return 1; } } private static void ExportFixtureReports(string fixturePath, string outputDirectory) { Require(File.Exists(fixturePath), "Fixture path was not found."); PrintReports(new PathSmoothingComparisonDemo().ExportFixtureReports(fixturePath, outputDirectory)); } private static void ExportEndToEndReports(string outputDirectory) { PrintReports(new PathSmoothingComparisonDemo().ExportEndToEndReports(outputDirectory)); } private static void ExportLocalG2Diagnostic(string fixturePath, string evidencePath, string outputDirectory) { Require(File.Exists(fixturePath), "Fixture path was not found."); Require(File.Exists(evidencePath), "Diagnostic evidence path was not found."); SmoothingReportExportResult report = new LocalG2DiagnosticVisualizationDemo().Export( fixturePath, evidencePath, outputDirectory); Require(report.Status == SmoothingReportExportStatus.Success, "Diagnostic report export failed: " + report.Reason); Console.WriteLine("Diagnostic report=" + outputDirectory); } private static void VerifyLocalG2Diagnostic(string fixturePath, string evidencePath) { Require(File.Exists(fixturePath), "Fixture path was not found."); Require(File.Exists(evidencePath), "Diagnostic evidence path was not found."); string outputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-diagnostic-" + Guid.NewGuid().ToString("N")); string badEvidencePath = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-bad-" + Guid.NewGuid().ToString("N") + ".json"); string rejectedOutputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-rejected-" + Guid.NewGuid().ToString("N")); string alteredFixturePath = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-altered-fixture-" + Guid.NewGuid().ToString("N") + ".json"); string rejectedFixtureOutputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-local-g2-fixture-rejected-" + Guid.NewGuid().ToString("N")); try { SmoothingReportExportResult report = new LocalG2DiagnosticVisualizationDemo().Export( fixturePath, evidencePath, outputDirectory); Require(report.Status == SmoothingReportExportStatus.Success, "Diagnostic report export failed: " + report.Reason); Require(report.SvgPaths.Count == 5 && report.PngPaths.Count == 5 && File.Exists(report.CsvPath), "Diagnostic export must publish five SVGs, five PNGs and one CSV."); Require(Path.GetFileName(report.SvgPaths[4]) == "05-local-g2-diagnostic-candidate.svg", "Diagnostic SVG stem is incorrect."); Require(Path.GetFileName(report.PngPaths[4]) == "05-local-g2-diagnostic-candidate.png", "Diagnostic PNG stem is incorrect."); string diagnosticSvg = File.ReadAllText(report.SvgPaths[4]); Require(diagnosticSvg.Contains("data-series=\"raw\"") && diagnosticSvg.Contains("data-series=\"local-g2-diagnostic\""), "Diagnostic SVG must contain raw and diagnostic samples."); Require(diagnosticSvg.Contains("净空拒绝") && diagnosticSvg.Contains("未发布") && diagnosticSvg.Contains("严格输出=原始路径"), "Diagnostic SVG must disclose clearance rejection, publication state, and strict output boundary."); VerifyDiagnosticPointOverlay(diagnosticSvg, report.PngPaths[6]); Require(!diagnosticSvg.Contains("violation-cross"), "Clearance rejection must not be drawn as an obstacle collision."); Require(File.ReadAllText(report.CsvPath).Contains("LocalG2Quintic,Unchanged"), "CSV must retain the normal strict Local G2 row."); for (int index = 0; index < report.PngPaths.Count; index++) VerifyPng(File.ReadAllBytes(report.PngPaths[index])); Require(!ContainsTemporaryFiles(outputDirectory), "Diagnostic export must not leave temporary files."); File.Copy(fixturePath, alteredFixturePath); File.AppendAllText(alteredFixturePath, " "); bool alteredFixtureRejected = false; try { new LocalG2DiagnosticVisualizationDemo().Export(alteredFixturePath, evidencePath, rejectedFixtureOutputDirectory); } catch (Exception) { alteredFixtureRejected = true; } Require(alteredFixtureRejected, "Fixture bytes that differ from diagnostic evidence must be rejected."); Require(!Directory.Exists(rejectedFixtureOutputDirectory), "Fixture hash mismatch must not create an output directory."); File.WriteAllText(badEvidencePath, File.ReadAllText(evidencePath).Replace( "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563", "0000000000000000000000000000000000000000000000000000000000000000")); bool badEvidenceRejected = false; try { new LocalG2DiagnosticVisualizationDemo().Export(fixturePath, badEvidencePath, rejectedOutputDirectory); } catch (Exception) { badEvidenceRejected = true; } Require(badEvidenceRejected, "Tampered diagnostic evidence must be rejected."); Require(!Directory.Exists(rejectedOutputDirectory), "Rejected diagnostic evidence must not create an output directory."); } finally { if (File.Exists(alteredFixturePath)) File.Delete(alteredFixturePath); if (Directory.Exists(rejectedFixtureOutputDirectory)) Directory.Delete(rejectedFixtureOutputDirectory, true); if (File.Exists(badEvidencePath)) File.Delete(badEvidencePath); if (Directory.Exists(rejectedOutputDirectory)) Directory.Delete(rejectedOutputDirectory, true); if (Directory.Exists(outputDirectory)) Directory.Delete(outputDirectory, true); } } private static void PrintReports(IReadOnlyList scenarios) { for (int scenarioIndex = 0; scenarioIndex < scenarios.Count; scenarioIndex++) { PathSmoothingComparisonScenarioResult scenario = scenarios[scenarioIndex]; if (scenario.Comparison == null) { Console.WriteLine(scenario.ScenarioId + " coarse=" + scenario.CoarsePathStatus + " " + scenario.Diagnostic); continue; } PrintEntry(scenario.ScenarioId, scenario.Comparison.RawPathBaseline); for (int entryIndex = 0; entryIndex < scenario.Comparison.Entries.Count; entryIndex++) PrintEntry(scenario.ScenarioId, scenario.Comparison.Entries[entryIndex]); Console.WriteLine(scenario.ScenarioId + " report=" + scenario.Report.Status + " " + scenario.Report.Reason); } } private static void PrintEntry(string scenarioId, PathSmoothingComparisonEntry entry) { Console.WriteLine(string.Format( CultureInfo.InvariantCulture, "{0} {1} status={2} length={3:F4} peakKappa={4:F4} clearance={5:F4}", scenarioId, entry.IsRawPathBaseline ? "RawPath" : entry.Method.ToString(), entry.Status, entry.Metrics.PathLengthMeters, entry.Metrics.MaximumAbsoluteVehicleCurvaturePerMeter, entry.Metrics.MinimumBodyClearanceMeters)); } private static void Verify(string fixturePath) { PathSmoothingComparisonRequest request = CreateComparisonRequest(); PathSmoothingComparisonResult comparison = new PathSmoothingComparisonService().Compare(request); Require(!comparison.IsCancelled, "Fixture comparison was cancelled."); Require(request.SmoothingRequest.CoarsePath.Count > 1, "Fixture coarse path is too short."); CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0]; CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1]; var model = new SmoothingFigureModelBuilder().Build( comparison, request.SmoothingRequest.Map, new Pose2D(first.X, first.Y, first.Heading), new Pose2D(last.X, last.Y, last.Heading), "straight", "Straight"); VerifyObstacleRectanglesAreMerged(fixturePath, comparison, new Pose2D(first.X, first.Y, first.Heading), new Pose2D(last.X, last.Y, last.Heading)); VerifyFourFigureDefinitionContract(model); VerifyNormalLocalG2Series(model); VerifyPointOnlyRendererContract(model); var resolver = new SmoothingFontResolver(); Require(resolver.TryResolve("SimSun", "Times New Roman", out SmoothingFontResolution fonts, out string fontReason), "Required report fonts are unavailable: " + fontReason); byte[] png; using (fonts) { Require(fonts.ChineseFamilyName == "SimSun", "Chinese font must resolve to exact SimSun family."); Require(fonts.LatinFamilyName == "Times New Roman", "Latin font must resolve to exact Times New Roman family."); Require(resolver.MeasureMixedText(fonts, "粗路径 κ(s) X (m) −π", 9f).Width > 0f, "Mixed Chinese/Latin sample must have nonempty measured bounds."); png = new SmoothingPngRenderer().Render(model, fonts); } VerifyPng(png); string outputDirectory = Path.Combine(Path.GetTempPath(), "path-smoothing-png-" + Guid.NewGuid().ToString("N")); try { var exporter = new SmoothingReportExporter(); SmoothingReportExportResult report = exporter.Export(new SmoothingReportExportRequest { Model = model, OutputDirectory = outputDirectory, FileStem = "straight-report", }); Require(report.Status == SmoothingReportExportStatus.Success, "Report export failed: " + report.Reason); Require(report.SvgPaths.Count == 4 && report.PngPaths.Count == 4 && File.Exists(report.CsvPath), "Successful export must publish four SVGs, four PNGs and one CSV."); VerifyPublishedFourFigureFiles(report, outputDirectory); Require(!ContainsTemporaryFiles(outputDirectory), "Successful export must not leave temporary files."); string unavailableDirectory = Path.Combine(outputDirectory, "missing-font"); SmoothingReportExportResult unavailable = exporter.Export(new SmoothingReportExportRequest { Model = model, OutputDirectory = unavailableDirectory, FileStem = "must-not-write", ChineseFontFamilyName = "Missing report font", LatinFontFamilyName = "Times New Roman", }); Require(unavailable.Status == SmoothingReportExportStatus.FontUnavailable, "Missing exact font must report FontUnavailable."); Require(!Directory.Exists(unavailableDirectory), "Missing-font export must remain atomic and create no output directory."); } finally { if (Directory.Exists(outputDirectory)) Directory.Delete(outputDirectory, true); } } private static PathSmoothingComparisonRequest CreateComparisonRequest() { PlanningMapBuildResult mapResult = new PlanningMapFactory().Create(new PlanningMapRequest { Bounds = new MapBoundsMm(0f, 5000f, 0f, 5000f), ResolutionMm = 50f, AllowExplicitEmptyMap = true, }); Require(mapResult.Succeeded && mapResult.Map != null, "PNG verification map must be created."); var vehicle = new VehicleParameters { LengthMeters = 0.20d, WidthMeters = 0.20d, SafetyMarginMeters = 0d, MaximumCurvaturePerMeter = 100d, }; var coarsePath = new List { CreatePoint(0.5d, 0.5d, 0d), CreatePoint(1.0d, 0.5d, 0.5d), CreatePoint(1.0d, 1.0d, 1.0d), CreatePoint(1.5d, 1.0d, 1.5d), }; var segments = new List { new PathSegment(0, TravelDirection.Forward, 0, coarsePath.Count - 1, false, false), }; return new PathSmoothingComparisonRequest(new PathSmoothingRequest( coarsePath, segments, mapResult.Map, vehicle, new PathSmoothingConfiguration())); } private static void VerifyFourFigureDefinitionContract(SmoothingFigureModel model) { SmoothingFigureSet figureSet = new SmoothingFigureSetBuilder().Build(model); var stems = new List(); for (int index = 0; index < figureSet.Figures.Count; index++) stems.Add(figureSet.Figures[index].FileStem); string expected = string.Join(",", new[] { "01-coarse-path-overview", "02-all-paths-comparison", "03-local-g2-overview", "04-curvature-comparison", }); Require(string.Join(",", stems) == expected, "Four-figure report stems must be stable and ordered."); Require(!figureSet.Figures[1].ShowsMapContext, "All-path comparison must contain only trajectories, axes and legend."); Require(figureSet.Figures[2].ShowsMapContext && figureSet.Figures[2].Series[0].Opacity < 1d, "Local G2 figure must retain a faded raw-path map reference."); Require(figureSet.Figures[0].WorldScalePointsPerMeter > 0d && Math.Abs((figureSet.Figures[0].WorldXMaxMeters - figureSet.Figures[0].WorldXMinMeters) / (figureSet.Figures[0].WorldYMaxMeters - figureSet.Figures[0].WorldYMinMeters) - figureSet.Figures[0].PlotWidthPoints / figureSet.Figures[0].PlotHeightPoints) < 0.000001d, "Overhead figures must preserve equal X/Y scale."); Require(figureSet.Figures[0].LegendYPoints - (figureSet.Figures[0].PlotYPoints + figureSet.Figures[0].PlotHeightPoints + 31d) >= 8d, "Legend must leave vertical clearance below the X-axis unit label."); } private static void VerifyNormalLocalG2Series(SmoothingFigureModel model) { Require(model.Series.Count == 2, "Normal comparison model must contain only raw-path and Local G2 series."); SmoothingFigureSeries localG2 = null; int count = 0; for (int index = 0; index < model.Series.Count; index++) { if (model.Series[index].Key != "local-g2") continue; localG2 = model.Series[index]; count++; } Require(count == 1, "Normal comparison model must include exactly one Local G2 series."); Require(Enum.IsDefined(typeof(PathSmoothingStatus), localG2.Status), "Local G2 must retain a defined smoothing status."); if (localG2.IsCurveVisible) Require(localG2.Points.Count >= 2, "Visible Local G2 output must include at least two samples."); } private static void VerifyObstacleRectanglesAreMerged(string fixturePath, PathSmoothingComparisonResult comparison, Pose2D start, Pose2D goal) { IReadOnlyList fixtures = SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath); IReadOnlyList requests = SmoothingScenarioFactory.CreateFixtureRequests(fixturePath); int rectangleIndex = -1; for (int index = 0; index < fixtures.Count; index++) { if (fixtures[index].Id == "rectangle-detour") { rectangleIndex = index; break; } } Require(rectangleIndex >= 0, "Fixture suite must include rectangle-detour for obstacle rendering verification."); SmoothingFigureModel model = new SmoothingFigureModelBuilder().Build( comparison, requests[rectangleIndex].SmoothingRequest.Map, start, goal, "obstacle", "obstacle"); Require(model.Obstacles.Count > 0, "Rectangle-detour fixture must produce report obstacles."); for (int firstIndex = 0; firstIndex < model.Obstacles.Count; firstIndex++) { SmoothingFigureObstacle first = model.Obstacles[firstIndex]; for (int secondIndex = firstIndex + 1; secondIndex < model.Obstacles.Count; secondIndex++) { SmoothingFigureObstacle second = model.Obstacles[secondIndex]; bool matchingColumn = Math.Abs(first.X - second.X) < 0.0000001d && Math.Abs(first.Width - second.Width) < 0.0000001d; bool verticallyAdjacent = Math.Abs((first.Y + first.Height) - second.Y) < 0.0000001d || Math.Abs((second.Y + second.Height) - first.Y) < 0.0000001d; Require(!(matchingColumn && verticallyAdjacent), "Adjacent occupied rows must merge into a single obstacle rectangle."); } } } private static void VerifyPointOnlyRendererContract(SmoothingFigureModel model) { SmoothingFigureDefinition figure = new SmoothingFigureSetBuilder().Build(model).Figures[1]; string svg = new SmoothingSvgRenderer().Render(figure); Require(svg.Contains("class=\"trajectory-point\""), "Trajectory samples must render as discrete SVG point markers."); Require(!svg.Contains("stroke-dasharray") && !svg.Contains("-path\""), "Trajectory SVG output must not use dashed or joined path strokes."); Require(svg.Contains("X (m)") && svg.Contains("Y (m)"), "Overhead SVG must label metre coordinate axes."); } private static void VerifyDiagnosticPointOverlay(string svg, string pngPath) { const string rawHeader = ""; const string diagnosticHeader = ""; Require(svg.Contains(rawHeader), "Diagnostic raw series must render as opaque gray outer dots."); Require(svg.Contains(diagnosticHeader), "Diagnostic candidate must render as opaque red inner dots."); IReadOnlyList rawPoints = ReadSvgTrajectoryPoints(svg, "raw"); IReadOnlyList diagnosticPoints = ReadSvgTrajectoryPoints(svg, "local-g2-diagnostic"); Require(rawPoints.Count == 88, "Diagnostic SVG must retain the original 88 raw sample centers."); Require(diagnosticPoints.Count == 88, "Diagnostic SVG must retain the original 88 candidate sample centers."); for (int index = 0; index < rawPoints.Count; index++) Require(rawPoints[index].Radius == "2.1", "Diagnostic raw points must use a 2.1 point radius."); for (int index = 0; index < diagnosticPoints.Count; index++) Require(diagnosticPoints[index].Radius == "1.35", "Diagnostic candidate points must use a 1.35 point radius."); var diagnosticIndicesByCenter = new Dictionary(StringComparer.Ordinal); for (int index = 0; index < diagnosticPoints.Count; index++) diagnosticIndicesByCenter[CenterKey(diagnosticPoints[index])] = index; int sharedCount = 0; bool hasNonEndpointSharedCenter = false; for (int index = 0; index < rawPoints.Count; index++) { if (!diagnosticIndicesByCenter.TryGetValue(CenterKey(rawPoints[index]), out int diagnosticIndex)) continue; sharedCount++; if (index > 0 && index < rawPoints.Count - 1 && diagnosticIndex > 0 && diagnosticIndex < diagnosticPoints.Count - 1) hasNonEndpointSharedCenter = true; } Require(sharedCount == 81 && hasNonEndpointSharedCenter, "Diagnostic overlay must retain exact shared non-endpoint raw and candidate centers without coordinate offsets."); VerifyDiagnosticOverlayPixels(pngPath, rawPoints, diagnosticPoints, diagnosticIndicesByCenter); } private static IReadOnlyList ReadSvgTrajectoryPoints(string svg, string seriesKey) { string marker = "data-series=\"" + seriesKey + "\""; int seriesMarker = svg.IndexOf(marker, StringComparison.Ordinal); Require(seriesMarker >= 0, "Diagnostic SVG series is missing: " + seriesKey); int seriesStart = svg.LastIndexOf("", seriesMarker, StringComparison.Ordinal); Require(seriesStart >= 0 && seriesEnd > seriesStart, "Diagnostic SVG series markup is invalid: " + seriesKey); string series = svg.Substring(seriesStart, seriesEnd - seriesStart); MatchCollection matches = Regex.Matches(series, "[^\"]+)\" cy=\"(?[^\"]+)\" r=\"(?[^\"]+)\"/>", RegexOptions.CultureInvariant); var points = new List(matches.Count); for (int index = 0; index < matches.Count; index++) points.Add(new SvgTrajectoryPoint(matches[index].Groups["x"].Value, matches[index].Groups["y"].Value, matches[index].Groups["r"].Value)); return points; } private static void VerifyDiagnosticOverlayPixels( string pngPath, IReadOnlyList rawPoints, IReadOnlyList diagnosticPoints, IReadOnlyDictionary diagnosticIndicesByCenter) { const double pixelsPerPoint = 600d / 72d; const double annulusRadiusPoints = 1.70d; using (var bitmap = new Bitmap(pngPath)) { for (int rawIndex = 1; rawIndex < rawPoints.Count - 1; rawIndex++) { SvgTrajectoryPoint raw = rawPoints[rawIndex]; if (!diagnosticIndicesByCenter.TryGetValue(CenterKey(raw), out int diagnosticIndex) || diagnosticIndex == 0 || diagnosticIndex == diagnosticPoints.Count - 1) continue; int centerX = (int)Math.Round(raw.XPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); int centerY = (int)Math.Round(raw.YPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); if (!IsDiagnosticRed(bitmap.GetPixel(centerX, centerY))) continue; for (int angleIndex = 0; angleIndex < 24; angleIndex++) { double angle = 2d * Math.PI * angleIndex / 24d; int annulusX = (int)Math.Round(centerX + Math.Cos(angle) * annulusRadiusPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); int annulusY = (int)Math.Round(centerY + Math.Sin(angle) * annulusRadiusPoints * pixelsPerPoint, MidpointRounding.AwayFromZero); if (annulusX < 0 || annulusX >= bitmap.Width || annulusY < 0 || annulusY >= bitmap.Height) continue; if (IsRawGray(bitmap.GetPixel(annulusX, annulusY))) return; } } } throw new InvalidOperationException("A shared non-endpoint sample must have a red center and gray annulus."); } private static bool IsDiagnosticRed(Color color) { return color.R >= 140 && color.G <= 95 && color.B <= 100 && color.R >= color.G + 60; } private static bool IsRawGray(Color color) { return color.R >= 45 && color.R <= 110 && Math.Abs(color.R - color.G) <= 8 && Math.Abs(color.G - color.B) <= 8; } private static string CenterKey(SvgTrajectoryPoint point) { return point.X + "|" + point.Y; } private readonly struct SvgTrajectoryPoint { public SvgTrajectoryPoint(string x, string y, string radius) { X = x; Y = y; Radius = radius; XPoints = double.Parse(x, CultureInfo.InvariantCulture); YPoints = double.Parse(y, CultureInfo.InvariantCulture); } public string X { get; } public string Y { get; } public string Radius { get; } public double XPoints { get; } public double YPoints { get; } } private static void VerifyPublishedFourFigureFiles(SmoothingReportExportResult report, string outputDirectory) { var expectedStems = new[] { "01-coarse-path-overview", "02-all-paths-comparison", "03-local-g2-overview", "04-curvature-comparison", }; VerifyPublishedPaths(report.SvgPaths, expectedStems, ".svg", outputDirectory); VerifyPublishedPaths(report.PngPaths, expectedStems, ".png", outputDirectory); Require(!File.Exists(Path.Combine(outputDirectory, "comparison.svg")) && !File.Exists(Path.Combine(outputDirectory, "comparison.png")), "Four-figure export must not leave legacy composite comparison images."); } private static void VerifyPublishedPaths(IReadOnlyList paths, string[] expectedStems, string extension, string outputDirectory) { var actual = new List(); for (int index = 0; index < paths.Count; index++) actual.Add(paths[index]); Require(actual.Count == expectedStems.Length, "Four-figure export must publish four " + extension + " files."); for (int index = 0; index < expectedStems.Length; index++) { string expected = Path.Combine(outputDirectory, expectedStems[index] + extension); Require(actual[index] == expected && File.Exists(actual[index]), "Published " + extension + " path must match the stable figure stem."); } } private static CoarsePathPoint CreatePoint(double x, double y, double arcLength) { return new CoarsePathPoint( x, y, 0d, 0d, arcLength, TravelDirection.Forward, 0d, 1d, false, CoarsePathPointSource.Start); } private static bool ContainsTemporaryFiles(string directory) { foreach (string ignored in Directory.EnumerateFiles(directory, "*.tmp")) return true; return false; } private static void VerifyPng(byte[] png) { Require(png != null && png.Length > PngSignature.Length, "PNG output is empty."); byte[] data = png ?? throw new InvalidOperationException("PNG output is empty."); for (int index = 0; index < PngSignature.Length; index++) Require(data[index] == PngSignature[index], "PNG signature is invalid."); bool sawHeader = false; bool sawPhysicalResolution = false; int offset = PngSignature.Length; while (offset < data.Length) { Require(offset + 12 <= data.Length, "PNG chunk header is truncated."); int length = checked((int)ReadUInt32BigEndian(data, offset)); int dataStart = offset + 8; int crcStart = checked(dataStart + length); Require(crcStart + 4 <= data.Length, "PNG chunk data is truncated."); uint expectedCrc = ReadUInt32BigEndian(data, crcStart); uint actualCrc = ComputeCrc32(data, offset + 4, length + 4); Require(expectedCrc == actualCrc, "PNG chunk CRC is invalid."); string type = System.Text.Encoding.ASCII.GetString(data, offset + 4, 4); if (type == "IHDR") { Require(length == 13, "IHDR length must be 13."); Require(ReadUInt32BigEndian(data, dataStart) == SmoothingPngRenderer.WidthPixels, "PNG width must be 4296."); Require(ReadUInt32BigEndian(data, dataStart + 4) == SmoothingPngRenderer.HeightPixels, "PNG height must be 3120."); sawHeader = true; } else if (type == "pHYs") { Require(length == 9, "pHYs length must be 9."); Require(ReadUInt32BigEndian(data, dataStart) == SmoothingPngRenderer.PixelsPerMeter, "PNG horizontal density must be 23622 pixels/meter."); Require(ReadUInt32BigEndian(data, dataStart + 4) == SmoothingPngRenderer.PixelsPerMeter, "PNG vertical density must be 23622 pixels/meter."); Require(data[dataStart + 8] == 1, "PNG pHYs unit must be meter."); sawPhysicalResolution = true; } offset = crcStart + 4; } Require(sawHeader && sawPhysicalResolution, "PNG must contain IHDR and pHYs chunks."); } private static uint ReadUInt32BigEndian(byte[] data, int offset) { return ((uint)data[offset] << 24) | ((uint)data[offset + 1] << 16) | ((uint)data[offset + 2] << 8) | data[offset + 3]; } private static uint ComputeCrc32(byte[] data, int offset, int length) { uint crc = 0xffffffffu; for (int index = 0; index < length; index++) { crc ^= data[offset + index]; for (int bit = 0; bit < 8; bit++) crc = (crc & 1u) == 0u ? crc >> 1 : (crc >> 1) ^ 0xedb88320u; } return crc ^ 0xffffffffu; } private static void Require(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); } }