722 lines
36 KiB
C#
722 lines
36 KiB
C#
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Numerics;
|
|
using SixLabors.Fonts.Tables.TrueType.Glyphs;
|
|
|
|
namespace SixLabors.Fonts.Tables.Woff {
|
|
// Source code is based on https://github.com/LayoutFarm/Typography
|
|
// see https://github.com/LayoutFarm/Typography/blob/master/Typography.OpenFont/WebFont/Woff2Reader.cs
|
|
// TODO: There's still some cleanup required here to bring the code up to a maintainable standard.
|
|
|
|
/// <summary>
|
|
/// Provides utility methods for reading and decoding WOFF2 font data,
|
|
/// including variable-length integer decoding and transformed glyph table reconstruction.
|
|
/// See: <see href="https://www.w3.org/TR/WOFF2/"/>.
|
|
/// </summary>
|
|
internal static class Woff2Utils
|
|
{
|
|
/// <summary>
|
|
/// The set of known table tags used for WOFF2 table directory encoding.
|
|
/// Table indices 0-62 map to these well-known tags; index 63 indicates an arbitrary 4-byte tag follows.
|
|
/// </summary>
|
|
// We don't reuse the const tag headers from our table types for clarity.
|
|
private static readonly string[] KnownTableTags =
|
|
{
|
|
"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post", "cvt ",
|
|
"fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT", "EBLC", "gasp",
|
|
"hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea", "vmtx", "BASE", "GDEF",
|
|
"GPOS", "GSUB", "EBSC", "JSTF", "MATH", "CBDT", "CBLC", "COLR", "CPAL",
|
|
"SVG ", "sbix", "acnt", "avar", "bdat", "bloc", "bsln", "cvar", "fdsc",
|
|
"feat", "fmtx", "fvar", "gvar", "hsty", "just", "lcar", "mort", "morx",
|
|
"opbd", "prop", "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill"
|
|
};
|
|
|
|
/// <summary>
|
|
/// The 255UInt16 encoding code indicating the value is stored as a following big-endian 16-bit word.
|
|
/// </summary>
|
|
private const byte OneMoreByteCode1 = 255;
|
|
|
|
/// <summary>
|
|
/// The 255UInt16 encoding code indicating the value is a following byte plus <c>LowestUCode * 2</c>.
|
|
/// </summary>
|
|
private const byte OneMoreByteCode2 = 254;
|
|
|
|
/// <summary>
|
|
/// The 255UInt16 encoding code indicating the value is stored as a following big-endian 16-bit word (explicit two bytes).
|
|
/// </summary>
|
|
private const byte WordCode = 253;
|
|
|
|
/// <summary>
|
|
/// The lowest single-byte code value that triggers multi-byte 255UInt16 decoding.
|
|
/// Values below this are returned directly as the decoded result.
|
|
/// </summary>
|
|
private const byte LowestUCode = 253;
|
|
|
|
/// <summary>
|
|
/// Reads the WOFF2 table directory headers from the given reader.
|
|
/// </summary>
|
|
/// <param name="reader">The big-endian binary reader positioned at the start of the table directory.</param>
|
|
/// <param name="tableCount">The number of table directory entries to read.</param>
|
|
/// <returns>A read-only dictionary mapping table tags to their <see cref="TableHeader"/> entries.</returns>
|
|
public static ReadOnlyDictionary<string, TableHeader> ReadWoff2Headers(BigEndianBinaryReader reader, int tableCount)
|
|
{
|
|
uint expectedTableStartAt = 0;
|
|
var headers = new Dictionary<string, TableHeader>(tableCount);
|
|
for (int i = 0; i < tableCount; i++)
|
|
{
|
|
Woff2TableHeader woffTableHeader = Read(reader, expectedTableStartAt, out uint nextExpectedTableStartAt);
|
|
expectedTableStartAt = nextExpectedTableStartAt;
|
|
headers.Add(woffTableHeader.Tag, woffTableHeader);
|
|
}
|
|
|
|
return new ReadOnlyDictionary<string, TableHeader>(headers);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a single WOFF2 table directory entry, decoding the flags byte, table tag,
|
|
/// original length, and optional transform length.
|
|
/// See: <see href="https://www.w3.org/TR/WOFF2/#table_dir_format"/>.
|
|
/// </summary>
|
|
/// <param name="reader">The big-endian binary reader.</param>
|
|
/// <param name="expectedTableStartAt">The expected offset for this table within the decompressed data stream.</param>
|
|
/// <param name="nextExpectedTableStartAt">When this method returns, contains the expected offset for the next table.</param>
|
|
/// <returns>The parsed <see cref="Woff2TableHeader"/>.</returns>
|
|
public static Woff2TableHeader Read(BigEndianBinaryReader reader, uint expectedTableStartAt, out uint nextExpectedTableStartAt)
|
|
{
|
|
// Leave the first byte open to store flagByte
|
|
const uint woff2FlagsTransform = 1 << 8;
|
|
byte flagsByte = reader.ReadByte();
|
|
int knownTable = flagsByte & 0x3F;
|
|
string tableName = knownTable == 0x3F ? reader.ReadTag() : KnownTableTags[knownTable];
|
|
|
|
uint flags = 0;
|
|
byte xformVersion = (byte)((flagsByte >> 6) & 0x03);
|
|
|
|
// 0 means xform for glyph/loca, non-0 for others
|
|
if (tableName is "glyf" or "loca")
|
|
{
|
|
if (xformVersion == 0)
|
|
{
|
|
flags |= woff2FlagsTransform;
|
|
}
|
|
}
|
|
else if (xformVersion != 0)
|
|
{
|
|
flags |= woff2FlagsTransform;
|
|
}
|
|
|
|
flags |= xformVersion;
|
|
|
|
if (!ReadUIntBase128(reader, out uint tableOrigLength))
|
|
{
|
|
throw new FontException("Error parsing woff2 table header");
|
|
}
|
|
|
|
uint tableTransformLength = tableOrigLength;
|
|
if ((flags & woff2FlagsTransform) != 0)
|
|
{
|
|
if (!ReadUIntBase128(reader, out tableTransformLength))
|
|
{
|
|
throw new FontException("Error parsing woff2 table header");
|
|
}
|
|
|
|
if (tableName == "loca" && tableTransformLength > 0)
|
|
{
|
|
throw new FontException("Error parsing woff2 table header");
|
|
}
|
|
}
|
|
|
|
nextExpectedTableStartAt = expectedTableStartAt + tableTransformLength;
|
|
if (nextExpectedTableStartAt < expectedTableStartAt)
|
|
{
|
|
throw new FontException("Error parsing woff2 table header");
|
|
}
|
|
|
|
return new Woff2TableHeader(tableName, 0, expectedTableStartAt, tableTransformLength);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads all glyph outlines from a WOFF2 transformed glyph table stream.
|
|
/// This reconstructs both simple and composite glyphs from the WOFF2 sub-streams
|
|
/// (nContour, nPoints, flags, glyph, composite, bbox, and instruction streams).
|
|
/// See: <see href="https://www.w3.org/TR/WOFF2/#glyf_table_format"/>.
|
|
/// </summary>
|
|
/// <param name="reader">The big-endian binary reader positioned at the start of the transformed glyf table.</param>
|
|
/// <param name="emptyGlyphLoader">The empty glyph loader to use for glyphs with no outline data.</param>
|
|
/// <returns>An array of <see cref="GlyphLoader"/> instances, one per glyph.</returns>
|
|
public static GlyphLoader[] LoadAllGlyphs(BigEndianBinaryReader reader, EmptyGlyphLoader emptyGlyphLoader)
|
|
{
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | Data Type | Semantic | Description and value type (if applicable) |
|
|
// +===========+=======================+=======================================================================================================+
|
|
// | Fixed | version | = 0x00000000 |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt16 | numGlyphs | Number of glyphs |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt16 | indexFormat | Offset format for loca table, should be consistent with indexToLocFormat |
|
|
// | | | of the original head table (see specification) |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt32 | nContourStreamSize | Size of nContour stream in bytes |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt32 | nPointsStreamSize | Size of nPoints stream in bytes |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt32 | flagStreamSize | Size of flag stream in bytes |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt32 | glyphStreamSize | Size of glyph stream in bytes (a stream of variable-length encoded values, see description below) |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt32 | compositeStreamSize | Size of composite stream in bytes (a stream of variable-length encoded values, see description below) |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt32 | bboxStreamSize | Size of bbox data in bytes representing combined length of bboxBitmap (a packed bit array) |
|
|
// | | | and bboxStream (a stream of Int16 values) |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt32 | instructionStreamSize | Size of instruction stream (a stream of UInt8 values) |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | Int16 | nContourStream[] | Stream of Int16 values representing number of contours for each glyph record |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | 255UInt16 | nPointsStream[] | Stream of values representing number of outline points for each contour in glyph records |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt8 | flagStream[] | Stream of UInt8 values representing flag values for each outline point. |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | Vary | glyphStream[] | Stream of bytes representing point coordinate values using variable length |
|
|
// | | | encoding format (defined in subclause 5.2) |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | Vary | compositeStream[] | Stream of bytes representing component flag values and associated composite glyph data |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt8 | bboxBitmap[] | Bitmap (a numGlyphs-long bit array) indicating explicit bounding boxes |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | Int16 | bboxStream[] | Stream of Int16 values representing glyph bounding box data |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
// | UInt8 | instructionStream[] | Stream of UInt8 values representing a set of instructions for each corresponding glyph |
|
|
// +-----------+-----------------------+-------------------------------------------------------------------------------------------------------+
|
|
uint version = reader.ReadUInt32();
|
|
ushort numGlyphs = reader.ReadUInt16();
|
|
ushort indexFormatOffset = reader.ReadUInt16();
|
|
|
|
uint nContourStreamSize = reader.ReadUInt32();
|
|
uint nPointsStreamSize = reader.ReadUInt32();
|
|
uint flagStreamSize = reader.ReadUInt32();
|
|
uint glyphStreamSize = reader.ReadUInt32();
|
|
uint compositeStreamSize = reader.ReadUInt32();
|
|
uint bboxStreamSize = reader.ReadUInt32();
|
|
uint instructionStreamSize = reader.ReadUInt32();
|
|
|
|
long nCountStreamOffset = reader.BaseStream.Position;
|
|
long nPointStreamOffset = nCountStreamOffset + nContourStreamSize;
|
|
long flagStreamOffset = nPointStreamOffset + nPointsStreamSize;
|
|
long glyphStreamOffset = flagStreamOffset + flagStreamSize;
|
|
long compositeStreamOffset = glyphStreamOffset + glyphStreamSize;
|
|
|
|
long bboxStreamOffset = compositeStreamOffset + compositeStreamSize;
|
|
long instructionStreamOffset = bboxStreamOffset + bboxStreamSize;
|
|
|
|
var glyphs = new GlyphVector[numGlyphs];
|
|
var allGlyphs = new GlyphData[numGlyphs];
|
|
var glyphLoaders = new GlyphLoader[numGlyphs];
|
|
var compositeGlyphs = new List<ushort>();
|
|
int contourCount = 0;
|
|
for (ushort i = 0; i < numGlyphs; i++)
|
|
{
|
|
short numContour = reader.ReadInt16();
|
|
allGlyphs[i] = new GlyphData(i, numContour);
|
|
if (numContour > 0)
|
|
{
|
|
contourCount += numContour;
|
|
|
|
// >0 => simple glyph
|
|
// -1 = composite
|
|
// 0 = empty glyph
|
|
}
|
|
else if (numContour < 0)
|
|
{
|
|
// Composite glyph, resolve later.
|
|
compositeGlyphs.Add(i);
|
|
}
|
|
}
|
|
|
|
ushort[] pntPerContours = new ushort[contourCount];
|
|
for (int i = 0; i < contourCount; i++)
|
|
{
|
|
// Each of these is the number of points of that contour.
|
|
pntPerContours[i] = Read255UInt16(reader);
|
|
}
|
|
|
|
// FlagStream, flags value for each point.
|
|
// Each byte in flags stream represents one point.
|
|
byte[] flagStream = reader.ReadBytes((int)flagStreamSize);
|
|
|
|
// Some composite glyphs have instructions so we must check all composite glyphs before read the glyph stream.
|
|
using (MemoryStream compositeMemoryStream = new())
|
|
{
|
|
reader.BaseStream.Position = compositeStreamOffset;
|
|
compositeMemoryStream.Write(reader.ReadBytes((int)compositeStreamSize), 0, (int)compositeStreamSize);
|
|
compositeMemoryStream.Position = 0;
|
|
|
|
using (BigEndianBinaryReader compositeReader = new(compositeMemoryStream, false))
|
|
{
|
|
for (ushort i = 0; i < compositeGlyphs.Count; i++)
|
|
{
|
|
ushort compositeGlyphIndex = compositeGlyphs[i];
|
|
allGlyphs[compositeGlyphIndex].CompositeHasInstructions = CompositeHasInstructions(compositeReader);
|
|
}
|
|
}
|
|
|
|
reader.BaseStream.Position = glyphStreamOffset;
|
|
}
|
|
|
|
int curFlagsIndex = 0;
|
|
int pntContourIndex = 0;
|
|
for (int i = 0; i < allGlyphs.Length; i++)
|
|
{
|
|
glyphs[i] = ReadSimpleGlyphData(
|
|
reader,
|
|
ref allGlyphs[i],
|
|
pntPerContours,
|
|
ref pntContourIndex,
|
|
flagStream,
|
|
ref curFlagsIndex);
|
|
}
|
|
|
|
// Now we read the composite stream again and create composite glyphs.
|
|
for (ushort i = 0; i < compositeGlyphs.Count; i++)
|
|
{
|
|
int compositeGlyphIndex = compositeGlyphs[i];
|
|
glyphs[compositeGlyphIndex] = ReadCompositeGlyphData(glyphs, reader);
|
|
}
|
|
|
|
// Read the bounding box stream.
|
|
reader.BaseStream.Position = bboxStreamOffset;
|
|
int bitmapCount = ((numGlyphs + 31) >> 5) << 2;
|
|
byte[] boundsBitmap = ExpandBitmap(reader.ReadBytes(bitmapCount));
|
|
for (ushort i = 0; i < numGlyphs; i++)
|
|
{
|
|
GlyphData data = allGlyphs[i];
|
|
if (boundsBitmap[i] == 1)
|
|
{
|
|
// Read explicit bounds from the stream.
|
|
// If the bounds are not explicit, the glyph loader will calculate them on demand.
|
|
glyphs[i].Bounds = Bounds.Load(reader);
|
|
}
|
|
else if (data.NumContour < 0)
|
|
{
|
|
throw new NotSupportedException("Composite glyph must have a bounding box.");
|
|
}
|
|
}
|
|
|
|
// Read the instructions stream.
|
|
reader.BaseStream.Position = instructionStreamOffset;
|
|
for (int i = 0; i < allGlyphs.Length; i++)
|
|
{
|
|
ref GlyphVector vector = ref glyphs[i];
|
|
GlyphData data = allGlyphs[i];
|
|
if (data.InstructionsLength > 0)
|
|
{
|
|
vector.Instructions = reader.ReadBytes(data.InstructionsLength);
|
|
}
|
|
|
|
glyphLoaders[i] = new Woff2GlyphLoader(vector);
|
|
}
|
|
|
|
// Finally compile the complete glyphs.
|
|
for (ushort i = 0; i < numGlyphs; i++)
|
|
{
|
|
if (!glyphs[i].HasValue())
|
|
{
|
|
glyphLoaders[i] = emptyGlyphLoader;
|
|
continue;
|
|
}
|
|
|
|
glyphLoaders[i] = new Woff2GlyphLoader(glyphs[i]);
|
|
}
|
|
|
|
return glyphLoaders;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads simple glyph outline data from the WOFF2 glyph stream, decoding point coordinates
|
|
/// using the triple encoding format defined in the WOFF2 specification.
|
|
/// </summary>
|
|
/// <param name="reader">The big-endian binary reader positioned in the glyph stream.</param>
|
|
/// <param name="glyphData">A reference to the glyph data containing contour count and instruction metadata.</param>
|
|
/// <param name="pntPerContours">The array of point counts per contour across all glyphs.</param>
|
|
/// <param name="pntContourIndex">A reference to the current index within <paramref name="pntPerContours"/>.</param>
|
|
/// <param name="flagStream">The flag stream bytes, one per outline point.</param>
|
|
/// <param name="flagStreamIndex">A reference to the current index within <paramref name="flagStream"/>.</param>
|
|
/// <returns>The decoded <see cref="GlyphVector"/>, or <see langword="default"/> for empty or composite glyphs.</returns>
|
|
private static GlyphVector ReadSimpleGlyphData(
|
|
BigEndianBinaryReader reader,
|
|
ref GlyphData glyphData,
|
|
ushort[] pntPerContours,
|
|
ref int pntContourIndex,
|
|
byte[] flagStream,
|
|
ref int flagStreamIndex)
|
|
{
|
|
if (glyphData.NumContour == 0)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
if (glyphData.NumContour < 0)
|
|
{
|
|
// Composite glyph. Check if this has instruction or not
|
|
// and read the length. We don't actually use the data but it ensures
|
|
// we maintain the correct location within the stream.
|
|
if (glyphData.CompositeHasInstructions)
|
|
{
|
|
Read255UInt16(reader);
|
|
}
|
|
|
|
return default; // Skip composite glyph (resolve later).
|
|
}
|
|
|
|
int curX = 0;
|
|
int curY = 0;
|
|
int numContour = glyphData.NumContour;
|
|
ushort[] endPoints = new ushort[numContour];
|
|
ushort pointCount = 0;
|
|
|
|
for (ushort i = 0; i < numContour; i++)
|
|
{
|
|
ushort numPoint = pntPerContours[pntContourIndex++];
|
|
pointCount += numPoint;
|
|
endPoints[i] = (ushort)(pointCount - 1);
|
|
}
|
|
|
|
var controlPoints = new ControlPoint[pointCount];
|
|
int n = 0;
|
|
for (int i = 0; i < numContour; i++)
|
|
{
|
|
int endContour = endPoints[i];
|
|
for (; n <= endContour; ++n)
|
|
{
|
|
byte f = flagStream[flagStreamIndex++];
|
|
|
|
// int f1 = (f >> 7); // Most significant 1 bit -> on/off curve.
|
|
int xyFormat = f & 0x7F; // Remaining 7 bits x, y format.
|
|
|
|
TripleEncodingRecord enc = TripleEncodingTable.EncTable[xyFormat]; // 0-128
|
|
|
|
byte[] packedXY = reader.ReadBytes(enc.ByteCount - 1); // byte count include 1 byte flags, so actual read=> byteCount-1
|
|
|
|
int x;
|
|
int y;
|
|
switch (enc.XBits)
|
|
{
|
|
default:
|
|
throw new NotSupportedException();
|
|
case 0: // 0,8,
|
|
x = 0;
|
|
y = enc.Ty(packedXY[0]);
|
|
break;
|
|
case 4: // 4,4
|
|
x = enc.Tx(packedXY[0] >> 4);
|
|
y = enc.Ty(packedXY[0] & 0xF);
|
|
break;
|
|
case 8: // 8,0 or 8,8
|
|
x = enc.Tx(packedXY[0]);
|
|
y = enc.YBits == 8 ?
|
|
enc.Ty(packedXY[1]) :
|
|
0;
|
|
break;
|
|
case 12: // 12,12
|
|
x = enc.Tx((packedXY[0] << 4) | (packedXY[1] >> 4));
|
|
y = enc.Ty(((packedXY[1] & 0xF) << 8) | packedXY[2]);
|
|
break;
|
|
case 16: // 16,16
|
|
x = enc.Tx((packedXY[0] << 8) | packedXY[1]);
|
|
y = enc.Ty((packedXY[2] << 8) | packedXY[3]);
|
|
break;
|
|
}
|
|
|
|
// Most significant 1 bit -> on/off curve.
|
|
controlPoints[n] = new(new Vector2(curX += x, curY += y), f >> 7 == 0);
|
|
}
|
|
}
|
|
|
|
// Read the instructions length for later parsing.
|
|
glyphData.InstructionsLength = Read255UInt16(reader);
|
|
|
|
// Bounds and instructions are read later.
|
|
return new GlyphVector(controlPoints, endPoints, default, Array.Empty<byte>(), false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether a composite glyph record in the composite stream contains instructions
|
|
/// by scanning through all component entries and checking the <see cref="CompositeGlyphFlags.WeHaveInstructions"/> flag.
|
|
/// </summary>
|
|
/// <param name="reader">The big-endian binary reader positioned at the start of the composite glyph record.</param>
|
|
/// <returns><see langword="true"/> if the composite glyph contains instructions; otherwise, <see langword="false"/>.</returns>
|
|
private static bool CompositeHasInstructions(BigEndianBinaryReader reader)
|
|
{
|
|
bool weHaveInstructions = false;
|
|
CompositeGlyphFlags flags = CompositeGlyphFlags.MoreComponents;
|
|
while ((flags & CompositeGlyphFlags.MoreComponents) != 0)
|
|
{
|
|
flags = reader.ReadUInt16<CompositeGlyphFlags>();
|
|
weHaveInstructions |= (flags & CompositeGlyphFlags.WeHaveInstructions) != 0;
|
|
int argSize = 2; // glyph index
|
|
if ((flags & CompositeGlyphFlags.Args1And2AreWords) != 0)
|
|
{
|
|
argSize += 4;
|
|
}
|
|
else
|
|
{
|
|
argSize += 2;
|
|
}
|
|
|
|
if ((flags & CompositeGlyphFlags.WeHaveAScale) != 0)
|
|
{
|
|
argSize += 2;
|
|
}
|
|
else if ((flags & CompositeGlyphFlags.WeHaveXAndYScale) != 0)
|
|
{
|
|
argSize += 4;
|
|
}
|
|
else if ((flags & CompositeGlyphFlags.WeHaveATwoByTwo) != 0)
|
|
{
|
|
argSize += 8;
|
|
}
|
|
|
|
reader.BaseStream.Seek(argSize, SeekOrigin.Current);
|
|
}
|
|
|
|
return weHaveInstructions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads composite glyph data from the WOFF2 composite stream, recursively resolving
|
|
/// component glyphs and applying their transforms.
|
|
/// </summary>
|
|
/// <param name="createdGlyphs">The array of all glyph vectors, used for resolving component references.</param>
|
|
/// <param name="reader">The big-endian binary reader positioned in the composite stream.</param>
|
|
/// <returns>The assembled composite <see cref="GlyphVector"/>.</returns>
|
|
private static GlyphVector ReadCompositeGlyphData(GlyphVector[] createdGlyphs, BigEndianBinaryReader reader)
|
|
{
|
|
List<ControlPoint> controlPoints = new();
|
|
List<ushort> endPoints = new();
|
|
CompositeGlyphFlags flags;
|
|
do
|
|
{
|
|
flags = reader.ReadUInt16<CompositeGlyphFlags>();
|
|
ushort glyphIndex = reader.ReadUInt16();
|
|
if (!createdGlyphs[glyphIndex].HasValue())
|
|
{
|
|
// This glyph has not been read yet, resolve it first.
|
|
long position = reader.BaseStream.Position;
|
|
createdGlyphs[glyphIndex] = ReadCompositeGlyphData(createdGlyphs, reader);
|
|
reader.BaseStream.Position = position;
|
|
}
|
|
|
|
CompositeGlyphLoader.LoadArguments(reader, flags, out int dx, out int dy);
|
|
|
|
Matrix3x2 transform = Matrix3x2.Identity;
|
|
transform.Translation = new Vector2(dx, dy);
|
|
|
|
if ((flags & CompositeGlyphFlags.WeHaveAScale) != 0)
|
|
{
|
|
float scale = reader.ReadF2Dot14();
|
|
transform.M11 = scale;
|
|
transform.M22 = scale;
|
|
}
|
|
else if ((flags & CompositeGlyphFlags.WeHaveXAndYScale) != 0)
|
|
{
|
|
transform.M11 = reader.ReadF2Dot14();
|
|
transform.M22 = reader.ReadF2Dot14();
|
|
}
|
|
else if ((flags & CompositeGlyphFlags.WeHaveATwoByTwo) != 0)
|
|
{
|
|
transform.M11 = reader.ReadF2Dot14();
|
|
transform.M12 = reader.ReadF2Dot14();
|
|
transform.M21 = reader.ReadF2Dot14();
|
|
transform.M22 = reader.ReadF2Dot14();
|
|
}
|
|
|
|
var clone = GlyphVector.DeepClone(createdGlyphs[glyphIndex]);
|
|
GlyphVector.TransformInPlace(ref clone, transform);
|
|
ushort endPointOffset = (ushort)controlPoints.Count;
|
|
|
|
controlPoints.AddRange(clone.ControlPoints);
|
|
foreach (ushort p in clone.EndPoints)
|
|
{
|
|
endPoints.Add((ushort)(p + endPointOffset));
|
|
}
|
|
}
|
|
while ((flags & CompositeGlyphFlags.MoreComponents) != 0);
|
|
|
|
// Bounds and instructions are read later.
|
|
return new GlyphVector(controlPoints, endPoints, default, Array.Empty<byte>(), true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands a packed bitmap byte array into an array of individual bit values,
|
|
/// where each byte in the result is either 0 or 1.
|
|
/// Used to decode the bounding box bitmap in the WOFF2 glyf table.
|
|
/// </summary>
|
|
/// <param name="orgBBoxBitmap">The packed bitmap bytes.</param>
|
|
/// <returns>An expanded byte array where each element represents a single bit from the input.</returns>
|
|
private static byte[] ExpandBitmap(byte[] orgBBoxBitmap)
|
|
{
|
|
byte[] expandArr = new byte[orgBBoxBitmap.Length * 8];
|
|
|
|
int index = 0;
|
|
for (int i = 0; i < orgBBoxBitmap.Length; i++)
|
|
{
|
|
byte b = orgBBoxBitmap[i];
|
|
expandArr[index++] = (byte)((b >> 7) & 0x1);
|
|
expandArr[index++] = (byte)((b >> 6) & 0x1);
|
|
expandArr[index++] = (byte)((b >> 5) & 0x1);
|
|
expandArr[index++] = (byte)((b >> 4) & 0x1);
|
|
expandArr[index++] = (byte)((b >> 3) & 0x1);
|
|
expandArr[index++] = (byte)((b >> 2) & 0x1);
|
|
expandArr[index++] = (byte)((b >> 1) & 0x1);
|
|
expandArr[index++] = (byte)((b >> 0) & 0x1);
|
|
}
|
|
|
|
return expandArr;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the UIntBase128 Data Type.
|
|
/// </summary>
|
|
/// <param name="reader">The binary reader using big endian encoding.</param>
|
|
/// <param name="result">The result as uint.</param>
|
|
/// <returns>true, if succeeded.</returns>
|
|
private static bool ReadUIntBase128(BigEndianBinaryReader reader, out uint result)
|
|
{
|
|
// UIntBase128 is a different variable length encoding of unsigned integers,
|
|
// suitable for values up to 2^(32) - 1.
|
|
// A UIntBase128 encoded number is a sequence of bytes for which the most significant bit
|
|
// is set for all but the last byte,
|
|
// and clear for the last byte.
|
|
//
|
|
// The number itself is base 128 encoded in the lower 7 bits of each byte.
|
|
// Thus, a decoding procedure for a UIntBase128 is:
|
|
// start with value = 0.
|
|
// Consume a byte, setting value = old value times 128 + (byte bitwise - and 127).
|
|
// Repeat last step until the most significant bit of byte is false.
|
|
//
|
|
// UIntBase128 encoding format allows a possibility of sub-optimal encoding,
|
|
// where e.g.the same numerical value can be represented with variable number of bytes(utilizing leading 'zeros').
|
|
// For example, the value 63 could be encoded as either one byte 0x3F or two(or more) bytes: [0x80, 0x3f].
|
|
// An encoder must not allow this to happen and must produce shortest possible encoding.
|
|
// A decoder MUST reject the font file if it encounters a UintBase128 - encoded value with leading zeros(a value that starts with the byte 0x80),
|
|
// if UintBase128 - encoded sequence is longer than 5 bytes,
|
|
// or if a UintBase128 - encoded value exceeds 232 - 1.
|
|
uint accum = 0;
|
|
result = 0;
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
byte data_byte = reader.ReadByte();
|
|
|
|
// No leading 0's
|
|
if (i == 0 && data_byte == 0x80)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If any of top 7 bits are set then << 7 would overflow.
|
|
if ((accum & 0xFE000000) != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
accum = (accum << 7) | (uint)(data_byte & 0x7F);
|
|
|
|
// Spin until most significant bit of data byte is false.
|
|
if ((data_byte & 0x80) == 0)
|
|
{
|
|
result = accum;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// UIntBase128 sequence exceeds 5 bytes.
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the UIntBase255 Data Type.
|
|
/// </summary>
|
|
/// <param name="reader">The binary reader using big endian encoding.</param>
|
|
/// <returns>The UIntBase255 result.</returns>
|
|
private static ushort Read255UInt16(BigEndianBinaryReader reader)
|
|
{
|
|
// 255UInt16 Variable-length encoding of a 16-bit unsigned integer for optimized intermediate font data storage.
|
|
// 255UInt16 is a variable-length encoding of an unsigned integer
|
|
// in the range 0 to 65535 inclusive.
|
|
// This data type is intended to be used as intermediate representation of various font values,
|
|
// which are typically expressed as UInt16 but represent relatively small values.
|
|
// Depending on the encoded value, the length of the data field may be one to three bytes,
|
|
// where the value of the first byte either represents the small value itself or is treated as a code that defines the format of the additional byte(s).
|
|
byte code = reader.ReadByte();
|
|
if (code == WordCode)
|
|
{
|
|
int value = reader.ReadByte();
|
|
value <<= 8;
|
|
value &= 0xff00;
|
|
int value2 = reader.ReadByte();
|
|
value |= value2 & 0x00ff;
|
|
|
|
return (ushort)value;
|
|
}
|
|
else if (code == OneMoreByteCode1)
|
|
{
|
|
return (ushort)(reader.ReadByte() + LowestUCode);
|
|
}
|
|
else if (code == OneMoreByteCode2)
|
|
{
|
|
return (ushort)(reader.ReadByte() + (LowestUCode * 2));
|
|
}
|
|
else
|
|
{
|
|
return code;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores intermediate metadata for a glyph being decoded from a WOFF2 transformed glyph table,
|
|
/// including its contour count, instruction length, and whether a composite glyph has instructions.
|
|
/// </summary>
|
|
private struct GlyphData
|
|
{
|
|
/// <summary>
|
|
/// The index of the glyph within the font.
|
|
/// </summary>
|
|
public readonly ushort GlyphIndex;
|
|
|
|
/// <summary>
|
|
/// The number of contours for this glyph.
|
|
/// A positive value indicates a simple glyph, negative indicates composite, and zero indicates empty.
|
|
/// </summary>
|
|
public readonly short NumContour;
|
|
|
|
/// <summary>
|
|
/// The length in bytes of the TrueType instructions for this glyph.
|
|
/// </summary>
|
|
public int InstructionsLength;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this composite glyph contains TrueType instructions.
|
|
/// </summary>
|
|
public bool CompositeHasInstructions;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="GlyphData"/> struct.
|
|
/// </summary>
|
|
/// <param name="glyphIndex">The index of the glyph within the font.</param>
|
|
/// <param name="contourCount">The number of contours for the glyph.</param>
|
|
public GlyphData(ushort glyphIndex, short contourCount)
|
|
{
|
|
this.GlyphIndex = glyphIndex;
|
|
this.NumContour = contourCount;
|
|
this.InstructionsLength = 0;
|
|
this.CompositeHasInstructions = false;
|
|
}
|
|
}
|
|
}
|
|
}
|