// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using SixLabors.Fonts.Tables.AdvancedTypographic.GPos;
using SixLabors.Fonts.Tables.AdvancedTypographic.Shapers;
using SixLabors.Fonts.Unicode;
namespace SixLabors.Fonts.Tables.AdvancedTypographic {
///
/// The Glyph Positioning table (GPOS) provides precise control over glyph placement for
/// sophisticated text layout and rendering in each script and language system that a font supports.
///
///
internal class GPosTable : Table
{
///
/// The tag for the horizontal kerning feature ('kern').
///
private static readonly Tag KernTag = Tag.Parse("kern");
///
/// The tag for the vertical kerning feature ('vkrn').
///
private static readonly Tag VKernTag = Tag.Parse("vkrn");
///
/// The OpenType table tag for the GPOS table.
///
internal const string TableName = "GPOS";
///
/// Initializes a new instance of the class.
///
/// The script list table, or if not present.
/// The feature list table.
/// The lookup list table.
/// The feature variations table for variable fonts, or .
public GPosTable(ScriptList? scriptList, FeatureListTable featureList, LookupListTable lookupList, FeatureVariationsTable? featureVariations = null)
{
this.ScriptList = scriptList;
this.FeatureList = featureList;
this.LookupList = lookupList;
this.FeatureVariations = featureVariations;
}
///
/// Gets the script list table, or if not present.
///
public ScriptList? ScriptList { get; }
///
/// Gets the feature list table.
///
public FeatureListTable FeatureList { get; }
///
/// Gets the lookup list table containing all positioning lookups.
///
public LookupListTable LookupList { get; }
///
/// Gets the feature variations table for variable fonts, or if not present.
///
public FeatureVariationsTable? FeatureVariations { get; }
///
/// Loads the from the font reader.
///
/// The font reader.
/// The , or if not present.
public static GPosTable? Load(FontReader fontReader)
{
if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader))
{
return null;
}
using (binaryReader)
{
return Load(binaryReader);
}
}
///
/// Loads the from a big endian binary reader.
///
/// The big endian binary reader.
/// The .
internal static GPosTable Load(BigEndianBinaryReader reader)
{
// GPOS Header, Version 1.0
// +----------+-------------------+-----------------------------------------------------------+
// | Type | Name | Description |
// +==========+===================+===========================================================+
// | uint16 | majorVersion | Major version of the GPOS table, = 1 |
// +----------+-------------------+-----------------------------------------------------------+
// | uint16 | minorVersion | Minor version of the GPOS table, = 0 |
// +----------+-------------------+-----------------------------------------------------------+
// | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GPOS table |
// +----------+-------------------+-----------------------------------------------------------+
// | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GPOS table |
// +----------+-------------------+-----------------------------------------------------------+
// | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GPOS table |
// +----------+-------------------+-----------------------------------------------------------+
// GPOS Header, Version 1.1
// +----------+-------------------------+-------------------------------------------------------------------------------+
// | Type | Name | Description |
// +==========+=========================+===============================================================================+
// | uint16 | majorVersion | Major version of the GPOS table, = 1 |
// +----------+-------------------------+-------------------------------------------------------------------------------+
// | uint16 | minorVersion | Minor version of the GPOS table, = 1 |
// +----------+-------------------------+-------------------------------------------------------------------------------+
// | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GPOS table |
// +----------+-------------------------+-------------------------------------------------------------------------------+
// | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GPOS table |
// +----------+-------------------------+-------------------------------------------------------------------------------+
// | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GPOS table |
// +----------+-------------------------+-------------------------------------------------------------------------------+
// | Offset32 | featureVariationsOffset | Offset to FeatureVariations table, from beginning of GPOS table (may be NULL) |
// +----------+-------------------------+-------------------------------------------------------------------------------+
ushort majorVersion = reader.ReadUInt16();
ushort minorVersion = reader.ReadUInt16();
ushort scriptListOffset = reader.ReadOffset16();
ushort featureListOffset = reader.ReadOffset16();
ushort lookupListOffset = reader.ReadOffset16();
uint featureVariationsOffset = (minorVersion == 1) ? reader.ReadOffset32() : 0;
// TODO: Optimization. Allow only reading the scriptList.
ScriptList? scriptList = ScriptList.Load(reader, scriptListOffset);
FeatureListTable featureList = FeatureListTable.Load(reader, featureListOffset);
LookupListTable lookupList = LookupListTable.Load(reader, lookupListOffset);
FeatureVariationsTable? featureVariations = featureVariationsOffset != 0
? FeatureVariationsTable.Load(reader, featureVariationsOffset, featureList)
: null;
return new GPosTable(scriptList, featureList, lookupList, featureVariations);
}
///
/// Tries to update the positions of glyphs in the collection using GPOS lookup rules.
///
/// The font metrics.
/// The glyph positioning collection.
/// When this method returns, indicates whether kerning was applied.
/// if any positioning was updated; otherwise, .
public bool TryUpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, out bool kerned)
{
// Set max constraints to prevent OutOfMemoryException or infinite loops from attacks.
int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(collection.Count);
int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(collection.Count);
int currentOperations = 0;
bool maxOperationsReached = false;
kerned = false;
bool updated = false;
for (int i = 0; i < collection.Count; i++)
{
if (!collection.ShouldProcess(fontMetrics, i))
{
continue;
}
ScriptClass current = this.GetScriptClass(CodePoint.GetScriptClass(collection[i].CodePoint));
int index = i;
int count = 1;
while (i < collection.Count - 1)
{
// We want to assign the same feature lookups to individual sections of the text rather
// than the text as a whole to ensure that different language shapers do not interfere
// with each other when the text contains multiple languages.
int ni = i + 1;
GlyphShapingData nextData = collection[ni];
if (!collection.ShouldProcess(fontMetrics, ni))
{
break;
}
ScriptClass next = this.GetScriptClass(CodePoint.GetScriptClass(nextData.CodePoint));
if (next != current &&
current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited &&
next is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited)
{
break;
}
if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited)
{
current = next;
}
i++;
count++;
if (i >= maxCount)
{
break;
}
}
Tag unicodeScriptTag = this.GetUnicodeScriptTag(current);
BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, collection.TextOptions);
if (shaper.MarkZeroingMode == MarkZeroingMode.PreGPos)
{
ZeroMarkAdvances(fontMetrics, collection, index, count);
}
// Plan positioning features for each glyph.
shaper.Plan(collection, index, count);
IEnumerable shapingStages = shaper.GetShapingStages();
SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0);
foreach (ShapingStage stage in shapingStages)
{
stage.PreProcessFeature(collection, index, count);
Tag featureTag = stage.FeatureTag;
if (this.TryGetFeatureLookups(fontMetrics, in featureTag, current, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups))
{
// Apply features in order.
foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups)
{
Tag feature = featureLookup.Feature;
LookupTable featureLookupTable = featureLookup.LookupTable;
iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet);
while (iterator.Index < index + count)
{
if (currentOperations++ >= maxOperationsCount)
{
maxOperationsReached = true;
goto EndLookups;
}
if (!collection[iterator.Index].EnabledFeatureTags.Contains(feature))
{
iterator.Next();
continue;
}
bool success = featureLookup.LookupTable.TryUpdatePosition(fontMetrics, this, collection, featureLookup.Feature, iterator.Index, count - (iterator.Index - index));
kerned |= success && (feature == KernTag || feature == VKernTag);
updated |= success;
iterator.Next();
}
}
}
stage.PostProcessFeature(collection, index, count);
}
EndLookups:
if (shaper.MarkZeroingMode == MarkZeroingMode.PostGpos)
{
ZeroMarkAdvances(fontMetrics, collection, index, count);
}
FixCursiveAttachment(collection, index, count);
FixMarkAttachment(collection, index, count);
UpdatePositions(fontMetrics, collection, index, count);
if (i >= maxCount || maxOperationsReached)
{
return updated;
}
}
return updated;
}
///
/// Tries to get the feature lookups for the given stage feature and script.
///
/// The font metrics.
/// The feature tag for the current shaping stage.
/// The script class.
/// When this method returns, contains the list of feature lookups if found.
/// if lookups were found; otherwise, .
private bool TryGetFeatureLookups(
FontMetrics fontMetrics,
in Tag stageFeature,
ScriptClass script,
[NotNullWhen(true)] out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? value)
{
if (this.ScriptList is null)
{
value = null;
return false;
}
// Resolve feature substitutions from FeatureVariations (variable fonts).
FeatureTableSubstitutionRecord[]? substitutions = this.FeatureVariations
?.FindMatchingSubstitutions(fontMetrics.GetNormalizedCoordinates());
ScriptListTable scriptListTable = this.ScriptList.Default();
Tag[] tags = UnicodeScriptTagMap.Instance[script];
for (int i = 0; i < tags.Length; i++)
{
if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? table))
{
scriptListTable = table;
break;
}
}
LangSysTable? defaultLangSysTable = scriptListTable.DefaultLangSysTable;
if (defaultLangSysTable != null)
{
value = this.GetFeatureLookups(stageFeature, substitutions, defaultLangSysTable);
return value.Count > 0;
}
value = this.GetFeatureLookups(stageFeature, substitutions, scriptListTable.LangSysTables);
return value.Count > 0;
}
///
/// Gets the OpenType script tag for the given script class, checking against the font's ScriptList.
///
/// The script class.
/// The matching script tag, or default if not found.
private Tag GetUnicodeScriptTag(ScriptClass script)
{
if (this.ScriptList is null)
{
return default;
}
Tag[] tags = UnicodeScriptTagMap.Instance[script];
for (int i = 0; i < tags.Length; i++)
{
if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? _))
{
return tags[i];
}
}
return default;
}
///
/// Gets the feature lookups for the given stage feature from the specified language system tables.
///
/// The feature tag for the current shaping stage.
/// Optional feature table substitutions from FeatureVariations.
/// The language system tables to search.
/// A sorted list of feature lookups.
private List<(Tag Feature, ushort Index, LookupTable LookupTable)> GetFeatureLookups(
in Tag stageFeature,
FeatureTableSubstitutionRecord[]? substitutions,
params LangSysTable[] langSysTables)
{
List<(Tag Feature, ushort Index, LookupTable LookupTable)> lookups = [];
for (int i = 0; i < langSysTables.Length; i++)
{
ushort[] featureIndices = langSysTables[i].FeatureIndices;
for (int j = 0; j < featureIndices.Length; j++)
{
ushort featureIndex = featureIndices[j];
FeatureTable featureTable = ResolveFeatureTable(this.FeatureList, featureIndex, substitutions);
Tag feature = featureTable.FeatureTag;
if (stageFeature != feature)
{
continue;
}
ushort[] lookupListIndices = featureTable.LookupListIndices;
for (int k = 0; k < lookupListIndices.Length; k++)
{
ushort lookupIndex = lookupListIndices[k];
LookupTable lookupTable = this.LookupList.LookupTables[lookupIndex];
lookups.Add(new(feature, lookupIndex, lookupTable));
}
}
}
lookups.Sort((x, y) => x.Index - y.Index);
return lookups;
}
///
/// Resolves the feature table for the given index, checking for substitutions from FeatureVariations first.
///
/// The feature list table.
/// The feature index.
/// Optional feature table substitutions from FeatureVariations.
/// The resolved feature table.
private static FeatureTable ResolveFeatureTable(
FeatureListTable featureList,
ushort featureIndex,
FeatureTableSubstitutionRecord[]? substitutions)
{
if (substitutions is not null)
{
for (int i = 0; i < substitutions.Length; i++)
{
if (substitutions[i].FeatureIndex == featureIndex)
{
return substitutions[i].AlternateFeatureTable;
}
}
}
return featureList.FeatureTables[featureIndex];
}
///
/// Maps a script class to an effective script class, checking whether the font supports it.
/// Falls back to if the script is not present in the font.
///
/// The script class to check.
/// The effective script class.
private ScriptClass GetScriptClass(ScriptClass current)
{
if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited)
{
return current;
}
if (this.ScriptList is null)
{
return ScriptClass.Default;
}
Tag[] tags = UnicodeScriptTagMap.Instance[current];
for (int i = 0; i < tags.Length; i++)
{
if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? _))
{
return current;
}
}
// Script for `current` not present in the font: use default shaper.
return ScriptClass.Default;
}
///
/// Fixes cursive attachment positioning by propagating Y (or X for vertical) offsets.
///
/// The glyph positioning collection.
/// The starting index.
/// The number of glyphs to process.
private static void FixCursiveAttachment(GlyphPositioningCollection collection, int index, int count)
{
LayoutMode layoutMode = collection.TextOptions.LayoutMode;
for (int i = 0; i < count; i++)
{
int currentIndex = i + index;
GlyphShapingData data = collection[currentIndex];
if (data.CursiveAttachment != -1)
{
int j = data.CursiveAttachment + currentIndex;
if (j < index || j >= index + count)
{
return;
}
GlyphShapingData cursiveData = collection[j];
if (!AdvancedTypographicUtils.IsVerticalGlyph(data.CodePoint, layoutMode))
{
data.Bounds.Y += cursiveData.Bounds.Y;
}
else
{
data.Bounds.X += cursiveData.Bounds.X;
}
}
}
}
///
/// Fixes mark attachment positioning by propagating offsets from base glyphs.
///
/// The glyph positioning collection.
/// The starting index.
/// The number of glyphs to process.
private static void FixMarkAttachment(GlyphPositioningCollection collection, int index, int count)
{
for (int i = 0; i < count; i++)
{
int currentIndex = i + index;
GlyphShapingData data = collection[currentIndex];
if (data.MarkAttachment != -1)
{
int j = data.MarkAttachment;
GlyphShapingData markData = collection[j];
data.Bounds.X += markData.Bounds.X;
data.Bounds.Y += markData.Bounds.Y;
if (data.Direction == TextDirection.LeftToRight)
{
for (int k = j; k < currentIndex; k++)
{
markData = collection[k];
data.Bounds.X -= markData.Bounds.Width;
data.Bounds.Y -= markData.Bounds.Height;
}
}
else
{
for (int k = j + 1; k < currentIndex + 1; k++)
{
markData = collection[k];
data.Bounds.X += markData.Bounds.Width;
data.Bounds.Y += markData.Bounds.Height;
}
}
}
}
}
///
/// Zeros the advance widths and heights for mark glyphs within the specified range.
///
/// The font metrics.
/// The glyph positioning collection.
/// The starting index.
/// The number of glyphs to process.
private static void ZeroMarkAdvances(FontMetrics fontMetrics, GlyphPositioningCollection collection, int index, int count)
{
for (int i = 0; i < count; i++)
{
int currentIndex = i + index;
GlyphShapingData data = collection[currentIndex];
if (AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data))
{
data.Bounds.Width = 0;
data.Bounds.Height = 0;
}
}
}
///
/// Updates glyph positions in the collection for the specified range.
///
/// The font metrics.
/// The glyph positioning collection.
/// The starting index.
/// The number of glyphs to process.
private static void UpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, int index, int count)
{
for (int i = 0; i < count; i++)
{
int currentIndex = i + index;
collection.UpdatePosition(fontMetrics, currentIndex);
}
}
}
}