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/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/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
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());
+ }
+}
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
+
+