mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
implement composite glyph support for subsetter #98
first pass at implementing composite glyph (glyphs formed by combining other simple glyphs) support for the subsetter. the produced file is valid as a pdf but does not display correctly for any composite glyphs. we need to check we're copying the full run of the composite glyph data as well as correctly setting any glyph indices, one idea is to try parsing the resulting font in pdfbox to see if fontbox can handle the subset we produce. next step is to add a test case with a single composite glyph and see what we're missing. also remove the old cmap replacer code because it is obsoleted by the full subsetter.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<char, byte> 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<string, InputHeader>(StringComparer.OrdinalIgnoreCase);
|
||||
var outputTableHeaders = new Dictionary<string, TrueTypeHeaderTable>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<char, byte> 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<char, byte> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GlyphRecord>(mapping.Length);
|
||||
var glyphsToCopyOriginalIndex = new List<int>(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<uint>();
|
||||
|
||||
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<int> 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<CompositeGlyphIndexReference> ReadCompositeGlyph(TrueTypeDataBytes data)
|
||||
{
|
||||
bool HasFlag(CompositeGlyphFlags actual, CompositeGlyphFlags value)
|
||||
{
|
||||
return (actual & value) != 0;
|
||||
}
|
||||
|
||||
var glyphIndices = new List<int>();
|
||||
var glyphIndices = new List<CompositeGlyphIndexReference>();
|
||||
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; }
|
||||
/// <summary>
|
||||
/// Indices of any glyphs this glyph depends on, if it's a composite glyph.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CompositeGlyphIndexReference> 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<CompositeGlyphIndexReference> dependentIndices = null)
|
||||
{
|
||||
Index = index;
|
||||
Offset = offset;
|
||||
Type = type;
|
||||
DataLength = dataLength;
|
||||
DependentIndices = dependentIndices ?? EmptyArray<int>.Instance;
|
||||
DependencyIndices = dependentIndices ?? EmptyArray<CompositeGlyphIndexReference>.Instance;
|
||||
}
|
||||
|
||||
public GlyphRecord(int index, int offset)
|
||||
@@ -293,7 +358,7 @@
|
||||
Offset = offset;
|
||||
Type = GlyphType.Empty;
|
||||
DataLength = 0;
|
||||
DependentIndices = EmptyArray<int>.Instance;
|
||||
DependencyIndices = EmptyArray<CompositeGlyphIndexReference>.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +369,25 @@
|
||||
Composite
|
||||
}
|
||||
|
||||
private struct CompositeGlyphIndexReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The index of the glyph reference by this composite glyph.
|
||||
/// </summary>
|
||||
public uint Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The offset of the index value in the data which this composite glyph was read from.
|
||||
/// </summary>
|
||||
public uint OffsetOfIndexWithinData { get; }
|
||||
|
||||
public CompositeGlyphIndexReference(uint index, uint offsetOfIndexWithinData)
|
||||
{
|
||||
Index = index;
|
||||
OffsetOfIndexWithinData = offsetOfIndexWithinData;
|
||||
}
|
||||
}
|
||||
|
||||
public class NewGlyphTable
|
||||
{
|
||||
public byte[] Bytes { get; }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user