diff --git a/src/UglyToad.PdfPig/Fonts/TrueType/Tables/GlyphDataTable.cs b/src/UglyToad.PdfPig/Fonts/TrueType/Tables/GlyphDataTable.cs index 1b9cfe2b..63fc825b 100644 --- a/src/UglyToad.PdfPig/Fonts/TrueType/Tables/GlyphDataTable.cs +++ b/src/UglyToad.PdfPig/Fonts/TrueType/Tables/GlyphDataTable.cs @@ -29,7 +29,7 @@ public static GlyphDataTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister.Builder tableRegister) { data.Seek(table.Offset); - + var indexToLocationTable = tableRegister.IndexToLocationTable; var offsets = indexToLocationTable.GlyphOffsets; diff --git a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeCMapReplacer.cs b/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeCMapReplacer.cs deleted file mode 100644 index 8d6033dc..00000000 --- a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeCMapReplacer.cs +++ /dev/null @@ -1,332 +0,0 @@ -namespace UglyToad.PdfPig.Writer.Fonts -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text; - using IO; - using PdfPig.Fonts.TrueType; - using PdfPig.Fonts.TrueType.Parser; - using PdfPig.Fonts.TrueType.Tables; - using PdfPig.Fonts.TrueType.Tables.CMapSubTables; - using Util; - - internal static class TrueTypeCMapReplacer - { - private const int SizeOfFraction = 4; - private const int SizeOfShort = 2; - private const int SizeOfTag = 4; - private const int SizeOfInt = 4; - - private const string CMapTag = "cmap"; - private const string HeadTag = "head"; - - public static byte[] ReplaceCMapTables(TrueTypeFontProgram fontProgram, IInputBytes fontBytes, IReadOnlyDictionary newEncoding) - { - if (fontBytes == null) - { - throw new ArgumentNullException(nameof(fontBytes)); - } - - if (newEncoding == null) - { - throw new ArgumentNullException(nameof(newEncoding)); - } - - var buffer = new byte[2048]; - - var inputTableHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - var outputTableHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - - var fileChecksumOffset = SizeOfTag; - - byte[] result; - - using (var stream = new MemoryStream()) - { - // Write the file header details and read the number of tables out. - CopyThroughBufferPreserveData(stream, buffer, fontBytes, SizeOfFraction + (SizeOfShort * 4)); - - var numberOfTables = ReadUShortFromBuffer(buffer, SizeOfFraction); - - // For each table read the table header values and preserve the order by storing the offset in the input file - // at which the the header was read. - for (var i = 0; i < numberOfTables; i++) - { - var offsetOfHeader = (uint)stream.Position; - - CopyThroughBufferPreserveData(stream, buffer, fontBytes, SizeOfTag + (SizeOfInt * 3)); - - var tag = Encoding.UTF8.GetString(buffer, 0, SizeOfTag); - - var checksum = ReadUIntFromBuffer(buffer, fileChecksumOffset); - var offset = ReadUIntFromBuffer(buffer, SizeOfTag + SizeOfInt); - var length = ReadUIntFromBuffer(buffer, SizeOfTag + (SizeOfInt * 2)); - - var headerTable = new TrueTypeHeaderTable(tag, checksum, offset, length); - - // Store the locations of the tables in this font. - inputTableHeaders[tag] = new InputHeader(headerTable, offsetOfHeader); - } - - // Copy raw bytes for each of the tables from the input to the output including any additional bytes not in - // tables but present in the input. - var inputOffset = fontBytes.CurrentOffset; - - foreach (var inputHeader in inputTableHeaders.OrderBy(x => x.Value.HeaderTable.Offset)) - { - var location = inputHeader.Value.HeaderTable; - - var gapFromPrevious = location.Offset - inputOffset; - - if (gapFromPrevious > 0) - { - CopyThroughBufferDiscardData(stream, buffer, fontBytes, gapFromPrevious); - } - - if (inputHeader.Value.IsTable(CMapTag)) - { - // Skip the CMap table for now, move it to the end in the output so we can resize it dynamically. - inputOffset = location.Offset + location.Length; - fontBytes.Seek(inputOffset); - - continue; - } - - var outputOffset = (uint)stream.Position; - - outputTableHeaders[location.Tag] = new TrueTypeHeaderTable(location.Tag, 0, outputOffset, location.Length); - - CopyThroughBufferDiscardData(stream, buffer, fontBytes, location.Length); - - var writtenLength = stream.Position - outputOffset; - - if (writtenLength != location.Length) - { - throw new InvalidOperationException($"Expected to write {location.Length} bytes for table {location.Tag} " + - $"but wrote {stream.Position - outputOffset}."); - } - - inputOffset = fontBytes.CurrentOffset; - } - - // Create a new cmap table here. - var table = GenerateWindowsSymbolTable(fontProgram, newEncoding); - var cmapLocation = inputTableHeaders[CMapTag]; - - fontBytes.Seek(cmapLocation.HeaderTable.Offset); - - var newCmapTableLocation = (uint)stream.Position; - var newCmapTableLength = (uint)table.Length; - CopyThroughBufferDiscardData(stream, buffer, new ByteArrayInputBytes(table), newCmapTableLength); - - outputTableHeaders[cmapLocation.Tag] = new TrueTypeHeaderTable(cmapLocation.Tag, 0, newCmapTableLocation, newCmapTableLength); - - foreach (var inputHeader in inputTableHeaders) - { - // Go back to the location of the offset - var headerOffsetLocation = inputHeader.Value.OffsetInInput + SizeOfTag + SizeOfInt; - stream.Seek(headerOffsetLocation, SeekOrigin.Begin); - - var outputHeader = outputTableHeaders[inputHeader.Key]; - - var inputLength = inputHeader.Value.HeaderTable.Length; - - var isCmap = inputHeader.Value.IsTable(CMapTag); - - if (outputHeader.Length != inputLength && !isCmap) - { - throw new InvalidOperationException($"Actual data length {outputHeader.Length} " + - $"did not match header length {inputLength} for table {inputHeader.Key}."); - } - - stream.WriteUInt(outputHeader.Offset); - - if (isCmap) - { - // Also overwrite length. - stream.WriteUInt(outputHeader.Length); - } - } - - stream.Seek(0, SeekOrigin.Begin); - - // Done writing to stream, just checksums left to repair. - result = stream.ToArray(); - } - - var inputBytes = new ByteArrayInputBytes(result); - - // Overwrite checksum values per table. - foreach (var inputHeader in inputTableHeaders) - { - var outputHeader = outputTableHeaders[inputHeader.Key]; - - var headerOffset = inputHeader.Value.OffsetInInput; - - var newChecksum = TrueTypeChecksumCalculator.Calculate(inputBytes, outputHeader); - - // Overwrite the checksum value. - WriteUInt(result, headerOffset + SizeOfTag, newChecksum); - } - - // Overwrite the checksum adjustment which records the whole font checksum. - var headTable = outputTableHeaders[HeadTag]; - var wholeFontChecksum = TrueTypeChecksumCalculator.CalculateWholeFontChecksum(inputBytes, headTable); - - // Calculate the checksum for the entire font and subtract the value from the hex value B1B0AFBA. - var checksumAdjustmentLocation = headTable.Offset + 8; - var checksumAdjustment = 0xB1B0AFBA - wholeFontChecksum; - - // Store the result in checksum adjustment. - WriteUInt(result, checksumAdjustmentLocation, checksumAdjustment); - - // TODO: take andada regular with no modifications but removing the os/2 table and validate. - var canParse = new TrueTypeFontParser().Parse(new TrueTypeDataBytes(new ByteArrayInputBytes(result))); - - return result; - } - - private static ushort ReadUShortFromBuffer(byte[] buffer, int location) - { - return (ushort)((buffer[location] << 8) + (buffer[location + 1] << 0)); - } - - private static uint ReadUIntFromBuffer(byte[] buffer, int location) - { - return (uint)(((long)buffer[location] << 24) - + ((long)buffer[location + 1] << 16) - + (buffer[location + 2] << 8) - + (buffer[location + 3] << 0)); - } - - private static void WriteUInt(byte[] array, uint offset, uint value) - { - array[offset] = (byte)(value >> 24); - array[offset + 1] = (byte)(value >> 16); - array[offset + 2] = (byte)(value >> 8); - array[offset + 3] = (byte)(value >> 0); - } - - private static void CopyThroughBufferDiscardData(Stream destination, byte[] buffer, IInputBytes input, long size) - { - var filled = 0; - while (filled < size) - { - var expected = (int)Math.Min(size - filled, 2048); - - var read = input.Read(buffer, expected); - - if (read != expected) - { - throw new InvalidOperationException($"Failed to read {size} bytes starting at offset {input.CurrentOffset - read}."); - } - - destination.Write(buffer, 0, read); - - filled += read; - } - } - - /// - /// Copies data from the input to the destination stream while also populating the buffer with the full - /// run of copied data in the buffer from position 0 -> size. - /// - private static void CopyThroughBufferPreserveData(Stream destination, byte[] buffer, IInputBytes input, int size) - { - if (size > buffer.Length) - { - throw new InvalidOperationException("Cannot use this method to read more bytes than fit in the buffer."); - } - - var read = input.Read(buffer, size); - if (read != size) - { - throw new InvalidOperationException($"Failed to read {size} bytes starting at offset {input.CurrentOffset - read}."); - } - - destination.Write(buffer, 0, read); - } - - private static byte[] GenerateWindowsSymbolTable(TrueTypeFontProgram font, IReadOnlyDictionary newEncoding) - { - // We generate a format 6 sub-table. - const ushort cmapVersion = 0; - const ushort encodingId = 0; - - var glyphIndices = MapNewEncodingToGlyphIndexArray(font, newEncoding); - - var cmapTable = new CMapTable(cmapVersion, new TrueTypeHeaderTable(CMapTag, 0, 0, 0), new[] - { - new TrimmedTableMappingCMapTable(TrueTypeCMapPlatform.Macintosh, encodingId, 0, glyphIndices.Length, glyphIndices), - new TrimmedTableMappingCMapTable(TrueTypeCMapPlatform.Windows, encodingId, 0, glyphIndices.Length, glyphIndices) - }); - - using (var stream = new MemoryStream()) - { - cmapTable.Write(stream); - return stream.ToArray(); - } - } - - private static ushort[] MapNewEncodingToGlyphIndexArray(TrueTypeFontProgram font, IReadOnlyDictionary newEncoding) - { - var mappingTable = font.WindowsUnicodeCMap ?? font.WindowsSymbolCMap; - - if (mappingTable == null) - { - throw new InvalidOperationException(); - } - - var first = default(ushort?); - var glyphIndices = new ushort[newEncoding.Count + 1]; - glyphIndices[0] = 0; - var i = 1; - foreach (var pair in newEncoding.OrderBy(x => x.Value)) - { - if (first.HasValue && pair.Value - first.Value != 1) - { - throw new InvalidOperationException("The new encoding contained a gap."); - } - - first = pair.Value; - - // this must be the actual glyph index from the original cmap table. - glyphIndices[i++] = (ushort)mappingTable.CharacterCodeToGlyphIndex(pair.Key); - } - - if (!first.HasValue) - { - throw new InvalidOperationException(); - } - - return glyphIndices; - } - - private class InputHeader - { - public string Tag => HeaderTable.Tag; - - public TrueTypeHeaderTable HeaderTable { get; } - - public uint OffsetInInput { get; } - - public InputHeader(TrueTypeHeaderTable headerTable, uint offsetInInput) - { - if (headerTable.Tag == null) - { - throw new ArgumentException($"No tag for header table: {HeaderTable}."); - } - - HeaderTable = headerTable; - OffsetInInput = offsetInInput; - } - - public bool IsTable(string tag) - { - return string.Equals(tag, Tag, StringComparison.OrdinalIgnoreCase); - } - } - } -} diff --git a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeGlyphTableSubsetter.cs b/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeGlyphTableSubsetter.cs index 2e1a5420..dba7f5c0 100644 --- a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeGlyphTableSubsetter.cs +++ b/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeGlyphTableSubsetter.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.IO; using PdfPig.Fonts.TrueType; using PdfPig.Fonts.TrueType.Glyphs; using Util; @@ -14,47 +15,91 @@ var data = new TrueTypeDataBytes(fontBytes); var existingGlyphs = GetGlyphRecordsInFont(font, data); - - var newGlyphRecords = new GlyphRecord[mapping.Length]; - var newGlyphTableLength = 0; + var glyphsToCopy = new List(mapping.Length); + var glyphsToCopyOriginalIndex = new List(mapping.Length); + + // Extract the glyphs required for this subset from the original table. for (var i = 0; i < mapping.Length; i++) { var map = mapping[i]; var record = existingGlyphs[map.OldIndex]; - newGlyphRecords[i] = record; - newGlyphTableLength += record.DataLength; + glyphsToCopy.Add(record); + glyphsToCopyOriginalIndex.Add(map.OldIndex); } - var newIndexToLoca = new uint[newGlyphRecords.Length + 1]; + var glyphLocations = new List(); - var outputIndex = 0u; - var output = new byte[newGlyphTableLength]; - for (var i = 0; i < newGlyphRecords.Length; i++) + var compositeIndicesToReplace = new List<(uint offset, ushort newIndex)>(); + + using (var stream = new MemoryStream()) { - var newRecord = newGlyphRecords[i]; - if (newRecord.Type == GlyphType.Composite) + for (var i = 0; i < glyphsToCopy.Count; i++) { - throw new NotSupportedException("TODO"); + compositeIndicesToReplace.Clear(); + + var newRecord = glyphsToCopy[i]; + + if (newRecord.Type == GlyphType.Composite) + { + // Any glyphs a composite glyph depends on must also be included. + for (var j = 0; j < newRecord.DependencyIndices.Count; j++) + { + // Get the indices of the dependency glyphs from the original font file. + var dependency = newRecord.DependencyIndices[j]; + + // If the dependency has already been included we can skip copying it again. + var newDependencyIndex = GetAlreadyCopiedDependencyIndex(dependency, glyphsToCopyOriginalIndex); + + if (!newDependencyIndex.HasValue) + { + // 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)); + } + } + + // Record the glyph location. + glyphLocations.Add((uint)stream.Position); + + if (newRecord.Type == GlyphType.Empty) + { + // TODO: if this is the last glyph this might be a problem. + continue; + } + + data.Seek(newRecord.Offset); + + 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) + { + glyphBytes[toReplace.offset] = (byte)(toReplace.newIndex >> 8); + glyphBytes[toReplace.offset + 1] = (byte)toReplace.newIndex; + } + + // Each glyph description must start at a 4 byte boundary. + stream.Write(glyphBytes, 0, glyphBytes.Length); } - newIndexToLoca[i] = outputIndex; - if (newRecord.Type == GlyphType.Empty) - { - continue; - } + var output = stream.ToArray(); - data.Seek(newRecord.Offset); - for (var j = 0; j < newRecord.DataLength; j++) - { - output[outputIndex++] = data.ReadByte(); - } + glyphLocations.Add((uint)output.Length); + var offsets = glyphLocations.ToArray(); + + return new NewGlyphTable(output, offsets); } - - newIndexToLoca[newIndexToLoca.Length - 1] = (uint)output.Length; - - return new NewGlyphTable(output, newIndexToLoca); } private static GlyphRecord[] GetGlyphRecordsInFont(TrueTypeFontProgram font, TrueTypeDataBytes data) @@ -111,6 +156,21 @@ return glyphRecords; } + private static int? GetAlreadyCopiedDependencyIndex(CompositeGlyphIndexReference dependency, IReadOnlyList copiedGlyphOriginalIndices) + { + for (var i = 0; i < copiedGlyphOriginalIndices.Count; i++) + { + var originalIndexAtK = copiedGlyphOriginalIndices[i]; + + if (originalIndexAtK == dependency.Index) + { + return i; + } + } + + return null; + } + private static void ReadSimpleGlyph(TrueTypeDataBytes data, int numberOfContours) { bool HasFlag(SimpleGlyphFlags flags, SimpleGlyphFlags value) @@ -218,21 +278,22 @@ return coordinates; } - private static int[] ReadCompositeGlyph(TrueTypeDataBytes data) + private static IReadOnlyList ReadCompositeGlyph(TrueTypeDataBytes data) { bool HasFlag(CompositeGlyphFlags actual, CompositeGlyphFlags value) { return (actual & value) != 0; } - var glyphIndices = new List(); + var glyphIndices = new List(); CompositeGlyphFlags flags; do { flags = (CompositeGlyphFlags)data.ReadUnsignedShort(); + var indexOffset = data.Position; var glyphIndex = data.ReadUnsignedShort(); - glyphIndices.Add(glyphIndex); + glyphIndices.Add(new CompositeGlyphIndexReference(glyphIndex, (uint)indexOffset)); if (HasFlag(flags, CompositeGlyphFlags.Args1And2AreWords)) { @@ -263,7 +324,7 @@ } } while (HasFlag(flags, CompositeGlyphFlags.MoreComponents)); - return glyphIndices.ToArray(); + return glyphIndices; } private class GlyphRecord @@ -276,15 +337,19 @@ public int DataLength { get; } - public int[] DependentIndices { get; } + /// + /// Indices of any glyphs this glyph depends on, if it's a composite glyph. + /// + public IReadOnlyList DependencyIndices { get; } - public GlyphRecord(int index, int offset, GlyphType type, int dataLength, int[] dependentIndices = null) + public GlyphRecord(int index, int offset, GlyphType type, int dataLength, + IReadOnlyList dependentIndices = null) { Index = index; Offset = offset; Type = type; DataLength = dataLength; - DependentIndices = dependentIndices ?? EmptyArray.Instance; + DependencyIndices = dependentIndices ?? EmptyArray.Instance; } public GlyphRecord(int index, int offset) @@ -293,7 +358,7 @@ Offset = offset; Type = GlyphType.Empty; DataLength = 0; - DependentIndices = EmptyArray.Instance; + DependencyIndices = EmptyArray.Instance; } } @@ -304,6 +369,25 @@ Composite } + private struct CompositeGlyphIndexReference + { + /// + /// The index of the glyph reference by this composite glyph. + /// + public uint Index { get; } + + /// + /// The offset of the index value in the data which this composite glyph was read from. + /// + public uint OffsetOfIndexWithinData { get; } + + public CompositeGlyphIndexReference(uint index, uint offsetOfIndexWithinData) + { + Index = index; + OffsetOfIndexWithinData = offsetOfIndexWithinData; + } + } + public class NewGlyphTable { public byte[] Bytes { get; } diff --git a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeSubsetter.cs b/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeSubsetter.cs index 18441031..d38b10d0 100644 --- a/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeSubsetter.cs +++ b/src/UglyToad.PdfPig/Writer/Fonts/TrueTypeSubsetter.cs @@ -130,9 +130,14 @@ } 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)indexMapping.Length); + WriteUShort(maxpBytes, 4, (ushort)(newGlyphTable.GlyphOffsets.Length - 1)); stream.Write(maxpBytes, 0, maxpBytes.Length); } else