// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; using System.IO; namespace SixLabors.Fonts.Tables.AdvancedTypographic { /// /// In OpenType Layout, index values identify glyphs. For efficiency and ease of representation, a font developer /// can group glyph indices to form glyph classes. Class assignments vary in meaning from one lookup subtable /// to another. For example, in the GSUB and GPOS tables, classes are used to describe glyph contexts. /// GDEF tables also use the idea of glyph classes. /// /// internal abstract class ClassDefinitionTable { /// /// Gets the class id for the given glyph id. /// Any glyph not included in the range of covered glyph IDs automatically belongs to Class 0. /// /// The glyph identifier. /// The class id. public abstract int ClassIndexOf(ushort glyphId); /// /// Tries to load a from the binary reader at the specified offset. /// /// The big endian binary reader. /// Offset from the beginning of the table. If 0, no table is loaded. /// When this method returns, contains the loaded table if successful. /// if the table was loaded; otherwise, . public static bool TryLoad(BigEndianBinaryReader reader, long offset, [NotNullWhen(true)] out ClassDefinitionTable? table) { if (offset == 0) { table = null; return false; } reader.Seek(offset, SeekOrigin.Begin); ushort classFormat = reader.ReadUInt16(); table = classFormat switch { 1 => ClassDefinitionFormat1Table.Load(reader), 2 => ClassDefinitionFormat2Table.Load(reader), _ => null }; return table is not null; } /// /// Loads a from the binary reader at the specified offset. /// /// The big endian binary reader. /// Offset from the beginning of the table. /// The . /// Thrown when the class format is invalid. public static ClassDefinitionTable Load(BigEndianBinaryReader reader, long offset) { reader.Seek(offset, SeekOrigin.Begin); ushort classFormat = reader.ReadUInt16(); return classFormat switch { 1 => ClassDefinitionFormat1Table.Load(reader), 2 => ClassDefinitionFormat2Table.Load(reader), _ => throw new InvalidFontFileException($"Invalid value for 'classFormat' {classFormat}. Should be '1' or '2'.") }; } } /// /// Class Definition Format 1: class assignment is defined by an array of class values /// indexed by glyph ID minus a start glyph ID. /// internal sealed class ClassDefinitionFormat1Table : ClassDefinitionTable { private readonly ushort startGlyphId; private readonly ushort[] classValueArray; /// /// Initializes a new instance of the class. /// /// The first glyph ID of the class value array. /// The array of class values, one per glyph ID. private ClassDefinitionFormat1Table(ushort startGlyphId, ushort[] classValueArray) { this.startGlyphId = startGlyphId; this.classValueArray = classValueArray; } /// /// Loads a from the binary reader. /// The format identifier has already been read. /// /// The big endian binary reader. /// The . public static ClassDefinitionFormat1Table Load(BigEndianBinaryReader reader) { // +--------+-----------------------------+------------------------------------------+ // | Type | Name | Description | // +========+=============================+==========================================+ // | uint16 | classFormat | Format identifier — format = 1 | // +--------+-----------------------------+------------------------------------------+ // | uint16 | startGlyphID | First glyph ID of the classValueArray | // +--------+-----------------------------+------------------------------------------+ // | uint16 | glyphCount | Size of the classValueArray | // +--------+-----------------------------+------------------------------------------+ // | uint16 | classValueArray[glyphCount] | Array of Class Values — one per glyph ID | // +--------+-----------------------------+------------------------------------------+ ushort startGlyphId = reader.ReadUInt16(); ushort glyphCount = reader.ReadUInt16(); ushort[] classValueArray = reader.ReadUInt16Array(glyphCount); return new ClassDefinitionFormat1Table(startGlyphId, classValueArray); } /// public override int ClassIndexOf(ushort glyphId) { int i = glyphId - this.startGlyphId; if (i >= 0 && i < this.classValueArray.Length) { return this.classValueArray[i]; } // Any glyph not included in the range of covered glyph IDs automatically belongs to Class 0. return 0; } } /// /// Class Definition Format 2: class assignment is defined by an array of ranges, /// each mapping a range of glyph IDs to a class value. /// internal sealed class ClassDefinitionFormat2Table : ClassDefinitionTable { private readonly ClassRangeRecord[] records; /// /// Initializes a new instance of the class. /// /// The array of class range records. private ClassDefinitionFormat2Table(ClassRangeRecord[] records) => this.records = records; /// /// Loads a from the binary reader. /// The format identifier has already been read. /// /// The big endian binary reader. /// The . public static ClassDefinitionFormat2Table Load(BigEndianBinaryReader reader) { // +------------------+------------------------------------+-----------------------------------------+ // | Type | Name | Description | // +==================+====================================+=========================================+ // | uint16 | classFormat | Format identifier — format = 2 | // +------------------+------------------------------------+-----------------------------------------+ // | uint16 | classRangeCount | Number of ClassRangeRecords | // +------------------+------------------------------------+-----------------------------------------+ // | ClassRangeRecord | classRangeRecords[classRangeCount] | Array of ClassRangeRecords — ordered by | // | | | startGlyphID | // +------------------+------------------------------------+-----------------------------------------+ ushort classRangeCount = reader.ReadUInt16(); ClassRangeRecord[] records = new ClassRangeRecord[classRangeCount]; for (int i = 0; i < records.Length; ++i) { // +--------+--------------+------------------------------------+ // | Type | Name | Description | // +========+==============+====================================+ // | uint16 | startGlyphID | First glyph ID in the range | // +--------+--------------+------------------------------------+ // | uint16 | endGlyphID | Last glyph ID in the range | // +--------+--------------+------------------------------------+ // | uint16 | class | Applied to all glyphs in the range | // +--------+--------------+------------------------------------+ records[i] = new ClassRangeRecord( reader.ReadUInt16(), reader.ReadUInt16(), reader.ReadUInt16()); } return new ClassDefinitionFormat2Table(records); } /// public override int ClassIndexOf(ushort glyphId) { // Records are ordered by StartGlyphId, so use binary search to find the // candidate range whose StartGlyphId is <= glyphId. ClassRangeRecord[] records = this.records; int lo = 0; int hi = records.Length - 1; while (lo <= hi) { int mid = (int)(((uint)lo + (uint)hi) >> 1); ClassRangeRecord rec = records[mid]; if (glyphId < rec.StartGlyphId) { hi = mid - 1; } else if (glyphId > rec.EndGlyphId) { lo = mid + 1; } else { return rec.Class; } } // Any glyph not included in the range of covered glyph IDs automatically belongs to Class 0. return 0; } } }