From 96c612039e964c5e59583445c8f53cde82dc40d8 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 23 Jul 2026 19:40:35 +1000 Subject: [PATCH] Add option to skip pdf byte normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the SkipPdfNormalization setting from Verify.PDFium (3fd22e9). Adds a SettingsTask/VerifySettings extension backed by a context key, so a caller whose producer already emits byte-deterministic documents can snapshot the bytes as produced. Normalizing those again copies the buffer, rescans it, and — when the XMP packet is canonicalized — rebuilds it and repairs the cross-reference table, all to change nothing. Default behaviour is unchanged: without the setting the bytes are normalized. SkipPdfNormalizationTests asserts the pairing rather than just that the setting round-trips: the skipped snapshot holds the producer's own bytes and the default one holds the neutralized bytes. A premise test pins that the sample is not already normalization-invariant, so the pair cannot both pass vacuously. --- src/Directory.Build.props | 2 +- src/Verify.Aspose/VerifyAsposeSettings.cs | 36 +++++++++++++++++++++-- src/Verify.Aspose/VerifyAspose_Pdf.cs | 7 +++-- 3 files changed, 39 insertions(+), 6 deletions(-) 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) {