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