// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.IO; namespace SixLabors.Fonts.Tables.AdvancedTypographic.Variations { /// /// Implements loading glyph variation data structure. /// /// internal class GlyphVariationData { /// /// Mask for the low bits to give the number of tuple variation tables. /// internal const int CountMask = 0x0FFF; /// /// Flag indicating that some or all tuple variation tables reference a shared set of "point" numbers. /// These shared numbers are represented as packed point number data at the start of the serialized data. /// internal const int SharedPointNumbersMask = 0x8000; /// /// Flag indicating that packed deltas are zero and omitted. Lower 6 bits give run count - 1. /// private const int DeltasAreZero = 0x80; /// /// Flag indicating that packed deltas are 16-bit (int16). Lower 6 bits give run count - 1. /// If neither nor is set, deltas are 8-bit (int8). /// private const int DeltasAreWords = 0x40; /// /// Mask for the lower 6 bits of a delta run header, giving run count - 1. /// private const int DeltaRunCountMask = 0x3F; /// /// Flag in the first byte of packed point numbers indicating that point numbers are 16-bit. /// private const int PointsAreWords = 0x80; /// /// Mask for the lower 7 bits of a point run header, giving run count - 1. /// private const int PointRunCountMask = 0x7F; /// /// Initializes a new instance of the class. /// /// The decoded tuple variation headers with their point indices and deltas. public GlyphVariationData(TupleVariationHeader[] tupleHeaders) => this.TupleHeaders = tupleHeaders; /// /// Gets the tuple variation headers with their decoded point indices and deltas. /// public TupleVariationHeader[] TupleHeaders { get; } /// /// Gets a value indicating whether this glyph has any variation data. /// public bool HasData => this.TupleHeaders.Length > 0; /// /// Loads glyph variation data from the specified binary reader. /// /// The big-endian binary reader. /// The byte offset from the start of the gvar table to this glyph's variation data. /// The number of variation axes. /// The . public static GlyphVariationData Load(BigEndianBinaryReader reader, long offset, int axisCount) { // GlyphVariationData // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ // | Type | Name | Description | // +======================+===========================================+==============================================================================+ // | uint16 | tupleVariationCount | A packed field. The high 4 bits are flags, | // | | | and the low 12 bits are the number of tuple variation tables for this glyph. | // | | | The count can be any number between 1 and 4095. | // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ // | Offset16 | dataOffset | Offset from the start of the GlyphVariationData table to the serialized data.| // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ // | TupleVariation | tupleVariationHeaders[tupleVariationCount]| Array of tuple variation headers. | // +----------------------+-------------------------------------------+------------------------------------------------------------------------------+ // NOTE: 'offset' is relative to the start of the gvar table. reader.Seek(offset, SeekOrigin.Begin); ushort tupleVariationCount = reader.ReadUInt16(); bool hasSharedPointNumbers = (tupleVariationCount & SharedPointNumbersMask) != 0; int tupleCount = tupleVariationCount & CountMask; // Spec: dataOffset is Offset16 (always 16-bit), independent of the gvar offset array format. // This offset is relative to the start of this GlyphVariationData table. ushort serializedDataOffset = reader.ReadOffset16(); // Read all tuple variation headers first (they come before the serialized data). TupleVariation[] tupleVariations = new TupleVariation[tupleCount]; for (int i = 0; i < tupleCount; i++) { tupleVariations[i] = TupleVariation.Load(reader, axisCount); } // Now read the serialized data that follows the headers. long serializedDataPos = offset + serializedDataOffset; reader.Seek(serializedDataPos, SeekOrigin.Begin); // If shared point numbers flag is set, decode them from the start of the serialized data. ushort[]? sharedPointNumbers = null; if (hasSharedPointNumbers) { sharedPointNumbers = DecodePackedPoints(reader); } // Decode each tuple's serialized data (point numbers and deltas). TupleVariationHeader[] tupleHeaders = new TupleVariationHeader[tupleCount]; for (int i = 0; i < tupleCount; i++) { TupleVariation header = tupleVariations[i]; long tupleDataStart = reader.BaseStream.Position; // Determine which point numbers this tuple uses. ushort[]? pointNumbers; if (header.HasPrivatePointNumbers) { pointNumbers = DecodePackedPoints(reader); } else { pointNumbers = sharedPointNumbers; } // The number of deltas to decode depends on whether specific points are referenced. // If pointNumbers is empty (length 0), deltas apply to all points and the count // is determined by the caller (TransformPoints). We use VariationDataSize to bound reading. int nPoints = pointNumbers is { Length: > 0 } ? pointNumbers.Length : 0; short[]? deltasX = null; short[]? deltasY = null; if (nPoints > 0) { deltasX = DecodePackedDeltas(reader, nPoints); deltasY = DecodePackedDeltas(reader, nPoints); } else { // When no explicit points are specified, we need to read all remaining data // for this tuple. The deltas apply to all glyph points + 4 phantom points. // We cannot know the point count here, so we store the raw bytes and decode later. // However, the simpler approach used by fontkit is to decode based on the remaining // bytes in this tuple's data block. We'll defer full decoding to TransformPoints // by storing the raw data range. long bytesConsumed = reader.BaseStream.Position - tupleDataStart; int remaining = header.VariationDataSize - (int)bytesConsumed; if (remaining > 0) { // Store raw bytes for deferred decoding when we know the point count. tupleHeaders[i] = new TupleVariationHeader(header, pointNumbers, null, null, reader.ReadBytes(remaining)); continue; } } // Skip any remaining bytes for this tuple that we haven't consumed. long consumed = reader.BaseStream.Position - tupleDataStart; int skip = header.VariationDataSize - (int)consumed; if (skip > 0) { reader.BaseStream.Position += skip; } tupleHeaders[i] = new TupleVariationHeader(header, pointNumbers, deltasX, deltasY, null); } return new GlyphVariationData(tupleHeaders); } /// /// Decodes packed point numbers from the serialized data. /// /// The binary reader positioned at the packed point data. /// /// An array of absolute point indices, or an empty array if all points are referenced. /// /// internal static ushort[] DecodePackedPoints(BigEndianBinaryReader reader) { // First byte determines the count of points. byte firstByte = reader.ReadByte(); int count; if ((firstByte & PointsAreWords) != 0) { // High bit set: count is ((firstByte & 0x7F) << 8) | nextByte. count = ((firstByte & PointRunCountMask) << 8) | reader.ReadByte(); } else { count = firstByte; } // A count of 0 means "all points" — return empty array as sentinel. if (count == 0) { return []; } // Read run-length encoded point number deltas. ushort[] points = new ushort[count]; int i = 0; while (i < count) { byte runHeader = reader.ReadByte(); bool runPointsAreWords = (runHeader & PointsAreWords) != 0; int runCount = (runHeader & PointRunCountMask) + 1; ushort accumulator = i > 0 ? points[i - 1] : (ushort)0; for (int j = 0; j < runCount && i < count; j++, i++) { ushort delta = runPointsAreWords ? reader.ReadUInt16() : reader.ReadByte(); accumulator += delta; points[i] = accumulator; } } return points; } /// /// Decodes packed delta values from the serialized data. /// /// The binary reader positioned at the packed delta data. /// The number of delta values to decode. /// An array of decoded delta values. /// internal static short[] DecodePackedDeltas(BigEndianBinaryReader reader, int count) { short[] deltas = new short[count]; int i = 0; while (i < count) { byte runHeader = reader.ReadByte(); bool areZero = (runHeader & DeltasAreZero) != 0; bool areWords = (runHeader & DeltasAreWords) != 0; int runCount = (runHeader & DeltaRunCountMask) + 1; for (int j = 0; j < runCount && i < count; j++, i++) { if (areZero) { deltas[i] = 0; } else if (areWords) { deltas[i] = reader.ReadInt16(); } else { deltas[i] = (short)(sbyte)reader.ReadByte(); } } } return deltas; } } /// /// Represents a fully decoded tuple variation header with its associated point indices and delta values. /// internal class TupleVariationHeader { /// /// Initializes a new instance of the class. /// /// The tuple variation header containing peak coordinates and flags. /// The point indices this tuple applies to, or null/empty for all points. /// The X coordinate deltas, or null if deferred. /// The Y coordinate deltas, or null if deferred. /// The raw serialized delta data for deferred decoding, or null if already decoded. public TupleVariationHeader( TupleVariation tupleVariation, ushort[]? pointNumbers, short[]? deltasX, short[]? deltasY, byte[]? rawDeltaData) { this.TupleVariation = tupleVariation; this.PointNumbers = pointNumbers; this.DeltasX = deltasX; this.DeltasY = deltasY; this.RawDeltaData = rawDeltaData; } /// /// Gets the tuple variation header containing peak coordinates and flags. /// public TupleVariation TupleVariation { get; } /// /// Gets the point indices this tuple applies to. /// An empty array means all points are referenced. /// Null means no point data was available. /// public ushort[]? PointNumbers { get; } /// /// Gets the X coordinate deltas for the referenced points. /// Null when deltas apply to all points and were deferred (see ). /// public short[]? DeltasX { get; } /// /// Gets the Y coordinate deltas for the referenced points. /// Null when deltas apply to all points and were deferred (see ). /// public short[]? DeltasY { get; } /// /// Gets the raw serialized delta data for deferred decoding. /// This is used when point numbers indicate "all points" and the actual point count /// is not known until is called. /// public byte[]? RawDeltaData { get; } } }