diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 640ee5f..60ac9a9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ CS1591 - 3.9.0 + 3.10.0 1.0.0 enable preview diff --git a/src/Tests/SkipPdfNormalizationTests.NormalizedSnapshotHoldsTheNeutralizedBytes#00.verified.png b/src/Tests/SkipPdfNormalizationTests.NormalizedSnapshotHoldsTheNeutralizedBytes#00.verified.png new file mode 100644 index 0000000..3967adf Binary files /dev/null and b/src/Tests/SkipPdfNormalizationTests.NormalizedSnapshotHoldsTheNeutralizedBytes#00.verified.png differ diff --git a/src/Tests/SkipPdfNormalizationTests.NormalizedSnapshotHoldsTheNeutralizedBytes#01.verified.png b/src/Tests/SkipPdfNormalizationTests.NormalizedSnapshotHoldsTheNeutralizedBytes#01.verified.png new file mode 100644 index 0000000..a8539e3 Binary files /dev/null and b/src/Tests/SkipPdfNormalizationTests.NormalizedSnapshotHoldsTheNeutralizedBytes#01.verified.png differ diff --git a/src/Tests/SkipPdfNormalizationTests.NormalizedSnapshotHoldsTheNeutralizedBytes.verified.pdf b/src/Tests/SkipPdfNormalizationTests.NormalizedSnapshotHoldsTheNeutralizedBytes.verified.pdf new file mode 100644 index 0000000..b6cdb4c Binary files /dev/null and b/src/Tests/SkipPdfNormalizationTests.NormalizedSnapshotHoldsTheNeutralizedBytes.verified.pdf differ diff --git a/src/Tests/SkipPdfNormalizationTests.SkippedSnapshotHoldsTheProducerBytes#00.verified.png b/src/Tests/SkipPdfNormalizationTests.SkippedSnapshotHoldsTheProducerBytes#00.verified.png new file mode 100644 index 0000000..f5f3e4e Binary files /dev/null and b/src/Tests/SkipPdfNormalizationTests.SkippedSnapshotHoldsTheProducerBytes#00.verified.png differ diff --git a/src/Tests/SkipPdfNormalizationTests.SkippedSnapshotHoldsTheProducerBytes#01.verified.png b/src/Tests/SkipPdfNormalizationTests.SkippedSnapshotHoldsTheProducerBytes#01.verified.png new file mode 100644 index 0000000..0abd159 Binary files /dev/null and b/src/Tests/SkipPdfNormalizationTests.SkippedSnapshotHoldsTheProducerBytes#01.verified.png differ diff --git a/src/Tests/SkipPdfNormalizationTests.SkippedSnapshotHoldsTheProducerBytes.verified.pdf b/src/Tests/SkipPdfNormalizationTests.SkippedSnapshotHoldsTheProducerBytes.verified.pdf new file mode 100644 index 0000000..c8b6351 Binary files /dev/null and b/src/Tests/SkipPdfNormalizationTests.SkippedSnapshotHoldsTheProducerBytes.verified.pdf differ diff --git a/src/Tests/SkipPdfNormalizationTests.cs b/src/Tests/SkipPdfNormalizationTests.cs new file mode 100644 index 0000000..64ea43f --- /dev/null +++ b/src/Tests/SkipPdfNormalizationTests.cs @@ -0,0 +1,27 @@ +using DeterministicPdf; + +// The Tests entry for SkipPdfNormalization only proves the setting round-trips through a +// verification. These assert the wiring it is actually there for: with the setting the snapshotted +// bytes are the producer's own, and without it they are the normalized ones. +[TestFixture] +public class SkipPdfNormalizationTests +{ + [Test] + public void SampleIsNotAlreadyNormalized() + { + // The premise the pair below rests on. If sample.pdf were already byte-identical to its + // normalized form they would both pass while asserting nothing. + var raw = File.ReadAllBytes("sample.pdf"); + + Assert.That(PdfNormalizer.Normalize(raw), Is.Not.EqualTo(raw)); + } + + [Test] + public Task SkippedSnapshotHoldsTheProducerBytes() => + Verify(new MemoryStream(File.ReadAllBytes("sample.pdf")), "pdf") + .SkipPdfNormalization(); + + [Test] + public Task NormalizedSnapshotHoldsTheNeutralizedBytes() => + Verify(new MemoryStream(File.ReadAllBytes("sample.pdf")), "pdf"); +} diff --git a/src/Tests/Tests.SkipPdfNormalization#00.verified.png b/src/Tests/Tests.SkipPdfNormalization#00.verified.png new file mode 100644 index 0000000..f5f3e4e Binary files /dev/null and b/src/Tests/Tests.SkipPdfNormalization#00.verified.png differ diff --git a/src/Tests/Tests.SkipPdfNormalization#01.verified.png b/src/Tests/Tests.SkipPdfNormalization#01.verified.png new file mode 100644 index 0000000..0abd159 Binary files /dev/null and b/src/Tests/Tests.SkipPdfNormalization#01.verified.png differ diff --git a/src/Tests/Tests.SkipPdfNormalization.verified.pdf b/src/Tests/Tests.SkipPdfNormalization.verified.pdf new file mode 100644 index 0000000..c8b6351 Binary files /dev/null and b/src/Tests/Tests.SkipPdfNormalization.verified.pdf differ diff --git a/src/Tests/Tests.cs b/src/Tests/Tests.cs index 79000d8..fd941a5 100644 --- a/src/Tests/Tests.cs +++ b/src/Tests/Tests.cs @@ -160,6 +160,11 @@ public Task VerifyPdf() => public Task VerifyWebp() => VerifyFile("sample.webp"); + [Test] + public Task SkipPdfNormalization() => + VerifyFile("sample.pdf") + .SkipPdfNormalization(); + [Test] public Task VerifyPdfWithName() => Verify(targets: [new("pdf", File.OpenRead("sample.pdf"), "name")]); diff --git a/src/Verify.ImageMagick/ImageMagickSettings.cs b/src/Verify.ImageMagick/ImageMagickSettings.cs index c57c5ac..072dd0e 100644 --- a/src/Verify.ImageMagick/ImageMagickSettings.cs +++ b/src/Verify.ImageMagick/ImageMagickSettings.cs @@ -92,4 +92,36 @@ public static SettingsTask ImageMagickPdfPassword(this SettingsTask settings, st return pdfPassword; } -} \ 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["ImageMagick.SkipNormalization"] = true; + + /// + public static SettingsTask SkipPdfNormalization(this SettingsTask settings) + { + settings.CurrentSettings.SkipPdfNormalization(); + return settings; + } + + internal static bool Normalize(this IReadOnlyDictionary context) => + !context.TryGetValue("ImageMagick.SkipNormalization", out var value) || + value is not true; +} diff --git a/src/Verify.ImageMagick/VerifyImageMagick_Pdf.cs b/src/Verify.ImageMagick/VerifyImageMagick_Pdf.cs index 2127aac..3e9b85f 100644 --- a/src/Verify.ImageMagick/VerifyImageMagick_Pdf.cs +++ b/src/Verify.ImageMagick/VerifyImageMagick_Pdf.cs @@ -74,8 +74,9 @@ internal static ConversionResult Convert(string? name, Stream stream, IReadOnlyD if (includePdf) { stream.Position = 0; + var pdf = context.Normalize() ? PdfNormalizer.Normalize(stream) : CopyRemaining(stream); targets.Add( - new("pdf", PdfNormalizer.Normalize(stream), name, performConversion: false) + new("pdf", pdf, name, performConversion: false) { BypassComparersForSubsequentOnDifference = true }); @@ -84,4 +85,13 @@ internal static ConversionResult Convert(string? name, Stream stream, IReadOnlyD targets.AddRange(streams.Select(_ => new Target("png", _, name))); return new(null, targets); } -} \ No newline at end of file + + // The source stream is consumed elsewhere in this method, so the pdf target gets its own copy. + static MemoryStream CopyRemaining(Stream stream) + { + var target = new MemoryStream(); + stream.CopyTo(target); + target.Position = 0; + return target; + } +}