// 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;
}
}
}