diff --git a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs index 0f67eec4..7909a1ae 100644 --- a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs +++ b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs @@ -145,6 +145,52 @@ } } + [Fact] + public void WindowsOnlyCanWriteSinglePageAccentedCharactersSystemFont() + { + var builder = new PdfDocumentBuilder(); + + builder.DocumentInformation.Title = "Hello Windows!"; + + var page = builder.AddPage(PageSize.A4); + + var file = @"C:\Windows\Fonts\Calibri.ttf"; + + if (!File.Exists(file)) + { + return; + } + + byte[] bytes; + try + { + bytes = File.ReadAllBytes(file); + } + catch + { + return; + } + + var font = builder.AddTrueTypeFont(bytes); + + page.AddText("eé", 12, new PdfPoint(30, 520), font); + + Assert.NotEmpty(page.Operations); + + var b = builder.Build(); + + WriteFile(nameof(WindowsOnlyCanWriteSinglePageAccentedCharactersSystemFont), b); + + Assert.NotEmpty(b); + + using (var document = PdfDocument.Open(b)) + { + var page1 = document.GetPage(1); + + Assert.Equal("eé", page1.Text); + } + } + [Fact] public void WindowsOnlyCanWriteSinglePageHelloWorldSystemFont() { diff --git a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeGlyphTableSubsetter.cs b/src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeGlyphTableSubsetter.cs similarity index 81% rename from src/UglyToad.PdfPig/Writer/Fonts/TrueTypeGlyphTableSubsetter.cs rename to src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeGlyphTableSubsetter.cs index dba7f5c0..c8357bfd 100644 --- a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeGlyphTableSubsetter.cs +++ b/src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeGlyphTableSubsetter.cs @@ -1,18 +1,51 @@ -namespace UglyToad.PdfPig.Writer.Fonts +namespace UglyToad.PdfPig.Writer.Fonts.Subsetting { using System; using System.Collections.Generic; using System.IO; + using PdfPig.Fonts.Exceptions; using PdfPig.Fonts.TrueType; using PdfPig.Fonts.TrueType.Glyphs; + using PdfPig.Fonts.TrueType.Tables; using Util; using IndexMap = TrueTypeSubsetter.OldToNewGlyphIndex; + /// + /// Produces a glyph table which contains a subset of the glyphs in the input font. + /// internal static class TrueTypeGlyphTableSubsetter { - public static NewGlyphTable SubsetGlyphTable(TrueTypeFontProgram font, byte[] fontBytes, IndexMap[] mapping) + /// + /// Creates a new glyph table from the input font which contains only the glyphs required by the input mapping. + /// + /// The font used to create this subset. + /// The raw bytes of the input font. + /// The mapping of old glyph indices to new glyph indices. + /// A new glyph table and associated information for use in creating a valid TrueType font file. + public static TrueTypeSubsetGlyphTable SubsetGlyphTable(TrueTypeFontProgram font, byte[] fontBytes, IndexMap[] mapping) { + if (font == null) + { + throw new ArgumentNullException(nameof(font)); + } + + if (fontBytes == null) + { + throw new ArgumentNullException(nameof(fontBytes)); + } + + if (mapping == null) + { + throw new ArgumentNullException(nameof(mapping)); + } + var data = new TrueTypeDataBytes(fontBytes); + var advanceWidthTable = font.TableRegister.HorizontalMetricsTable; + + if (advanceWidthTable == null) + { + throw new InvalidFontFormatException($"Font: {font} did not contain a horizontal metrics table, cannot subset."); + } var existingGlyphs = GetGlyphRecordsInFont(font, data); @@ -30,6 +63,7 @@ } var glyphLocations = new List(); + var advanceWidths = new List(); var compositeIndicesToReplace = new List<(uint offset, ushort newIndex)>(); @@ -56,13 +90,13 @@ { // Else we need to copy the dependency glyph from the original. var actualDependencyRecord = existingGlyphs[dependency.Index]; - + // We need to add it to the set of glyphs to copy. newDependencyIndex = glyphsToCopy.Count; glyphsToCopy.Add(actualDependencyRecord); glyphsToCopyOriginalIndex.Add((int)dependency.Index); } - + var withinGlyphDataIndexOffset = dependency.OffsetOfIndexWithinData - newRecord.Offset; compositeIndicesToReplace.Add(((uint)withinGlyphDataIndexOffset, (ushort)newDependencyIndex)); @@ -72,6 +106,9 @@ // Record the glyph location. glyphLocations.Add((uint)stream.Position); + var advanceWidth = advanceWidthTable.HorizontalMetrics[glyphsToCopyOriginalIndex[i]]; + advanceWidths.Add(advanceWidth); + if (newRecord.Type == GlyphType.Empty) { // TODO: if this is the last glyph this might be a problem. @@ -83,14 +120,21 @@ var glyphBytes = data.ReadByteArray(newRecord.DataLength); // Update any indices referenced by composite glyphs to match the new index of the dependency. - foreach (var toReplace in compositeIndicesToReplace) + foreach (var (offset, newIndex) in compositeIndicesToReplace) { - glyphBytes[toReplace.offset] = (byte)(toReplace.newIndex >> 8); - glyphBytes[toReplace.offset + 1] = (byte)toReplace.newIndex; + glyphBytes[offset] = (byte)(newIndex >> 8); + glyphBytes[offset + 1] = (byte)newIndex; } + + stream.Write(glyphBytes, 0, glyphBytes.Length); // Each glyph description must start at a 4 byte boundary. - stream.Write(glyphBytes, 0, glyphBytes.Length); + var remainder = glyphBytes.Length % 4; + var bytesToPad = remainder == 0 ? 0 : 4 - remainder; + for (var j = 0; j < bytesToPad; j++) + { + stream.WriteByte(0); + } } var output = stream.ToArray(); @@ -98,7 +142,7 @@ glyphLocations.Add((uint)output.Length); var offsets = glyphLocations.ToArray(); - return new NewGlyphTable(output, offsets); + return new TrueTypeSubsetGlyphTable(output, offsets, advanceWidths.ToArray()); } } @@ -120,7 +164,7 @@ if (indexToLocationTable.GlyphOffsets[i + 1] <= indexToLocationTable.GlyphOffsets[i]) { - glyphRecords[i] = new GlyphRecord(i, glyphOffset); + glyphRecords[i] = new GlyphRecord(glyphOffset); continue; } @@ -143,13 +187,13 @@ if (type == GlyphType.Simple) { ReadSimpleGlyph(data, numberOfContours); - glyphRecords[i] = new GlyphRecord(i, glyphOffset, type, (int)(data.Position - glyphOffset)); + glyphRecords[i] = new GlyphRecord(glyphOffset, type, (int)(data.Position - glyphOffset)); } else { var glyphIndices = ReadCompositeGlyph(data); - glyphRecords[i] = new GlyphRecord(i, glyphOffset, type, (int)(data.Position - glyphOffset), glyphIndices); + glyphRecords[i] = new GlyphRecord(glyphOffset, type, (int)(data.Position - glyphOffset), glyphIndices); } } @@ -221,9 +265,11 @@ } } + // ReSharper disable UnusedVariable var xCoordinates = ReadCoordinates(perPointFlags, data, SimpleGlyphFlags.XSingleByte, SimpleGlyphFlags.ThisXIsTheSame); var yCoordinates = ReadCoordinates(perPointFlags, data, SimpleGlyphFlags.YSingleByte, SimpleGlyphFlags.ThisYIsTheSame); + // ReSharper restore UnusedVariable } private static short[] ReadCoordinates(SimpleGlyphFlags[] flags, TrueTypeDataBytes data, @@ -329,8 +375,6 @@ private class GlyphRecord { - public int Index { get; } - public int Offset { get; } public GlyphType Type { get; } @@ -342,19 +386,17 @@ /// public IReadOnlyList DependencyIndices { get; } - public GlyphRecord(int index, int offset, GlyphType type, int dataLength, + public GlyphRecord(int offset, GlyphType type, int dataLength, IReadOnlyList dependentIndices = null) { - Index = index; Offset = offset; Type = type; DataLength = dataLength; DependencyIndices = dependentIndices ?? EmptyArray.Instance; } - public GlyphRecord(int index, int offset) + public GlyphRecord(int offset) { - Index = index; Offset = offset; Type = GlyphType.Empty; DataLength = 0; @@ -369,6 +411,9 @@ Composite } + /// + /// Marks a glyph index referenced by a composite glyph. + /// private struct CompositeGlyphIndexReference { /// @@ -387,18 +432,5 @@ OffsetOfIndexWithinData = offsetOfIndexWithinData; } } - - public class NewGlyphTable - { - public byte[] Bytes { get; } - - public uint[] GlyphOffsets { get; } - - public NewGlyphTable(byte[] bytes, uint[] glyphOffsets) - { - Bytes = bytes ?? throw new ArgumentNullException(nameof(bytes)); - GlyphOffsets = glyphOffsets ?? throw new ArgumentNullException(nameof(glyphOffsets)); - } - } } } diff --git a/src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeSubsetEncoding.cs b/src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeSubsetEncoding.cs new file mode 100644 index 00000000..e06136db --- /dev/null +++ b/src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeSubsetEncoding.cs @@ -0,0 +1,14 @@ +namespace UglyToad.PdfPig.Writer.Fonts.Subsetting +{ + using System.Collections.Generic; + + internal class TrueTypeSubsetEncoding + { + public IReadOnlyList Characters { get; } + + public TrueTypeSubsetEncoding(IReadOnlyList characters) + { + Characters = characters; + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeSubsetGlyphTable.cs b/src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeSubsetGlyphTable.cs new file mode 100644 index 00000000..613e61bb --- /dev/null +++ b/src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeSubsetGlyphTable.cs @@ -0,0 +1,61 @@ +namespace UglyToad.PdfPig.Writer.Fonts.Subsetting +{ + using System; + using PdfPig.Fonts.TrueType.Tables; + + /// + /// Details of the new glyph 'glyf' table created when subsetting a TrueType font. + /// + internal class TrueTypeSubsetGlyphTable + { + /// + /// The raw bytes of the new table. + /// + public byte[] Bytes { get; } + + /// + /// The offsets of each of the glyphs in the new table. + /// + public uint[] GlyphOffsets { get; } + + /// + /// The corresponding horizontal metrics for each glyph. + /// + public HorizontalMetricsTable.HorizontalMetric[] HorizontalMetrics { get; } + + /// + /// The number of glyphs in the new table. + /// + public ushort GlyphCount => (ushort)(GlyphOffsets.Length - 1); + + /// + /// Create a new . + /// + public TrueTypeSubsetGlyphTable(byte[] bytes, uint[] glyphOffsets, HorizontalMetricsTable.HorizontalMetric[] horizontalMetrics) + { + Bytes = bytes ?? throw new ArgumentNullException(nameof(bytes)); + GlyphOffsets = glyphOffsets ?? throw new ArgumentNullException(nameof(glyphOffsets)); + HorizontalMetrics = horizontalMetrics ?? throw new ArgumentNullException(nameof(horizontalMetrics)); + } + + /// + /// Convert the values to s. + /// + public long[] OffsetsAsLongs() + { + var data = new long[GlyphOffsets.Length]; + for (var i = 0; i < GlyphOffsets.Length; i++) + { + data[i] = GlyphOffsets[i]; + } + + return data; + } + + /// + public override string ToString() + { + return $"{GlyphCount} glyphs. Data is {Bytes.Length} bytes."; + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeSubsetter.cs b/src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeSubsetter.cs similarity index 83% rename from src/UglyToad.PdfPig/Writer/Fonts/TrueTypeSubsetter.cs rename to src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeSubsetter.cs index d38b10d0..45734413 100644 --- a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeSubsetter.cs +++ b/src/UglyToad.PdfPig/Writer/Fonts/Subsetting/TrueTypeSubsetter.cs @@ -1,10 +1,11 @@ -namespace UglyToad.PdfPig.Writer.Fonts +namespace UglyToad.PdfPig.Writer.Fonts.Subsetting { using System; using System.Collections.Generic; using System.IO; using System.Linq; using IO; + using PdfPig.Fonts.Exceptions; using PdfPig.Fonts.TrueType; using PdfPig.Fonts.TrueType.Parser; using PdfPig.Fonts.TrueType.Tables; @@ -12,7 +13,9 @@ using Util; internal static class TrueTypeSubsetter - { + { + private const ushort IndexToLocLong = 1; + /* * The PDF specification requires the following 10 tables: * glyf @@ -25,6 +28,7 @@ * fpgm * prep * cmap + * But not all fonts include 'cvt ' and 'fpgm'. */ private static readonly IReadOnlyList RequiredTags = new[] { @@ -73,12 +77,20 @@ for (var i = 0; i < RequiredTags.Count; i++) { var tag = RequiredTags[i]; - var entry = new DirectoryEntry(tag, stream.Position, font.TableHeaders[tag]); + + if (!font.TableHeaders.TryGetValue(tag, out var inputHeader)) + { + throw new InvalidFontFormatException($"Font does not contain table required for subsetting: {tag}."); + } + + var entry = new DirectoryEntry(tag, stream.Position, inputHeader); entry.DummyHeader.Write(stream); directoryEntries[i] = entry; } - TrueTypeGlyphTableSubsetter.NewGlyphTable newGlyphTable = null; + // Generate the glyph subset. + TrueTypeSubsetGlyphTable trueTypeSubsetGlyphTable = TrueTypeGlyphTableSubsetter.SubsetGlyphTable(font, fontBytes, indexMapping); + // Write the actual tables. for (var i = 0; i < directoryEntries.Length; i++) { @@ -95,49 +107,40 @@ } else if (entry.Tag == TrueTypeHeaderTable.Glyf) { - newGlyphTable = TrueTypeGlyphTableSubsetter.SubsetGlyphTable(font, fontBytes, indexMapping); - stream.Write(newGlyphTable.Bytes, 0, newGlyphTable.Bytes.Length); + stream.Write(trueTypeSubsetGlyphTable.Bytes, 0, trueTypeSubsetGlyphTable.Bytes.Length); } else if (entry.Tag == TrueTypeHeaderTable.Hmtx) { - var hmtx = GetHorizontalMetricsTable(font, entry, indexMapping); + var hmtx = GetHorizontalMetricsTable(entry, trueTypeSubsetGlyphTable); hmtx.Write(stream); } else if (entry.Tag == TrueTypeHeaderTable.Loca) { - if (newGlyphTable == null) - { - throw new InvalidOperationException(); - } - var table = new IndexToLocationTable(entry.DummyHeader, IndexToLocationTable.EntryFormat.Long, - newGlyphTable.GlyphOffsets.Select(x => (long)x).ToArray()); + trueTypeSubsetGlyphTable.GlyphOffsets.Select(x => (long)x).ToArray()); table.Write(stream); } else if (entry.Tag == TrueTypeHeaderTable.Head) { // Update indexToLoc format. var headBytes = GetRawInputTableBytes(fontBytes, entry); - WriteUShort(headBytes, headBytes.Length - 4, 1); + WriteUShort(headBytes, headBytes.Length - 4, IndexToLocLong); stream.Write(headBytes, 0, headBytes.Length); + + // TODO: zero out checksum adjustment bytes. } else if (entry.Tag == TrueTypeHeaderTable.Hhea) { // Update number of h metrics. var hheaBytes = GetRawInputTableBytes(fontBytes, entry); - WriteUShort(hheaBytes, hheaBytes.Length - 2, (ushort)indexMapping.Length); + WriteUShort(hheaBytes, hheaBytes.Length - 2, (ushort)trueTypeSubsetGlyphTable.HorizontalMetrics.Length); stream.Write(hheaBytes, 0, hheaBytes.Length); } else if (entry.Tag == TrueTypeHeaderTable.Maxp) { - if (newGlyphTable == null) - { - throw new InvalidOperationException(); - } - // Update number of glyphs. var maxpBytes = GetRawInputTableBytes(fontBytes, entry); - WriteUShort(maxpBytes, 4, (ushort)(newGlyphTable.GlyphOffsets.Length - 1)); + WriteUShort(maxpBytes, 4, trueTypeSubsetGlyphTable.GlyphCount); stream.Write(maxpBytes, 0, maxpBytes.Length); } else @@ -182,7 +185,7 @@ result[0] = new OldToNewGlyphIndex(0, 0, '\0'); - var previousCMap = font.MacRomanCMap ?? font.WindowsUnicodeCMap ?? font.WindowsSymbolCMap; + var previousCMap = font.WindowsUnicodeCMap ?? font.WindowsSymbolCMap ?? font.MacRomanCMap; if (previousCMap == null) { @@ -223,21 +226,9 @@ return cmap; } - private static HorizontalMetricsTable GetHorizontalMetricsTable(TrueTypeFontProgram font, DirectoryEntry entry, OldToNewGlyphIndex[] encoding) + private static HorizontalMetricsTable GetHorizontalMetricsTable(DirectoryEntry entry, TrueTypeSubsetGlyphTable glyphTable) { - var current = font.TableRegister.HorizontalMetricsTable; - - var newMetrics = new HorizontalMetricsTable.HorizontalMetric[encoding.Length]; - - for (var i = 0; i < encoding.Length; i++) - { - var mapping = encoding[i]; - // TODO: might be an additional lsb only. - var value = current.HorizontalMetrics[mapping.OldIndex]; - newMetrics[i] = value; - } - - return new HorizontalMetricsTable(entry.DummyHeader, newMetrics, EmptyArray.Instance); + return new HorizontalMetricsTable(entry.DummyHeader, glyphTable.HorizontalMetrics, EmptyArray.Instance); } private static byte[] GetRawInputTableBytes(byte[] font, DirectoryEntry entry) @@ -297,15 +288,5 @@ } } } - - internal class TrueTypeSubsetEncoding - { - public IReadOnlyList Characters { get; } - - public TrueTypeSubsetEncoding(IReadOnlyList characters) - { - Characters = characters; - } - } } diff --git a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeWritingFont.cs b/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeWritingFont.cs index fddc9865..78782af5 100644 --- a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeWritingFont.cs +++ b/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeWritingFont.cs @@ -7,13 +7,13 @@ using Core; using Filters; using Geometry; - using IO; using Logging; using Tokens; using PdfPig.Fonts; using PdfPig.Fonts.Exceptions; using PdfPig.Fonts.TrueType; using PdfPig.Fonts.TrueType.Tables; + using Subsetting; internal class TrueTypeWritingFont : IWritingFont {