// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.IO; using static SixLabors.Fonts.Tables.AdvancedTypographic.CoverageFormat2Table; namespace SixLabors.Fonts.Tables.AdvancedTypographic { /// /// Each subtable (except an Extension LookupType subtable) in a lookup references a Coverage table (Coverage), /// which specifies all the glyphs affected by a substitution or positioning operation described in the subtable. /// The GSUB, GPOS, and GDEF tables rely on this notion of coverage. /// If a glyph does not appear in a Coverage table, the client can skip that subtable and move /// immediately to the next subtable. /// /// internal abstract class CoverageTable { /// /// Gets the coverage index for the specified glyph, or -1 if the glyph is not covered. /// /// The glyph identifier. /// The zero-based coverage index, or -1 if not found. public abstract int CoverageIndexOf(ushort glyphId); /// /// Loads a from the binary reader at the specified offset. /// /// The big endian binary reader. /// Offset from the beginning of the table. /// The . public static CoverageTable Load(BigEndianBinaryReader reader, long offset) { reader.Seek(offset, SeekOrigin.Begin); ushort coverageFormat = reader.ReadUInt16(); return coverageFormat switch { 1 => CoverageFormat1Table.Load(reader), 2 => CoverageFormat2Table.Load(reader), // Harfbuzz (Coverage.hh) treats this as an empty table and does not throw. // SofiaSans Condensed can trigger this. See https://github.com/SixLabors/Fonts/issues/470 _ => EmptyCoverageTable.Instance }; } /// /// Loads an array of values from the binary reader. /// /// The big endian binary reader. /// The base offset from which coverage offsets are relative. /// The array of offsets to individual coverage tables. /// The array of . public static CoverageTable[] LoadArray(BigEndianBinaryReader reader, long offset, ReadOnlySpan coverageOffsets) { CoverageTable[] tables = new CoverageTable[coverageOffsets.Length]; for (int i = 0; i < tables.Length; i++) { tables[i] = Load(reader, offset + coverageOffsets[i]); } return tables; } } /// /// Coverage Format 1: individual glyph indices listed in numerical order. /// internal sealed class CoverageFormat1Table : CoverageTable { private readonly ushort[] glyphArray; /// /// Initializes a new instance of the class. /// /// The array of glyph IDs in numerical order. private CoverageFormat1Table(ushort[] glyphArray) => this.glyphArray = glyphArray; /// public override int CoverageIndexOf(ushort glyphId) { int n = Array.BinarySearch(this.glyphArray, glyphId); return n < 0 ? -1 : n; } /// /// Loads a from the binary reader. /// The format identifier has already been read. /// /// The big endian binary reader. /// The . public static CoverageFormat1Table Load(BigEndianBinaryReader reader) { // +--------+------------------------+-----------------------------------------+ // | Type | Name | Description | // +========+========================+=========================================+ // | uint16 | coverageFormat | Format identifier — format = 1 | // +--------+------------------------+-----------------------------------------+ // | uint16 | glyphCount | Number of glyphs in the glyph array | // +--------+------------------------+-----------------------------------------+ // | uint16 | glyphArray[glyphCount] | Array of glyph IDs — in numerical order | // +--------+------------------------+-----------------------------------------+ ushort glyphCount = reader.ReadUInt16(); ushort[] glyphArray = reader.ReadUInt16Array(glyphCount); return new CoverageFormat1Table(glyphArray); } } /// /// Coverage Format 2: ranges of consecutive glyph IDs, ordered by startGlyphID. /// internal sealed class CoverageFormat2Table : CoverageTable { private readonly CoverageRangeRecord[] records; /// /// Initializes a new instance of the class. /// /// The array of coverage range records. private CoverageFormat2Table(CoverageRangeRecord[] records) => this.records = records; /// public override int CoverageIndexOf(ushort glyphId) { // Records are ordered by StartGlyphId, so use binary search to find the // candidate range whose StartGlyphId is <= glyphId. CoverageRangeRecord[] records = this.records; int lo = 0; int hi = records.Length - 1; while (lo <= hi) { int mid = (int)(((uint)lo + (uint)hi) >> 1); CoverageRangeRecord rec = records[mid]; if (glyphId < rec.StartGlyphId) { hi = mid - 1; } else if (glyphId > rec.EndGlyphId) { lo = mid + 1; } else { return rec.Index + glyphId - rec.StartGlyphId; } } return -1; } /// /// Loads a from the binary reader. /// The format identifier has already been read. /// /// The big endian binary reader. /// The . public static CoverageFormat2Table Load(BigEndianBinaryReader reader) { // +-------------+--------------------------+--------------------------------------------------+ // | Type | Name | Description | // +=============+==========================+==================================================+ // | uint16 | coverageFormat | Format identifier — format = 2 | // +-------------+--------------------------+--------------------------------------------------+ // | uint16 | rangeCount | Number of RangeRecords | // +-------------+--------------------------+--------------------------------------------------+ // | RangeRecord | rangeRecords[rangeCount] | Array of glyph ranges — ordered by startGlyphID. | // +-------------+--------------------------+--------------------------------------------------+ ushort rangeCount = reader.ReadUInt16(); CoverageRangeRecord[] records = new CoverageRangeRecord[rangeCount]; 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 | startCoverageIndex | Coverage Index of first glyph ID in range | // +--------+--------------------+-------------------------------------------+ records[i] = new CoverageRangeRecord( reader.ReadUInt16(), reader.ReadUInt16(), reader.ReadUInt16()); } return new CoverageFormat2Table(records); } /// /// An empty coverage table that never matches any glyph. Used as a fallback for invalid coverage formats. /// internal sealed class EmptyCoverageTable : CoverageTable { /// /// Initializes a new instance of the class. /// private EmptyCoverageTable() { } /// /// Gets the singleton instance of the empty coverage table. /// public static EmptyCoverageTable Instance { get; } = new(); /// public override int CoverageIndexOf(ushort glyphId) => -1; } } }