diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 92240eb..85c8eb4 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ CS1591;CS0649;CA1416;NU1608;NU1109 - 5.27.0 + 5.28.0 preview 1.0.0 Aspose, Verify diff --git a/src/Verify.Aspose/VerifyAsposeSettings.cs b/src/Verify.Aspose/VerifyAsposeSettings.cs index e023c00..086cdf7 100644 --- a/src/Verify.Aspose/VerifyAsposeSettings.cs +++ b/src/Verify.Aspose/VerifyAsposeSettings.cs @@ -1,4 +1,4 @@ -namespace VerifyTestsAspose; +namespace VerifyTestsAspose; public static class VerifyAsposeSettings { @@ -67,4 +67,36 @@ internal static PngDevice GetPdfPngDevice(this IReadOnlyDictionary) value; return func(page); } -} \ No newline at end of file + + /// + /// Snapshots the pdf bytes exactly as produced, skipping the normalization that neutralizes the + /// trailer /ID, the /CreationDate and /ModDate, and the XMP dates and + /// identifiers. Use it when the producer already emits byte-deterministic documents, since + /// normalizing them again copies the whole buffer, rescans it, and — when the XMP packet is + /// canonicalized — rebuilds it and repairs the cross-reference table, all to change nothing. + /// + /// + /// Only skip this when the producer is genuinely deterministic. Without it a freshly generated + /// pdf carries a wall-clock /CreationDate and a fresh /ID, so the snapshot differs + /// on every run. + /// + /// The XMP canonicalization is worth calling out because it is the pass that changes bytes for + /// an already-deterministic producer: it collapses the packet's whitespace, so enabling or + /// disabling this setting on an existing suite shifts the stored .verified.pdf even + /// though nothing about the document changed. Expect to re-accept those snapshots once. + /// + /// + public static void SkipPdfNormalization(this VerifySettings settings) => + settings.Context["VerifyAsposeSkipPdfNormalization"] = true; + + /// + public static SettingsTask SkipPdfNormalization(this SettingsTask settings) + { + settings.CurrentSettings.SkipPdfNormalization(); + return settings; + } + + internal static bool NormalizePdf(this IReadOnlyDictionary settings) => + !settings.TryGetValue("VerifyAsposeSkipPdfNormalization", out var value) || + value is not true; +} diff --git a/src/Verify.Aspose/VerifyAspose_Pdf.cs b/src/Verify.Aspose/VerifyAspose_Pdf.cs index bc0f3cf..1fbf953 100644 --- a/src/Verify.Aspose/VerifyAspose_Pdf.cs +++ b/src/Verify.Aspose/VerifyAspose_Pdf.cs @@ -134,7 +134,7 @@ static ConversionResult ConvertPdf(string? name, Document document, IReadOnlyDic // Building the deterministic pdf is expensive, so skip it when the pdf target is excluded. if (!settings.IsTargetExcluded("pdf")) { - targets.Add(BuildPdfTarget(document)); + targets.Add(BuildPdfTarget(document, settings.NormalizePdf())); } targets.AddRange(GetPdfStreams(name, document, settings)); @@ -145,11 +145,12 @@ static ConversionResult ConvertPdf(string? name, Document document, IReadOnlyDic // The pdf snapshot is always the full document, regardless of PagesToInclude: PagesToInclude // only trims the rendered png pages in GetPdfStreams. Mirrors BuildXlsxTarget/BuildDocxTarget, // which do the same for the OOXML formats via DeterministicPackage. - static Target BuildPdfTarget(Document document) + static Target BuildPdfTarget(Document document, bool normalize) { using var source = new MemoryStream(); document.Save(source); - var resultStream = PdfNormalizer.Normalize(source); + source.Position = 0; + var resultStream = normalize ? PdfNormalizer.Normalize(source) : new MemoryStream(source.ToArray()); return new("pdf", resultStream, performConversion: false) {