From aab4d4bc690809f0fc8558dcee1671bba61bcffd Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Mon, 5 Oct 2026 22:38:01 +0200 Subject: [PATCH 1/3] Updated NuGet packages Bumps Microsoft.SourceLink.GitHub to resolve the NU1902 audit warning (Microsoft.Build.Tasks.Git advisory) that failed restore, plus test and sample package updates. --- Directory.Packages.props | 17 +++++------------ .../Synercoding.FileFormats.Pdf.Tests.csproj | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 934cc8a..847ff20 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,36 +1,29 @@ - true true all - https://api.nuget.org/v3/index.json; - - - - - + + - + - - + - - + \ No newline at end of file diff --git a/tests/Synercoding.FileFormats.Pdf.Tests/Synercoding.FileFormats.Pdf.Tests.csproj b/tests/Synercoding.FileFormats.Pdf.Tests/Synercoding.FileFormats.Pdf.Tests.csproj index 72dfee8..e926191 100644 --- a/tests/Synercoding.FileFormats.Pdf.Tests/Synercoding.FileFormats.Pdf.Tests.csproj +++ b/tests/Synercoding.FileFormats.Pdf.Tests/Synercoding.FileFormats.Pdf.Tests.csproj @@ -22,13 +22,13 @@ PreserveNewest - + PreserveNewest PreserveNewest - + PreserveNewest @@ -50,5 +50,19 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + From 8c2b1b98489b13e6772d257ca4f1c31e1335e5f9 Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Mon, 5 Oct 2026 22:38:03 +0200 Subject: [PATCH 2/3] Fix FlateDecode trailer, add subset font tags and OverprintMode - FlateDecode now uses ZLibStream so the Adler-32 trailer is written; previously only a hand-written zlib header plus raw deflate data was emitted, which Ghostscript reports as "incorrect data check". - Subset fonts get a deterministic six-letter tag prefix (ABCDEF+Name) on the Type0 BaseFont, CIDFont BaseFont and FontDescriptor FontName, as required by ISO 32000-2 9.9.2. - ExtendedGraphicsState supports OverprintMode (/OPM). --- .../Content/ExtendedGraphicsState.cs | 7 ++++ .../Content/OverprintMode.cs | 17 +++++++++ .../Content/Text/Fonts/CID/CIDFontWriter.cs | 32 ++++++++++++---- .../IO/Filters/FlateDecode.cs | 21 ++-------- .../Primitives/PdfName.cs | 1 + .../Primitives/PdfNames.cs | 2 + .../TrueType/FontSubsettingVisualTests.cs | 24 ++++++++++++ .../IO/Filters/FlateDecodeTests.cs | 38 +++++++++++++++++++ 8 files changed, 117 insertions(+), 25 deletions(-) create mode 100644 src/Synercoding.FileFormats.Pdf/Content/OverprintMode.cs create mode 100644 tests/Synercoding.FileFormats.Pdf.Tests/IO/Filters/FlateDecodeTests.cs diff --git a/src/Synercoding.FileFormats.Pdf/Content/ExtendedGraphicsState.cs b/src/Synercoding.FileFormats.Pdf/Content/ExtendedGraphicsState.cs index d9efaad..89c93be 100644 --- a/src/Synercoding.FileFormats.Pdf/Content/ExtendedGraphicsState.cs +++ b/src/Synercoding.FileFormats.Pdf/Content/ExtendedGraphicsState.cs @@ -23,6 +23,11 @@ public sealed record class ExtendedGraphicsState /// public bool? OverprintNonStroking { get; init; } + /// + /// The overprint mode, which determines how overprinting is applied when painting in a DeviceCMYK color space. + /// + public OverprintMode? OverprintMode { get; init; } + /// /// The current stroking alpha constant, specifying the constant shape or constant opacity value /// to be used for stroking operations in the transparent imaging model. @@ -71,6 +76,8 @@ internal IPdfDictionary ToPdfDictionary() dictionary[PdfNames.OP] = new PdfBoolean(Overprint.Value); if (OverprintNonStroking.HasValue) dictionary[PdfNames.op] = new PdfBoolean(OverprintNonStroking.Value); + if (OverprintMode.HasValue) + dictionary[PdfNames.OPM] = new PdfNumber((int)OverprintMode.Value); if (CurrentAlphaConstantStroking.HasValue) dictionary[PdfNames.CA] = new PdfNumber(CurrentAlphaConstantStroking.Value); if (CurrentAlphaConstantNonStroking.HasValue) diff --git a/src/Synercoding.FileFormats.Pdf/Content/OverprintMode.cs b/src/Synercoding.FileFormats.Pdf/Content/OverprintMode.cs new file mode 100644 index 0000000..5dd591d --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/Content/OverprintMode.cs @@ -0,0 +1,17 @@ +namespace Synercoding.FileFormats.Pdf.Content; + +/// +/// Specifies how overprinting is applied when painting in a DeviceCMYK color space. +/// +public enum OverprintMode +{ + /// + /// Each source color component value replaces the value previously painted for the corresponding device colorant, + /// regardless of what the new value is. + /// + Standard = 0, + /// + /// A tint value of 0.0 for a source color component leaves the corresponding component of the previously painted color unchanged. + /// + NonZero = 1 +} diff --git a/src/Synercoding.FileFormats.Pdf/Content/Text/Fonts/CID/CIDFontWriter.cs b/src/Synercoding.FileFormats.Pdf/Content/Text/Fonts/CID/CIDFontWriter.cs index 89de86f..ae2b51c 100644 --- a/src/Synercoding.FileFormats.Pdf/Content/Text/Fonts/CID/CIDFontWriter.cs +++ b/src/Synercoding.FileFormats.Pdf/Content/Text/Fonts/CID/CIDFontWriter.cs @@ -32,12 +32,17 @@ public static void WriteType0Font(ObjectWriter writer, PdfObjectId fontId, Font PdfObjectId? cidToGidMapId = null; Dictionary? cidToGidMap = null; + var baseFontName = ttFont.FontName; + // Write CIDSystemInfo _writeCIDSystemInfo(writer, cidSystemInfoId); // Handle font subsetting if enabled and we have tracked usage if (writerSettings.EnableSubsetting && tracker.HasUsage) { + // Subset fonts must have their name prefixed with a tag (ISO 32000-2, 9.9.2) + baseFontName = _getSubsetTag(ttFont, tracker) + "+" + ttFont.FontName; + var subsetter = new TrueType.FontSubsetter(); var result = subsetter.CreateSubset(ttFont, tracker); @@ -58,10 +63,10 @@ public static void WriteType0Font(ObjectWriter writer, PdfObjectId fontId, Font } // Write font descriptor - _writeFontDescriptor(writer, fontDescriptorId, ttFont, fontFileId); + _writeFontDescriptor(writer, fontDescriptorId, ttFont, baseFontName, fontFileId); // Write CID font (descendant font) with optional CIDToGIDMap - _writeCIDFont(writer, descendantFontId, ttFont, fontDescriptorId, cidSystemInfoId, tracker, cidToGidMapId); + _writeCIDFont(writer, descendantFontId, ttFont, baseFontName, fontDescriptorId, cidSystemInfoId, tracker, cidToGidMapId); // Write CIDToGIDMap if needed if (cidToGidMapId != null && cidToGidMap != null) @@ -77,7 +82,7 @@ public static void WriteType0Font(ObjectWriter writer, PdfObjectId fontId, Font { [PdfNames.Type] = PdfNames.Font, [PdfNames.Subtype] = PdfNames.Type0, - [PdfNames.BaseFont] = PdfName.Get(ttFont.FontName), + [PdfNames.BaseFont] = PdfName.Get(baseFontName), [PdfNames.Encoding] = PdfNames.IdentityH, [PdfNames.DescendantFonts] = new PdfArray() { descendantFontId.GetReference() }, [PdfNames.ToUnicode] = toUnicodeId.GetReference() @@ -152,7 +157,20 @@ private static void _writeFontFileWithSubsetting(ObjectWriter writer, PdfObjectI }); } - private static void _writeFontDescriptor(ObjectWriter writer, PdfObjectId descriptorId, TrueTypeFont font, PdfObjectId fontFileId) + private static string _getSubsetTag(TrueTypeFont font, FontUsageTracker tracker) + { + // Deterministic tag derived from the font name and the used characters, so identical input gives identical output. + var input = font.FontName + "|" + new string(tracker.UsedCharacters.Order().ToArray()); + var hash = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(input)); + + Span tag = stackalloc char[6]; + for (int i = 0; i < tag.Length; i++) + tag[i] = (char)( 'A' + ( hash[i] % 26 ) ); + + return new string(tag); + } + + private static void _writeFontDescriptor(ObjectWriter writer, PdfObjectId descriptorId, TrueTypeFont font, string fontName, PdfObjectId fontFileId) { var tables = font.Tables; var head = tables.Head!; @@ -170,7 +188,7 @@ private static void _writeFontDescriptor(ObjectWriter writer, PdfObjectId descri var descriptor = new PdfDictionary() { [PdfNames.Type] = PdfNames.FontDescriptor, - [PdfNames.FontName] = PdfName.Get(font.FontName), + [PdfNames.FontName] = PdfName.Get(fontName), [PdfNames.Flags] = new PdfNumber(flags), [PdfNames.FontBBox] = new PdfArray { @@ -194,14 +212,14 @@ private static void _writeFontDescriptor(ObjectWriter writer, PdfObjectId descri }); } - private static void _writeCIDFont(ObjectWriter writer, PdfObjectId cidFontId, TrueTypeFont font, + private static void _writeCIDFont(ObjectWriter writer, PdfObjectId cidFontId, TrueTypeFont font, string baseFontName, PdfObjectId fontDescriptorId, PdfObjectId cidSystemInfoId, FontUsageTracker tracker, PdfObjectId? cidToGidMapId = null) { var cidFont = new PdfDictionary() { [PdfNames.Type] = PdfNames.Font, [PdfNames.Subtype] = PdfNames.CIDFontType2, - [PdfNames.BaseFont] = PdfName.Get(font.FontName), + [PdfNames.BaseFont] = PdfName.Get(baseFontName), [PdfNames.CIDSystemInfo] = cidSystemInfoId.GetReference(), [PdfNames.FontDescriptor] = fontDescriptorId.GetReference(), [PdfNames.DW] = new PdfNumber(1000) // Default width diff --git a/src/Synercoding.FileFormats.Pdf/IO/Filters/FlateDecode.cs b/src/Synercoding.FileFormats.Pdf/IO/Filters/FlateDecode.cs index c6268ee..23816f2 100644 --- a/src/Synercoding.FileFormats.Pdf/IO/Filters/FlateDecode.cs +++ b/src/Synercoding.FileFormats.Pdf/IO/Filters/FlateDecode.cs @@ -33,25 +33,10 @@ public byte[] Encode(byte[] input, IPdfDictionary? parameters) using (var outputStream = new MemoryStream()) { - const CompressionLevel LEVEL = CompressionLevel.SmallestSize; - - var (method, flags) = _getHeader(LEVEL); - outputStream.WriteByte(method); - outputStream.WriteByte(flags); - - using (var flateStream = new DeflateStream(outputStream, LEVEL, leaveOpen: true)) - flateStream.Write(input); + // ZLibStream writes both the zlib header and the Adler-32 trailer, which FlateDecode requires. + using (var zlibStream = new ZLibStream(outputStream, CompressionLevel.SmallestSize, leaveOpen: true)) + zlibStream.Write(input); return outputStream.ToArray(); } } - - private (byte CompressionMethod, byte Flags) _getHeader(CompressionLevel compressionLevel) - => compressionLevel switch - { - CompressionLevel.Optimal => (0x78, 0x9C), - CompressionLevel.Fastest => (0x78, 0x5E), - CompressionLevel.SmallestSize => (0x78, 0xDA), - CompressionLevel.NoCompression => (0x78, 0x01), - var level => throw new NotImplementedException("Unsupported compression level: {level}") - }; } diff --git a/src/Synercoding.FileFormats.Pdf/Primitives/PdfName.cs b/src/Synercoding.FileFormats.Pdf/Primitives/PdfName.cs index 4c12755..b267b30 100644 --- a/src/Synercoding.FileFormats.Pdf/Primitives/PdfName.cs +++ b/src/Synercoding.FileFormats.Pdf/Primitives/PdfName.cs @@ -108,6 +108,7 @@ public sealed class PdfName : IPdfPrimitive, IEquatable { "OneColumn", new PdfName("OneColumn") }, { "OP", new PdfName("OP") }, { "op", new PdfName("op") }, + { "OPM", new PdfName("OPM") }, { "Ordering", new PdfName("Ordering") }, { "Overlay", new PdfName("Overlay") }, { "P", new PdfName("P") }, diff --git a/src/Synercoding.FileFormats.Pdf/Primitives/PdfNames.cs b/src/Synercoding.FileFormats.Pdf/Primitives/PdfNames.cs index 53fd2a7..b03e236 100644 --- a/src/Synercoding.FileFormats.Pdf/Primitives/PdfNames.cs +++ b/src/Synercoding.FileFormats.Pdf/Primitives/PdfNames.cs @@ -212,6 +212,8 @@ public static PdfName OP => PdfName.Get(nameof(OP)); public static PdfName op => PdfName.Get(nameof(op)); + public static PdfName OPM + => PdfName.Get(nameof(OPM)); public static PdfName Ordering => PdfName.Get(nameof(Ordering)); public static PdfName Overlay diff --git a/tests/Synercoding.FileFormats.Pdf.Tests/Content/Text/Fonts/TrueType/FontSubsettingVisualTests.cs b/tests/Synercoding.FileFormats.Pdf.Tests/Content/Text/Fonts/TrueType/FontSubsettingVisualTests.cs index cec526a..a8753e8 100644 --- a/tests/Synercoding.FileFormats.Pdf.Tests/Content/Text/Fonts/TrueType/FontSubsettingVisualTests.cs +++ b/tests/Synercoding.FileFormats.Pdf.Tests/Content/Text/Fonts/TrueType/FontSubsettingVisualTests.cs @@ -112,6 +112,30 @@ public void Test_SubsetFont_RepeatedCharacters_VisuallyEqual() $"Subset PDF should be smaller. Full: {fullFontPdf.Length}, Subset: {subsetFontPdf.Length}"); } + [Fact] + public void Test_SubsetFont_FontNames_HaveSubsetTag() + { + var fontName = Font.Load(_testFontPath).FontName; + var pdf = System.Text.Encoding.ASCII.GetString(_createPdfWithFont("Tagged", enableSubsetting: true)); + + var matches = System.Text.RegularExpressions.Regex.Matches(pdf, @"/(?:BaseFont|FontName) */([^\s/<>\[\]]+)"); + + // Type0 BaseFont, CIDFont BaseFont and FontDescriptor FontName + Assert.Equal(3, matches.Count); + var names = matches.Select(m => m.Groups[1].Value).Distinct().ToArray(); + var name = Assert.Single(names); + Assert.Matches(@"^[A-Z]{6}\+", name); + Assert.EndsWith(fontName, name); + } + + [Fact] + public void Test_FullFont_FontNames_HaveNoSubsetTag() + { + var pdf = System.Text.Encoding.ASCII.GetString(_createPdfWithFont("Untagged", enableSubsetting: false)); + + Assert.DoesNotMatch(@"/(?:BaseFont|FontName) */[A-Z]{6}\+", pdf); + } + /// /// Creates a PDF with the specified text and subsetting setting. /// Based on _writeTextOnlyPdf from ConsoleTester. diff --git a/tests/Synercoding.FileFormats.Pdf.Tests/IO/Filters/FlateDecodeTests.cs b/tests/Synercoding.FileFormats.Pdf.Tests/IO/Filters/FlateDecodeTests.cs new file mode 100644 index 0000000..d743b69 --- /dev/null +++ b/tests/Synercoding.FileFormats.Pdf.Tests/IO/Filters/FlateDecodeTests.cs @@ -0,0 +1,38 @@ +using Synercoding.FileFormats.Pdf.IO.Filters; +using System.Buffers.Binary; +using System.IO.Compression; + +namespace Synercoding.FileFormats.Pdf.Tests.IO.Filters; + +public class FlateDecodeTests +{ + [Fact] + public void Test_Encode_WritesZlibHeaderAndAdler32Trailer() + { + var input = System.Text.Encoding.ASCII.GetBytes("Wikipedia"); + + var encoded = new FlateDecode().Encode(input, null); + + // zlib header: deflate method with 32K window, and a header checksum that is a multiple of 31 + Assert.Equal(0x78, encoded[0]); + Assert.Equal(0, ( ( encoded[0] << 8 ) | encoded[1] ) % 31); + + // Adler-32 of "Wikipedia" is 0x11E60398, stored big-endian at the end of the stream + Assert.Equal(0x11E60398u, BinaryPrimitives.ReadUInt32BigEndian(encoded.AsSpan(encoded.Length - 4))); + } + + [Fact] + public void Test_Encode_RoundTripsThroughZlib() + { + var input = new byte[10_000]; + new Random(42).NextBytes(input); + + var encoded = new FlateDecode().Encode(input, null); + + using var decompressed = new MemoryStream(); + using (var zlib = new ZLibStream(new MemoryStream(encoded), CompressionMode.Decompress)) + zlib.CopyTo(decompressed); + + Assert.Equal(input, decompressed.ToArray()); + } +} From 906ffed8d0424b22f127f1d57e4af6a55c9a8495 Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Mon, 5 Oct 2026 23:18:00 +0200 Subject: [PATCH 3/3] Updated release notes --- src/Synercoding.FileFormats.Pdf/PackageDetails.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Synercoding.FileFormats.Pdf/PackageDetails.props b/src/Synercoding.FileFormats.Pdf/PackageDetails.props index 2784a92..c056a42 100644 --- a/src/Synercoding.FileFormats.Pdf/PackageDetails.props +++ b/src/Synercoding.FileFormats.Pdf/PackageDetails.props @@ -10,7 +10,7 @@ Synercoding.FileFormats.Pdf Synercoding.FileFormats.Pdf Contains classes which makes it easy to quickly create a pdf file. - Fixed a broken constructor. + Fixed a broken constructor. Fixed FlateDecode streams missing the zlib Adler-32 trailer, which caused "incorrect data check" warnings in Ghostscript. Subset fonts now get the required six-letter tag prefix (e.g. ABCDEF+FontName). Added OverprintMode (OPM) support to the extended graphics state. README.md LICENSE