Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<NoWarn>CS1591</NoWarn>
<Version>3.9.0</Version>
<Version>3.10.0</Version>
<AssemblyVersion>1.0.0</AssemblyVersion>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
27 changes: 27 additions & 0 deletions src/Tests/SkipPdfNormalizationTests.cs
Original file line number Diff line number Diff line change
@@ -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");
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/Tests/Tests.SkipPdfNormalization.verified.pdf
Binary file not shown.
5 changes: 5 additions & 0 deletions src/Tests/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]);
Expand Down
34 changes: 33 additions & 1 deletion src/Verify.ImageMagick/ImageMagickSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,36 @@ public static SettingsTask ImageMagickPdfPassword(this SettingsTask settings, st

return pdfPassword;
}
}

/// <summary>
/// Snapshots the pdf bytes exactly as produced, skipping the normalization that neutralizes the
/// trailer <c>/ID</c>, the <c>/CreationDate</c> and <c>/ModDate</c>, 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.
/// </summary>
/// <remarks>
/// Only skip this when the producer is genuinely deterministic. Without it a freshly generated
/// pdf carries a wall-clock <c>/CreationDate</c> and a fresh <c>/ID</c>, so the snapshot differs
/// on every run.
/// <para>
/// 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 <c>.verified.pdf</c> even
/// though nothing about the document changed. Expect to re-accept those snapshots once.
/// </para>
/// </remarks>
public static void SkipPdfNormalization(this VerifySettings settings) =>
settings.Context["ImageMagick.SkipNormalization"] = true;

/// <inheritdoc cref="SkipPdfNormalization(VerifySettings)"/>
public static SettingsTask SkipPdfNormalization(this SettingsTask settings)
{
settings.CurrentSettings.SkipPdfNormalization();
return settings;
}

internal static bool Normalize(this IReadOnlyDictionary<string, object> context) =>
!context.TryGetValue("ImageMagick.SkipNormalization", out var value) ||
value is not true;
}
14 changes: 12 additions & 2 deletions src/Verify.ImageMagick/VerifyImageMagick_Pdf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand All @@ -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);
}
}

// 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;
}
}
Loading