// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.IO; namespace SixLabors.Fonts.Tables.AdvancedTypographic { /// /// The FeatureVariations table is used in variable fonts to provide alternate sets of /// feature table lookups for different regions of the variation space. /// Shared by both GPOS and GSUB tables (version 1.1). /// /// internal sealed class FeatureVariationsTable { /// /// Initializes a new instance of the class. /// /// The array of feature variation records. private FeatureVariationsTable(FeatureVariationRecord[] records) => this.Records = records; /// /// Gets the array of feature variation records. /// public FeatureVariationRecord[] Records { get; } /// /// Loads the FeatureVariations table. /// /// The big endian binary reader. /// Absolute offset to the beginning of the FeatureVariations table. /// The FeatureListTable, used to resolve feature tags for substitutions. /// The FeatureVariationsTable, or null if the offset is 0. public static FeatureVariationsTable? Load(BigEndianBinaryReader reader, long offset, FeatureListTable featureList) { if (offset == 0) { return null; } // FeatureVariations table // +----------+------------------------------------------------------+---------------------------------------------------------------+ // | Type | Name | Description | // +==========+======================================================+===============================================================+ // | uint16 | majorVersion | Major version — set to 1 | // +----------+------------------------------------------------------+---------------------------------------------------------------+ // | uint16 | minorVersion | Minor version — set to 0 | // +----------+------------------------------------------------------+---------------------------------------------------------------+ // | uint32 | featureVariationRecordCount | Number of FeatureVariationRecords | // +----------+------------------------------------------------------+---------------------------------------------------------------+ // | FeatureVariationRecord | featureVariationRecords[count] | Array of FeatureVariationRecords | // +----------+------------------------------------------------------+---------------------------------------------------------------+ reader.Seek(offset, SeekOrigin.Begin); ushort majorVersion = reader.ReadUInt16(); ushort minorVersion = reader.ReadUInt16(); uint recordCount = reader.ReadUInt32(); // Read all record offsets first, then load data to avoid excessive seeking. int count = (int)recordCount; using Buffer conditionSetOffsetsBuffer = new(count); using Buffer substitutionOffsetsBuffer = new(count); Span conditionSetOffsets = conditionSetOffsetsBuffer.GetSpan(); Span substitutionOffsets = substitutionOffsetsBuffer.GetSpan(); for (int i = 0; i < count; i++) { conditionSetOffsets[i] = reader.ReadOffset32(); substitutionOffsets[i] = reader.ReadOffset32(); } FeatureVariationRecord[] records = new FeatureVariationRecord[count]; for (int i = 0; i < count; i++) { ConditionSetTable conditionSet = ConditionSetTable.Load(reader, offset + conditionSetOffsets[i]); FeatureTableSubstitutionRecord[] substitutions = LoadFeatureTableSubstitution(reader, offset + substitutionOffsets[i], featureList); records[i] = new FeatureVariationRecord(conditionSet, substitutions); } return new FeatureVariationsTable(records); } /// /// Finds the first matching whose conditions are satisfied /// by the given normalized coordinates, and returns its feature substitutions. /// Returns null if no record matches or no variation coordinates are available. /// /// The normalized variation coordinates. /// The matching substitution records, or null. public FeatureTableSubstitutionRecord[]? FindMatchingSubstitutions(ReadOnlySpan normalizedCoords) { if (normalizedCoords.IsEmpty) { return null; } for (int i = 0; i < this.Records.Length; i++) { if (this.Records[i].ConditionSet.Evaluate(normalizedCoords)) { return this.Records[i].Substitutions; } } return null; } /// /// Loads the FeatureTableSubstitution records from the binary reader at the specified offset. /// /// The big endian binary reader. /// Absolute offset to the FeatureTableSubstitution table. /// The FeatureListTable, used to resolve feature tags for substitutions. /// The array of feature table substitution records. private static FeatureTableSubstitutionRecord[] LoadFeatureTableSubstitution( BigEndianBinaryReader reader, long offset, FeatureListTable featureList) { // FeatureTableSubstitution table // +----------+------------------------------------------------------+---------------------------------------------------------------+ // | Type | Name | Description | // +==========+======================================================+===============================================================+ // | uint16 | majorVersion | Major version — set to 1 | // +----------+------------------------------------------------------+---------------------------------------------------------------+ // | uint16 | minorVersion | Minor version — set to 0 | // +----------+------------------------------------------------------+---------------------------------------------------------------+ // | uint16 | substitutionCount | Number of FeatureTableSubstitutionRecords | // +----------+------------------------------------------------------+---------------------------------------------------------------+ // | FeatureTableSubstitutionRecord | substitutions[count] | Array of records | // +----------+------------------------------------------------------+---------------------------------------------------------------+ reader.Seek(offset, SeekOrigin.Begin); ushort majorVersion = reader.ReadUInt16(); ushort minorVersion = reader.ReadUInt16(); ushort substitutionCount = reader.ReadUInt16(); // Read record headers (featureIndex + offset pairs). using Buffer featureIndicesBuffer = new(substitutionCount); using Buffer featureTableOffsetsBuffer = new(substitutionCount); Span featureIndices = featureIndicesBuffer.GetSpan(); Span featureTableOffsets = featureTableOffsetsBuffer.GetSpan(); for (int i = 0; i < substitutionCount; i++) { featureIndices[i] = reader.ReadUInt16(); featureTableOffsets[i] = reader.ReadOffset32(); } // Load each alternate Feature table. FeatureTableSubstitutionRecord[] records = new FeatureTableSubstitutionRecord[substitutionCount]; for (int i = 0; i < substitutionCount; i++) { ushort featureIndex = featureIndices[i]; // Resolve the original feature tag from the FeatureList so the substitute // carries the same tag. Tag featureTag = featureIndex < featureList.FeatureTables.Length ? featureList.FeatureTables[featureIndex].FeatureTag : default; FeatureTable alternateFeatureTable = FeatureTable.Load(featureTag, reader, offset + featureTableOffsets[i]); records[i] = new FeatureTableSubstitutionRecord(featureIndex, alternateFeatureTable); } return records; } } /// /// A set of conditions that must all be true for a FeatureVariationRecord to match. /// internal sealed class ConditionSetTable { /// /// Initializes a new instance of the class. /// /// The array of condition tables. private ConditionSetTable(ConditionTable[] conditions) => this.Conditions = conditions; /// /// Gets the array of condition tables that must all be satisfied. /// public ConditionTable[] Conditions { get; } /// /// Loads the from the binary reader at the specified offset. /// /// The big endian binary reader. /// Absolute offset to the beginning of the ConditionSet table. /// The . public static ConditionSetTable Load(BigEndianBinaryReader reader, long offset) { // ConditionSet table // +----------+----------------------------+------------------------------------------+ // | Type | Name | Description | // +==========+============================+==========================================+ // | uint16 | conditionCount | Number of conditions | // +----------+----------------------------+------------------------------------------+ // | Offset32 | conditionOffsets[count] | Offsets to Condition tables, from | // | | | beginning of ConditionSet table | // +----------+----------------------------+------------------------------------------+ reader.Seek(offset, SeekOrigin.Begin); ushort conditionCount = reader.ReadUInt16(); using Buffer conditionOffsetsBuffer = new(conditionCount); Span conditionOffsets = conditionOffsetsBuffer.GetSpan(); for (int i = 0; i < conditionCount; i++) { conditionOffsets[i] = reader.ReadOffset32(); } ConditionTable[] conditions = new ConditionTable[conditionCount]; for (int i = 0; i < conditionCount; i++) { conditions[i] = ConditionTable.Load(reader, offset + conditionOffsets[i]); } return new ConditionSetTable(conditions); } /// /// Evaluates whether all conditions in this set are satisfied by the given normalized coordinates. /// /// The normalized variation coordinates. /// True if all conditions match. public bool Evaluate(ReadOnlySpan normalizedCoords) { for (int i = 0; i < this.Conditions.Length; i++) { if (!this.Conditions[i].Evaluate(normalizedCoords)) { return false; } } return true; } } #pragma warning disable SA1201 // Elements should appear in the correct order /// /// A single record in the FeatureVariations table, pairing a condition set with /// a set of feature table substitutions. /// internal readonly struct FeatureVariationRecord { /// /// Initializes a new instance of the struct. /// /// The condition set that must be satisfied. /// The feature table substitutions to apply when conditions are met. public FeatureVariationRecord(ConditionSetTable conditionSet, FeatureTableSubstitutionRecord[] substitutions) { this.ConditionSet = conditionSet; this.Substitutions = substitutions; } /// /// Gets the condition set that must be satisfied for this record to apply. /// public ConditionSetTable ConditionSet { get; } /// /// Gets the array of feature table substitution records to apply when conditions are met. /// public FeatureTableSubstitutionRecord[] Substitutions { get; } } /// /// A substitution record that maps a feature index to an alternate Feature table. /// internal readonly struct FeatureTableSubstitutionRecord { /// /// Initializes a new instance of the struct. /// /// The index into the FeatureList of the feature being substituted. /// The alternate Feature table to use. public FeatureTableSubstitutionRecord(ushort featureIndex, FeatureTable alternateFeatureTable) { this.FeatureIndex = featureIndex; this.AlternateFeatureTable = alternateFeatureTable; } /// /// Gets the index into the FeatureList of the feature being substituted. /// public ushort FeatureIndex { get; } /// /// Gets the alternate Feature table to use in place of the original. /// public FeatureTable AlternateFeatureTable { get; } } /// /// A condition that checks whether a normalized coordinate for a specific axis /// falls within a given range. /// internal readonly struct ConditionTable { /// /// Initializes a new instance of the struct. /// /// The index of the variation axis. /// The minimum normalized coordinate value. /// The maximum normalized coordinate value. public ConditionTable(ushort axisIndex, float filterRangeMinValue, float filterRangeMaxValue) { this.AxisIndex = axisIndex; this.FilterRangeMinValue = filterRangeMinValue; this.FilterRangeMaxValue = filterRangeMaxValue; } /// /// Gets the index of the variation axis (into fvar axes array). /// public ushort AxisIndex { get; } /// /// Gets the minimum normalized coordinate value for the condition to be true. /// public float FilterRangeMinValue { get; } /// /// Gets the maximum normalized coordinate value for the condition to be true. /// public float FilterRangeMaxValue { get; } /// /// Loads the from the binary reader at the specified offset. /// /// The big endian binary reader. /// Absolute offset to the beginning of the Condition table. /// The . public static ConditionTable Load(BigEndianBinaryReader reader, long offset) { // Condition table, Format 1 (ConditionAxisRange) // +----------+----------------------------+------------------------------------------+ // | Type | Name | Description | // +==========+============================+==========================================+ // | uint16 | format | Format = 1 | // +----------+----------------------------+------------------------------------------+ // | uint16 | axisIndex | Index of variation axis | // +----------+----------------------------+------------------------------------------+ // | F2DOT14 | filterRangeMinValue | Minimum normalized coordinate value | // +----------+----------------------------+------------------------------------------+ // | F2DOT14 | filterRangeMaxValue | Maximum normalized coordinate value | // +----------+----------------------------+------------------------------------------+ reader.Seek(offset, SeekOrigin.Begin); ushort format = reader.ReadUInt16(); // Only Format 1 is defined. if (format != 1) { return default; } ushort axisIndex = reader.ReadUInt16(); float filterRangeMinValue = reader.ReadF2Dot14(); float filterRangeMaxValue = reader.ReadF2Dot14(); return new ConditionTable(axisIndex, filterRangeMinValue, filterRangeMaxValue); } /// /// Evaluates whether the given normalized coordinates satisfy this condition. /// /// The normalized variation coordinates. /// True if the coordinate for this axis is within the filter range. public bool Evaluate(ReadOnlySpan normalizedCoords) { if (this.AxisIndex >= normalizedCoords.Length) { return false; } float coord = normalizedCoords[this.AxisIndex]; return coord >= this.FilterRangeMinValue && coord <= this.FilterRangeMaxValue; } } #pragma warning restore SA1201 }