// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.Fonts.Tables.AdvancedTypographic.GPos;
using SixLabors.Fonts.Unicode;
using System;
using System.Collections.Generic;
namespace SixLabors.Fonts.Tables.AdvancedTypographic {
///
/// Provides shared utility methods for advanced typographic layout processing in GPOS and GSUB tables.
///
internal static class AdvancedTypographicUtils
{
///
/// The maximum context length for sequence matching operations.
/// Used to prevent excessive processing from maliciously crafted fonts.
/// Based on HarfBuzz hb-buffer.hh.
///
public const int MaxContextLength = 64;
///
/// The maximum length factor multiplied by collection count to compute max allowable collection size.
///
private const int MaxLengthFactor = 64;
///
/// The minimum value for the max allowable collection size.
///
private const int MaxLengthMinimum = 16384;
///
/// The maximum operations factor multiplied by collection count to compute max allowable operations.
///
private const int MaxOperationsFactor = 1024;
///
/// The minimum value for the max allowable operations count.
///
private const int MaxOperationsMinimum = 16384;
///
/// The absolute maximum number of shaping characters, set to half of int.MaxValue.
///
private const int MaxShapingCharsLength = 0x3FFFFFFF; // Half int max.
///
/// Defines the direction for sequence matching operations.
///
internal enum MatchDirection
{
///
/// Match in the forward direction.
///
Forward,
///
/// Match in the backward direction.
///
Backward
}
///
/// Gets a value indicating whether the glyph represented by the codepoint should be interpreted vertically.
///
/// The codepoint represented by the glyph.
/// The layout mode.
/// The .
public static bool IsVerticalGlyph(CodePoint codePoint, LayoutMode layoutMode)
{
if (layoutMode.IsVertical())
{
return true;
}
bool isVerticalLayout = layoutMode.IsVerticalMixed();
return isVerticalLayout && CodePoint.GetVerticalOrientationType(codePoint) is VerticalOrientationType.Upright or VerticalOrientationType.TransformUpright;
}
///
/// Gets the maximum allowable shaping collection count for the given input length.
///
/// The input collection length.
/// The maximum allowable count.
public static int GetMaxAllowableShapingCollectionCount(int length)
=> (int)Math.Min(Math.Max((long)length * MaxLengthFactor, MaxLengthMinimum), MaxShapingCharsLength);
///
/// Gets the maximum allowable shaping operations count for the given input length.
///
/// The input collection length.
/// The maximum allowable operations count.
public static int GetMaxAllowableShapingOperationsCount(int length)
=> (int)Math.Min(Math.Max((long)length * MaxOperationsFactor, MaxOperationsMinimum), MaxShapingCharsLength);
///
/// Applies nested lookups from sequence lookup records for GSUB contextual/chaining lookups.
///
/// The font metrics.
/// The GSUB table.
/// The feature tag being applied.
/// The lookup flags for glyph filtering.
/// The mark filtering set index.
/// The sequence lookup records specifying which lookups to apply at which positions.
/// The glyph substitution collection.
/// The starting index in the collection.
/// The number of glyphs in the input sequence.
/// if the lookups were applied.
public static bool ApplyLookupList(
FontMetrics fontMetrics,
GSubTable table,
Tag feature,
LookupFlags lookupFlags,
ushort markFilteringSet,
SequenceLookupRecord[] records,
GlyphSubstitutionCollection collection,
int index,
int count)
{
SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet);
int currentCount = collection.Count;
foreach (SequenceLookupRecord lookupRecord in records)
{
ushort sequenceIndex = lookupRecord.SequenceIndex;
ushort lookupIndex = lookupRecord.LookupListIndex;
iterator.Index = index;
iterator.Increment(sequenceIndex);
GSub.LookupTable lookup = table.LookupList.LookupTables[lookupIndex];
_ = lookup.TrySubstitution(fontMetrics, table, collection, feature, iterator.Index, count - (iterator.Index - index));
// Account for substitutions changing the length of the collection.
if (collection.Count != currentCount)
{
count -= currentCount - collection.Count;
currentCount = collection.Count;
}
}
return true;
}
///
/// Applies nested lookups from sequence lookup records for GPOS contextual/chaining lookups.
///
/// The font metrics.
/// The GPOS table.
/// The feature tag being applied.
/// The lookup flags for glyph filtering.
/// The mark filtering set index.
/// The sequence lookup records specifying which lookups to apply at which positions.
/// The glyph positioning collection.
/// The starting index in the collection.
/// The number of glyphs in the input sequence.
/// if the lookups were applied.
public static bool ApplyLookupList(
FontMetrics fontMetrics,
GPosTable table,
Tag feature,
LookupFlags lookupFlags,
ushort markFilteringSet,
SequenceLookupRecord[] records,
GlyphPositioningCollection collection,
int index,
int count)
{
SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet);
foreach (SequenceLookupRecord lookupRecord in records)
{
ushort sequenceIndex = lookupRecord.SequenceIndex;
ushort lookupIndex = lookupRecord.LookupListIndex;
iterator.Index = index;
iterator.Increment(sequenceIndex);
LookupTable lookup = table.LookupList.LookupTables[lookupIndex];
_ = lookup.TryUpdatePosition(fontMetrics, table, collection, feature, iterator.Index, count - (iterator.Index - index));
}
return true;
}
///
/// Matches an input glyph sequence by glyph ID, verifying that each glyph has the specified feature enabled.
///
/// The skipping glyph iterator.
/// The feature tag that must be enabled on matched glyphs.
/// The initial increment from the iterator's current position.
/// The array of glyph IDs to match.
/// A span to store matched glyph indices, or default if not needed.
/// if the entire sequence was matched; otherwise, .
public static bool MatchInputSequence(SkippingGlyphIterator iterator, Tag feature, ushort increment, ushort[] sequence, Span matches)
=> Match(
increment,
sequence,
iterator,
(component, data) =>
{
if (!ContainsFeatureTag(data.Features, feature))
{
return false;
}
return component == data.GlyphId;
},
matches);
///
/// Determines whether the feature list contains the specified feature tag in an enabled state.
///
/// The list of tag entries to search.
/// The feature tag to find.
/// if the feature is present and enabled; otherwise, .
private static bool ContainsFeatureTag(List featureList, Tag feature)
{
foreach (TagEntry tagEntry in featureList)
{
if (tagEntry.Tag == feature && tagEntry.Enabled)
{
return true;
}
}
return false;
}
///
/// Matches a glyph sequence by glyph ID.
///
/// The skipping glyph iterator.
/// The initial increment from the iterator's current position.
/// The array of glyph IDs to match.
/// if the entire sequence was matched; otherwise, .
public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, ushort[] sequence)
=> Match(
increment,
sequence,
iterator,
(component, data) => component == data.GlyphId,
default);
///
/// Matches a glyph sequence by class values using a class definition table.
///
/// The skipping glyph iterator.
/// The initial increment from the iterator's current position.
/// The array of class values to match.
/// The class definition table used to map glyph IDs to class values.
/// if the entire sequence was matched; otherwise, .
public static bool MatchClassSequence(
SkippingGlyphIterator iterator,
int increment,
ushort[] sequence,
ClassDefinitionTable classDefinitionTable)
=> Match(
increment,
sequence,
iterator,
(component, data) => component == classDefinitionTable.ClassIndexOf(data.GlyphId),
default);
///
/// Matches a forward glyph sequence using coverage tables.
///
/// The skipping glyph iterator.
/// The array of coverage tables to match against.
/// The starting index in the collection.
/// The exclusive end index in the collection.
/// if all coverage tables matched; otherwise, .
public static bool MatchCoverageSequence(
SkippingGlyphIterator iterator,
CoverageTable[] coverageTable,
int startIndex,
int endExclusive)
=> Match(
iterator,
startIndex,
coverageTable,
MatchDirection.Forward,
endExclusive,
(component, data) => component.CoverageIndexOf(data.GlyphId) >= 0,
default);
///
/// Matches a backward (backtrack) glyph sequence using coverage tables.
/// Per the spec, backtrack[0] matches i-1, then i-2, and so on.
///
/// The skipping glyph iterator.
/// The array of backtrack coverage tables to match against.
/// The starting index in the collection (the first backtrack position).
/// The exclusive end index in the collection.
/// if all backtrack coverage tables matched; otherwise, .
public static bool MatchBacktrackCoverageSequence(
SkippingGlyphIterator iterator,
CoverageTable[] backtrack,
int startIndex,
int endExclusive)
=> Match(
iterator,
startIndex,
backtrack,
MatchDirection.Backward,
endExclusive,
(component, data) => component.CoverageIndexOf(data.GlyphId) >= 0,
default);
///
/// Applies a chained sequence rule by matching backtrack, input, and lookahead glyph ID sequences.
///
/// The skipping glyph iterator.
/// The chained sequence rule table to apply.
/// if all sequences matched; otherwise, .
public static bool ApplyChainedSequenceRule(SkippingGlyphIterator iterator, ChainedSequenceRuleTable rule)
{
if (rule.BacktrackSequence.Length > 0
&& !MatchSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence))
{
return false;
}
if (rule.InputSequence.Length > 0
&& !MatchSequence(iterator, 1, rule.InputSequence))
{
return false;
}
if (rule.LookaheadSequence.Length > 0
&& !MatchSequence(iterator, 1 + rule.InputSequence.Length, rule.LookaheadSequence))
{
return false;
}
return true;
}
///
/// Applies a chained class sequence rule by matching backtrack, input, and lookahead class sequences.
///
/// The skipping glyph iterator.
/// The chained class sequence rule table to apply.
/// The class definition table for the input sequence.
/// The class definition table for the backtrack sequence.
/// The class definition table for the lookahead sequence.
/// if all sequences matched; otherwise, .
public static bool ApplyChainedClassSequenceRule(
SkippingGlyphIterator iterator,
ChainedClassSequenceRuleTable rule,
ClassDefinitionTable inputClassDefinitionTable,
ClassDefinitionTable backtrackClassDefinitionTable,
ClassDefinitionTable lookaheadClassDefinitionTable)
{
if (rule.BacktrackSequence.Length > 0
&& !MatchClassSequence(iterator, -rule.BacktrackSequence.Length, rule.BacktrackSequence, backtrackClassDefinitionTable))
{
return false;
}
if (rule.InputSequence.Length > 0 &&
!MatchClassSequence(iterator, 1, rule.InputSequence, inputClassDefinitionTable))
{
return false;
}
if (rule.LookaheadSequence.Length > 0
&& !MatchClassSequence(iterator, 1 + rule.InputSequence.Length, rule.LookaheadSequence, lookaheadClassDefinitionTable))
{
return false;
}
return true;
}
///
/// Checks all coverage tables (backtrack, input, and lookahead) for a chained context Format 3 match.
///
/// The font metrics.
/// The lookup flags for glyph filtering.
/// The mark filtering set index.
/// The glyph shaping collection.
/// The starting index of the input sequence.
/// The number of glyphs available from the starting index.
/// The array of input coverage tables.
/// The array of backtrack coverage tables.
/// The array of lookahead coverage tables.
/// if all coverages matched; otherwise, .
public static bool CheckAllCoverages(
FontMetrics fontMetrics,
LookupFlags lookupFlags,
ushort markFilteringSet,
IGlyphShapingCollection collection,
int index,
int count,
CoverageTable[] input,
CoverageTable[] backtrack,
CoverageTable[] lookahead)
{
int endExclusive = index + count;
SkippingGlyphIterator iterator = new(fontMetrics, collection, index, lookupFlags, markFilteringSet);
// Compute backtrack start using skippy prev(), not index-1.
int backtrackStart = index;
if (backtrack.Length > 0)
{
SkippingGlyphIterator backIt = iterator;
backIt.Index = index;
backtrackStart = backIt.Prev(); // first backtrack glyph (i-1 in skippy space)
}
if (!MatchBacktrackCoverageSequence(iterator, backtrack, backtrackStart, endExclusive))
{
return false;
}
// Input starts at the current glyph position.
if (!MatchCoverageSequence(iterator, input, index, endExclusive))
{
return false;
}
// Compute lookahead start by advancing through the input sequence using skippy Next(),
// not by raw index arithmetic.
int lookaheadStart = index;
if (lookahead.Length > 0)
{
SkippingGlyphIterator fwdIt = iterator;
fwdIt.Index = index;
fwdIt.Increment(input.Length); // advance input.Length steps in skippy space
lookaheadStart = fwdIt.Index;
}
if (!MatchCoverageSequence(iterator, lookahead, lookaheadStart, endExclusive))
{
return false;
}
return true;
}
///
/// Applies anchor-based positioning for mark-to-base, mark-to-ligature, or mark-to-mark attachment.
///
/// The font metrics.
/// The glyph positioning collection.
/// The index of the mark glyph in the collection.
/// The anchor table for the base glyph, or if no anchor is defined.
/// The mark record containing the mark anchor table and class.
/// The index of the base glyph in the collection.
/// The feature tag being applied.
public static void ApplyAnchor(
FontMetrics fontMetrics,
GlyphPositioningCollection collection,
int index,
AnchorTable? baseAnchor,
MarkRecord markRecord,
int baseGlyphIndex,
Tag feature)
{
// baseAnchor may be null because OpenType MarkToBase allows NULL anchor offsets
// in BaseArray/BaseRecord. A NULL offset means "this base glyph has no anchor
// for this mark class", and the lookup must be ignored for this markābase pair.
if (baseAnchor is null)
{
return;
}
GlyphShapingData baseData = collection[baseGlyphIndex];
AnchorXY baseXY = baseAnchor.GetAnchor(fontMetrics, baseData, collection);
GlyphShapingData markData = collection[index];
AnchorXY markXY = markRecord.MarkAnchorTable.GetAnchor(fontMetrics, markData, collection);
markData.Bounds.X = baseXY.XCoordinate - markXY.XCoordinate;
markData.Bounds.Y = baseXY.YCoordinate - markXY.YCoordinate;
markData.MarkAttachment = baseGlyphIndex;
markData.AppliedFeatures.Add(feature);
}
///
/// Applies a value record's positioning adjustments to a glyph in the collection.
///
/// The font metrics.
/// The glyph positioning collection.
/// The index of the glyph in the collection.
/// The value record containing positioning adjustments.
/// The feature tag being applied.
public static void ApplyPosition(
FontMetrics fontMetrics,
GlyphPositioningCollection collection,
int index,
ValueRecord record,
Tag feature)
{
GlyphShapingData current = collection[index];
current.Bounds.Width += record.XAdvance;
current.Bounds.Height += record.YAdvance;
current.Bounds.X += record.XPlacement;
current.Bounds.Y += record.YPlacement;
// Apply variation deltas from VariationIndex tables (variable fonts).
if (record.HasVariation)
{
current.Bounds.X += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.XPlacementVariation));
current.Bounds.Y += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.YPlacementVariation));
current.Bounds.Width += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.XAdvanceVariation));
current.Bounds.Height += (short)MathF.Round(fontMetrics.GetGDefVariationDelta(record.YAdvanceVariation));
}
current.AppliedFeatures.Add(feature);
}
///
/// Determines whether the specified glyph is a mark glyph based on GDEF class or Unicode properties.
///
/// The font metrics.
/// The glyph identifier.
/// The glyph shaping data.
/// if the glyph is a mark; otherwise, .
public static bool IsMarkGlyph(FontMetrics fontMetrics, ushort glyphId, GlyphShapingData shapingData)
{
if (!fontMetrics.TryGetGlyphClass(glyphId, out GlyphClassDef? glyphClass) &&
!CodePoint.IsMark(shapingData.CodePoint))
{
return false;
}
if (glyphClass != GlyphClassDef.MarkGlyph)
{
return false;
}
return true;
}
///
/// Gets the glyph shaping class (mark, base, ligature, mark attachment type) for the specified glyph,
/// using GDEF table data if available or falling back to Unicode properties.
/// Results are cached on the instance.
///
/// The font metrics.
/// The glyph identifier.
/// The glyph shaping data, used for caching and Unicode fallback.
/// The .
public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, ushort glyphId, GlyphShapingData shapingData)
{
// Cache the shaping class on the GlyphShapingData to avoid repeated GDEF lookups.
// The cache key stores the glyph id; -1 means "not cached".
if (shapingData.ShapingClassCacheKey == glyphId)
{
return shapingData.CachedShapingClass;
}
bool isMark;
bool isBase;
bool isLigature;
ushort markAttachmentType = 0;
if (fontMetrics.TryGetGlyphClass(glyphId, out GlyphClassDef? glyphClass))
{
isMark = glyphClass == GlyphClassDef.MarkGlyph;
isBase = glyphClass == GlyphClassDef.BaseGlyph;
isLigature = glyphClass == GlyphClassDef.LigatureGlyph;
if (fontMetrics.TryGetMarkAttachmentClass(glyphId, out GlyphClassDef? markAttachmentClass))
{
markAttachmentType = (ushort)markAttachmentClass;
}
}
else
{
// TODO: We may have to store each codepoint. FontKit checks all.
isMark = CodePoint.IsMark(shapingData.CodePoint);
isBase = !isMark;
isLigature = shapingData.CodePointCount > 1;
}
GlyphShapingClass result = new(isMark, isBase, isLigature, markAttachmentType);
shapingData.CachedShapingClass = result;
shapingData.ShapingClassCacheKey = glyphId;
return result;
}
///
/// Determines whether the specified glyph is in the given mark filtering set.
///
/// The font metrics.
/// The mark filtering set index.
/// The glyph identifier.
/// if the glyph is in the mark filtering set; otherwise, .
public static bool IsInMarkFilteringSet(FontMetrics fontMetrics, ushort markFilteringSet, ushort glyphId)
=> fontMetrics.IsInMarkFilteringSet(markFilteringSet, glyphId);
///
/// Matches a sequence of elements against glyphs using an increment-based approach.
///
/// The type of sequence elements to match.
/// The initial increment from the iterator's current position.
/// The array of elements to match.
/// The skipping glyph iterator.
/// The condition function to test each element against glyph data.
/// A span to store matched glyph indices, or default if not needed.
/// if all elements in the sequence were matched; otherwise, .
private static bool Match(
int increment,
T[] sequence,
SkippingGlyphIterator iterator,
Func condition,
Span matches)
{
int position = iterator.Index;
int offset = iterator.Increment(increment);
IGlyphShapingCollection collection = iterator.Collection;
if (offset < 0)
{
return false;
}
int i = 0;
while (i < sequence.Length && i < MaxContextLength && offset < collection.Count)
{
if (!condition(sequence[i], collection[offset]))
{
break;
}
if (matches.Length == MaxContextLength)
{
matches[i] = iterator.Index;
}
i++;
offset = iterator.Next();
}
iterator.Index = position;
return i == sequence.Length;
}
///
/// Matches a sequence of elements against glyphs using a directional (forward/backward) approach.
///
/// The type of sequence elements to match.
/// The skipping glyph iterator.
/// The starting index in the collection.
/// The array of elements to match.
/// The direction to iterate (forward or backward).
/// The exclusive end index in the collection.
/// The condition function to test each element against glyph data.
/// A span to store matched glyph indices, or default if not needed.
/// if all elements in the sequence were matched; otherwise, .
private static bool Match(
SkippingGlyphIterator iterator,
int startIndex,
T[] sequence,
MatchDirection direction,
int endExclusive,
Func condition,
Span matches)
{
if (sequence.Length == 0)
{
return true;
}
int saved = iterator.Index;
iterator.Index = startIndex;
IGlyphShapingCollection collection = iterator.Collection;
int limit = Math.Min(endExclusive, collection.Count);
for (int i = 0; i < sequence.Length && i < MaxContextLength; i++)
{
if (iterator.Index < 0 || iterator.Index >= limit)
{
iterator.Index = saved;
return false;
}
GlyphShapingData data = collection[iterator.Index];
if (!condition(sequence[i], data))
{
iterator.Index = saved;
return false;
}
if (matches.Length == MaxContextLength)
{
matches[i] = iterator.Index;
}
if (i + 1 < sequence.Length)
{
iterator.Index = direction == MatchDirection.Forward
? iterator.Next()
: iterator.Prev();
}
}
iterator.Index = saved;
return true;
}
}
}