diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 22e61ff..640ee5f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ CS1591 - 3.8.8 + 3.9.0 1.0.0 enable preview diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 647cef0..9ee0eae 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,6 +5,7 @@ + diff --git a/src/Tests/PasswordSamples.cs b/src/Tests/PasswordSamples.cs index 15b894e..834588d 100644 --- a/src/Tests/PasswordSamples.cs +++ b/src/Tests/PasswordSamples.cs @@ -1,8 +1,21 @@ [TestFixture] public class PasswordSamples { + // A password protected pdf cannot produce a deterministic pdf target, so the pdf target is + // excluded and only the rendered pages are verified. [Test] public Task PasswordSample() => VerifyFile("password.pdf") - .ImageMagickPdfPassword("password"); -} \ No newline at end of file + .ImageMagickPdfPassword("password") + .ExcludeTargets("pdf"); + + [Test] + public void PasswordWithPdfTargetThrows() + { + var exception = Assert.ThrowsAsync( + () => VerifyFile("password.pdf") + .ImageMagickPdfPassword("password"))!; + + Assert.That(exception.Message, Does.Contain("""ExcludeTargets("pdf")""")); + } +} diff --git a/src/Tests/Tests.VerifyPdf.verified.pdf b/src/Tests/Tests.VerifyPdf.verified.pdf new file mode 100644 index 0000000..b6cdb4c Binary files /dev/null and b/src/Tests/Tests.VerifyPdf.verified.pdf differ diff --git a/src/Tests/Tests.VerifyPdfWithName#name.verified.pdf b/src/Tests/Tests.VerifyPdfWithName#name.verified.pdf new file mode 100644 index 0000000..b6cdb4c Binary files /dev/null and b/src/Tests/Tests.VerifyPdfWithName#name.verified.pdf differ diff --git a/src/Verify.ImageMagick/GlobalUsings.cs b/src/Verify.ImageMagick/GlobalUsings.cs index 29c4d80..6c12e9e 100644 --- a/src/Verify.ImageMagick/GlobalUsings.cs +++ b/src/Verify.ImageMagick/GlobalUsings.cs @@ -1,4 +1,5 @@ -global using System.Diagnostics.CodeAnalysis; +global using DeterministicPdf; +global using System.Diagnostics.CodeAnalysis; global using EmptyFiles; global using ImageMagick; global using ImageMagick.Formats; diff --git a/src/Verify.ImageMagick/Verify.ImageMagick.csproj b/src/Verify.ImageMagick/Verify.ImageMagick.csproj index af946a4..30b25d9 100644 --- a/src/Verify.ImageMagick/Verify.ImageMagick.csproj +++ b/src/Verify.ImageMagick/Verify.ImageMagick.csproj @@ -3,6 +3,7 @@ net48;net6.0;net7.0;net8.0;net9.0;net10.0 + diff --git a/src/Verify.ImageMagick/VerifyImageMagick_Pdf.cs b/src/Verify.ImageMagick/VerifyImageMagick_Pdf.cs index 68e97b4..2127aac 100644 --- a/src/Verify.ImageMagick/VerifyImageMagick_Pdf.cs +++ b/src/Verify.ImageMagick/VerifyImageMagick_Pdf.cs @@ -16,8 +16,23 @@ internal static ConversionResult Convert(string? name, Stream stream, IReadOnlyD var magickSettings = context.MagickReadSettings(); magickSettings.Format = magickFormat; var password = context.PdfPassword(); + var includePdf = !context.IsTargetExcluded("pdf"); if (password != null) { + // Checked before rendering, which is the expensive part, so the failure is immediate. + // An encrypted document cannot have a deterministic pdf snapshot: the trailer /ID seeds + // the encryption key, so neutralizing it would leave the document undecryptable. Rather + // than silently omitting the target, which would make the snapshot set differ from an + // unencrypted document for no visible reason, this is explicit. + if (includePdf) + { + throw new( + """ + A password protected pdf cannot produce a deterministic pdf target, since the trailer /ID seeds the encryption key and neutralizing it would leave the document undecryptable. + Exclude the pdf target to verify only the rendered pages: ExcludeTargets("pdf") + """); + } + magickSettings.SetDefines( new PdfReadDefines { @@ -25,6 +40,10 @@ internal static ConversionResult Convert(string? name, Stream stream, IReadOnlyD }); } + // Made seekable so the source document can be re-read for the pdf target after + // MagickImageCollection has consumed it. + stream = WrapStream(stream); + using var images = new MagickImageCollection(); images.Read(stream, magickSettings); var count = images.Count; @@ -47,6 +66,22 @@ internal static ConversionResult Convert(string? name, Stream stream, IReadOnlyD streams.Add(memoryStream); } - return new(null, streams.Select(_ => new Target("png", _, name))); + List targets = []; + + // The pdf snapshot is always the full document, regardless of PagesToInclude, which trims + // only the rendered pages above. Mirrors the svg target in ConvertSvg, which likewise emits + // the source document alongside the render. + if (includePdf) + { + stream.Position = 0; + targets.Add( + new("pdf", PdfNormalizer.Normalize(stream), name, performConversion: false) + { + BypassComparersForSubsequentOnDifference = true + }); + } + + targets.AddRange(streams.Select(_ => new Target("png", _, name))); + return new(null, targets); } } \ No newline at end of file