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
17 changes: 5 additions & 12 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
<Project>

<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<NuGetAuditMode>all</NuGetAuditMode>
</PropertyGroup>

<PropertyGroup>
<RestoreSources>
https://api.nuget.org/v3/index.json;
</RestoreSources>
</PropertyGroup>

<ItemGroup Label="Build packages">
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.401" />
</ItemGroup>

<ItemGroup Label="General packages">
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageVersion Include="Synercoding.Primitives" Version="1.0.0-rc11" />
</ItemGroup>

<ItemGroup Label="Testing packages">
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="coverlet.collector" Version="10.1.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0" />
<PackageVersion Include="Pdfium.Rendering" Version="1.0.0-alpha05" />
</ItemGroup>

<ItemGroup Label="Example app packages">
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.12" />
</ItemGroup>

</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public sealed record class ExtendedGraphicsState
/// </summary>
public bool? OverprintNonStroking { get; init; }

/// <summary>
/// The overprint mode, which determines how overprinting is applied when painting in a DeviceCMYK color space.
/// </summary>
public OverprintMode? OverprintMode { get; init; }

/// <summary>
/// The current stroking alpha constant, specifying the constant shape or constant opacity value
/// to be used for stroking operations in the transparent imaging model.
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions src/Synercoding.FileFormats.Pdf/Content/OverprintMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Synercoding.FileFormats.Pdf.Content;

/// <summary>
/// Specifies how overprinting is applied when painting in a DeviceCMYK color space.
/// </summary>
public enum OverprintMode
{
/// <summary>
/// Each source color component value replaces the value previously painted for the corresponding device colorant,
/// regardless of what the new value is.
/// </summary>
Standard = 0,
/// <summary>
/// A tint value of 0.0 for a source color component leaves the corresponding component of the previously painted color unchanged.
/// </summary>
NonZero = 1
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,17 @@ public static void WriteType0Font(ObjectWriter writer, PdfObjectId fontId, Font
PdfObjectId? cidToGidMapId = null;
Dictionary<ushort, ushort>? 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);

Expand All @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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<char> 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!;
Expand All @@ -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
{
Expand All @@ -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
Expand Down
21 changes: 3 additions & 18 deletions src/Synercoding.FileFormats.Pdf/IO/Filters/FlateDecode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
};
}
2 changes: 1 addition & 1 deletion src/Synercoding.FileFormats.Pdf/PackageDetails.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Product>Synercoding.FileFormats.Pdf</Product>
<Title>Synercoding.FileFormats.Pdf</Title>
<Description>Contains classes which makes it easy to quickly create a pdf file.</Description>
<PackageReleaseNotes>Fixed a broken constructor.</PackageReleaseNotes>
<PackageReleaseNotes>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.</PackageReleaseNotes>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions src/Synercoding.FileFormats.Pdf/Primitives/PdfName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public sealed class PdfName : IPdfPrimitive, IEquatable<PdfName>
{ "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") },
Expand Down
2 changes: 2 additions & 0 deletions src/Synercoding.FileFormats.Pdf/Primitives/PdfNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
/// Creates a PDF with the specified text and subsetting setting.
/// Based on _writeTextOnlyPdf from ConsoleTester.
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
<Content Include="..\Font-Files\juliett-font\JuliettRegular-7OXnA.ttf" Link="TestFiles\Fonts\JuliettRegular-7OXnA.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\Pdf-Files\encrypted-aes-128bits.pdf" Link="TestFiles\Pdfs\encrypted-aes-128bits.pdf" >
<Content Include="..\Pdf-Files\encrypted-aes-128bits.pdf" Link="TestFiles\Pdfs\encrypted-aes-128bits.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\Pdf-Files\encrypted-aes-256bits.pdf" Link="TestFiles\Pdfs\encrypted-aes-256bits.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\Pdf-Files\encrypted-rc4-128bits.pdf" Link="TestFiles\Pdfs\encrypted-rc4-128bits.pdf" >
<Content Include="..\Pdf-Files\encrypted-rc4-128bits.pdf" Link="TestFiles\Pdfs\encrypted-rc4-128bits.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
Expand All @@ -50,5 +50,19 @@
<PackageReference Include="bblanchon.PDFium.Linux" VersionOverride="$(PdfiumVersion)" Condition="'$(RuntimeIdentifier)'=='' Or $(RuntimeIdentifier.StartsWith('linux-'))" />
<PackageReference Include="bblanchon.PDFium.macOS" VersionOverride="$(PdfiumVersion)" Condition="'$(RuntimeIdentifier)'=='' Or '$(RuntimeIdentifier)'=='osx-x64' Or '$(RuntimeIdentifier)'=='osx-arm64'" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<PackageReference Update="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
Loading