ImageSharp/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs
2026-08-03 22:31:27 +02:00

701 lines
31 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 {
/// <summary>
/// Provides shared utility methods for advanced typographic layout processing in GPOS and GSUB tables.
/// </summary>
internal static class AdvancedTypographicUtils
{
/// <summary>
/// The maximum context length for sequence matching operations.
/// Used to prevent excessive processing from maliciously crafted fonts.
/// Based on HarfBuzz hb-buffer.hh.
/// </summary>
public const int MaxContextLength = 64;
/// <summary>
/// The maximum length factor multiplied by collection count to compute max allowable collection size.
/// </summary>
private const int MaxLengthFactor = 64;
/// <summary>
/// The minimum value for the max allowable collection size.
/// </summary>
private const int MaxLengthMinimum = 16384;
/// <summary>
/// The maximum operations factor multiplied by collection count to compute max allowable operations.
/// </summary>
private const int MaxOperationsFactor = 1024;
/// <summary>
/// The minimum value for the max allowable operations count.
/// </summary>
private const int MaxOperationsMinimum = 16384;
/// <summary>
/// The absolute maximum number of shaping characters, set to half of int.MaxValue.
/// </summary>
private const int MaxShapingCharsLength = 0x3FFFFFFF; // Half int max.
/// <summary>
/// Defines the direction for sequence matching operations.
/// </summary>
internal enum MatchDirection
{
/// <summary>
/// Match in the forward direction.
/// </summary>
Forward,
/// <summary>
/// Match in the backward direction.
/// </summary>
Backward
}
/// <summary>
/// Gets a value indicating whether the glyph represented by the codepoint should be interpreted vertically.
/// </summary>
/// <param name="codePoint">The codepoint represented by the glyph.</param>
/// <param name="layoutMode">The layout mode.</param>
/// <returns>The <see cref="bool"/>.</returns>
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;
}
/// <summary>
/// Gets the maximum allowable shaping collection count for the given input length.
/// </summary>
/// <param name="length">The input collection length.</param>
/// <returns>The maximum allowable count.</returns>
public static int GetMaxAllowableShapingCollectionCount(int length)
=> (int)Math.Min(Math.Max((long)length * MaxLengthFactor, MaxLengthMinimum), MaxShapingCharsLength);
/// <summary>
/// Gets the maximum allowable shaping operations count for the given input length.
/// </summary>
/// <param name="length">The input collection length.</param>
/// <returns>The maximum allowable operations count.</returns>
public static int GetMaxAllowableShapingOperationsCount(int length)
=> (int)Math.Min(Math.Max((long)length * MaxOperationsFactor, MaxOperationsMinimum), MaxShapingCharsLength);
/// <summary>
/// Applies nested lookups from sequence lookup records for GSUB contextual/chaining lookups.
/// </summary>
/// <param name="fontMetrics">The font metrics.</param>
/// <param name="table">The GSUB table.</param>
/// <param name="feature">The feature tag being applied.</param>
/// <param name="lookupFlags">The lookup flags for glyph filtering.</param>
/// <param name="markFilteringSet">The mark filtering set index.</param>
/// <param name="records">The sequence lookup records specifying which lookups to apply at which positions.</param>
/// <param name="collection">The glyph substitution collection.</param>
/// <param name="index">The starting index in the collection.</param>
/// <param name="count">The number of glyphs in the input sequence.</param>
/// <returns><see langword="true"/> if the lookups were applied.</returns>
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;
}
/// <summary>
/// Applies nested lookups from sequence lookup records for GPOS contextual/chaining lookups.
/// </summary>
/// <param name="fontMetrics">The font metrics.</param>
/// <param name="table">The GPOS table.</param>
/// <param name="feature">The feature tag being applied.</param>
/// <param name="lookupFlags">The lookup flags for glyph filtering.</param>
/// <param name="markFilteringSet">The mark filtering set index.</param>
/// <param name="records">The sequence lookup records specifying which lookups to apply at which positions.</param>
/// <param name="collection">The glyph positioning collection.</param>
/// <param name="index">The starting index in the collection.</param>
/// <param name="count">The number of glyphs in the input sequence.</param>
/// <returns><see langword="true"/> if the lookups were applied.</returns>
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;
}
/// <summary>
/// Matches an input glyph sequence by glyph ID, verifying that each glyph has the specified feature enabled.
/// </summary>
/// <param name="iterator">The skipping glyph iterator.</param>
/// <param name="feature">The feature tag that must be enabled on matched glyphs.</param>
/// <param name="increment">The initial increment from the iterator's current position.</param>
/// <param name="sequence">The array of glyph IDs to match.</param>
/// <param name="matches">A span to store matched glyph indices, or default if not needed.</param>
/// <returns><see langword="true"/> if the entire sequence was matched; otherwise, <see langword="false"/>.</returns>
public static bool MatchInputSequence(SkippingGlyphIterator iterator, Tag feature, ushort increment, ushort[] sequence, Span<int> matches)
=> Match(
increment,
sequence,
iterator,
(component, data) =>
{
if (!ContainsFeatureTag(data.Features, feature))
{
return false;
}
return component == data.GlyphId;
},
matches);
/// <summary>
/// Determines whether the feature list contains the specified feature tag in an enabled state.
/// </summary>
/// <param name="featureList">The list of tag entries to search.</param>
/// <param name="feature">The feature tag to find.</param>
/// <returns><see langword="true"/> if the feature is present and enabled; otherwise, <see langword="false"/>.</returns>
private static bool ContainsFeatureTag(List<TagEntry> featureList, Tag feature)
{
foreach (TagEntry tagEntry in featureList)
{
if (tagEntry.Tag == feature && tagEntry.Enabled)
{
return true;
}
}
return false;
}
/// <summary>
/// Matches a glyph sequence by glyph ID.
/// </summary>
/// <param name="iterator">The skipping glyph iterator.</param>
/// <param name="increment">The initial increment from the iterator's current position.</param>
/// <param name="sequence">The array of glyph IDs to match.</param>
/// <returns><see langword="true"/> if the entire sequence was matched; otherwise, <see langword="false"/>.</returns>
public static bool MatchSequence(SkippingGlyphIterator iterator, int increment, ushort[] sequence)
=> Match(
increment,
sequence,
iterator,
(component, data) => component == data.GlyphId,
default);
/// <summary>
/// Matches a glyph sequence by class values using a class definition table.
/// </summary>
/// <param name="iterator">The skipping glyph iterator.</param>
/// <param name="increment">The initial increment from the iterator's current position.</param>
/// <param name="sequence">The array of class values to match.</param>
/// <param name="classDefinitionTable">The class definition table used to map glyph IDs to class values.</param>
/// <returns><see langword="true"/> if the entire sequence was matched; otherwise, <see langword="false"/>.</returns>
public static bool MatchClassSequence(
SkippingGlyphIterator iterator,
int increment,
ushort[] sequence,
ClassDefinitionTable classDefinitionTable)
=> Match(
increment,
sequence,
iterator,
(component, data) => component == classDefinitionTable.ClassIndexOf(data.GlyphId),
default);
/// <summary>
/// Matches a forward glyph sequence using coverage tables.
/// </summary>
/// <param name="iterator">The skipping glyph iterator.</param>
/// <param name="coverageTable">The array of coverage tables to match against.</param>
/// <param name="startIndex">The starting index in the collection.</param>
/// <param name="endExclusive">The exclusive end index in the collection.</param>
/// <returns><see langword="true"/> if all coverage tables matched; otherwise, <see langword="false"/>.</returns>
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);
/// <summary>
/// Matches a backward (backtrack) glyph sequence using coverage tables.
/// Per the spec, backtrack[0] matches i-1, then i-2, and so on.
/// </summary>
/// <param name="iterator">The skipping glyph iterator.</param>
/// <param name="backtrack">The array of backtrack coverage tables to match against.</param>
/// <param name="startIndex">The starting index in the collection (the first backtrack position).</param>
/// <param name="endExclusive">The exclusive end index in the collection.</param>
/// <returns><see langword="true"/> if all backtrack coverage tables matched; otherwise, <see langword="false"/>.</returns>
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);
/// <summary>
/// Applies a chained sequence rule by matching backtrack, input, and lookahead glyph ID sequences.
/// </summary>
/// <param name="iterator">The skipping glyph iterator.</param>
/// <param name="rule">The chained sequence rule table to apply.</param>
/// <returns><see langword="true"/> if all sequences matched; otherwise, <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Applies a chained class sequence rule by matching backtrack, input, and lookahead class sequences.
/// </summary>
/// <param name="iterator">The skipping glyph iterator.</param>
/// <param name="rule">The chained class sequence rule table to apply.</param>
/// <param name="inputClassDefinitionTable">The class definition table for the input sequence.</param>
/// <param name="backtrackClassDefinitionTable">The class definition table for the backtrack sequence.</param>
/// <param name="lookaheadClassDefinitionTable">The class definition table for the lookahead sequence.</param>
/// <returns><see langword="true"/> if all sequences matched; otherwise, <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Checks all coverage tables (backtrack, input, and lookahead) for a chained context Format 3 match.
/// </summary>
/// <param name="fontMetrics">The font metrics.</param>
/// <param name="lookupFlags">The lookup flags for glyph filtering.</param>
/// <param name="markFilteringSet">The mark filtering set index.</param>
/// <param name="collection">The glyph shaping collection.</param>
/// <param name="index">The starting index of the input sequence.</param>
/// <param name="count">The number of glyphs available from the starting index.</param>
/// <param name="input">The array of input coverage tables.</param>
/// <param name="backtrack">The array of backtrack coverage tables.</param>
/// <param name="lookahead">The array of lookahead coverage tables.</param>
/// <returns><see langword="true"/> if all coverages matched; otherwise, <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Applies anchor-based positioning for mark-to-base, mark-to-ligature, or mark-to-mark attachment.
/// </summary>
/// <param name="fontMetrics">The font metrics.</param>
/// <param name="collection">The glyph positioning collection.</param>
/// <param name="index">The index of the mark glyph in the collection.</param>
/// <param name="baseAnchor">The anchor table for the base glyph, or <see langword="null"/> if no anchor is defined.</param>
/// <param name="markRecord">The mark record containing the mark anchor table and class.</param>
/// <param name="baseGlyphIndex">The index of the base glyph in the collection.</param>
/// <param name="feature">The feature tag being applied.</param>
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 markbase 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);
}
/// <summary>
/// Applies a value record's positioning adjustments to a glyph in the collection.
/// </summary>
/// <param name="fontMetrics">The font metrics.</param>
/// <param name="collection">The glyph positioning collection.</param>
/// <param name="index">The index of the glyph in the collection.</param>
/// <param name="record">The value record containing positioning adjustments.</param>
/// <param name="feature">The feature tag being applied.</param>
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);
}
/// <summary>
/// Determines whether the specified glyph is a mark glyph based on GDEF class or Unicode properties.
/// </summary>
/// <param name="fontMetrics">The font metrics.</param>
/// <param name="glyphId">The glyph identifier.</param>
/// <param name="shapingData">The glyph shaping data.</param>
/// <returns><see langword="true"/> if the glyph is a mark; otherwise, <see langword="false"/>.</returns>
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;
}
/// <summary>
/// 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 <see cref="GlyphShapingData"/> instance.
/// </summary>
/// <param name="fontMetrics">The font metrics.</param>
/// <param name="glyphId">The glyph identifier.</param>
/// <param name="shapingData">The glyph shaping data, used for caching and Unicode fallback.</param>
/// <returns>The <see cref="GlyphShapingClass"/>.</returns>
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;
}
/// <summary>
/// Determines whether the specified glyph is in the given mark filtering set.
/// </summary>
/// <param name="fontMetrics">The font metrics.</param>
/// <param name="markFilteringSet">The mark filtering set index.</param>
/// <param name="glyphId">The glyph identifier.</param>
/// <returns><see langword="true"/> if the glyph is in the mark filtering set; otherwise, <see langword="false"/>.</returns>
public static bool IsInMarkFilteringSet(FontMetrics fontMetrics, ushort markFilteringSet, ushort glyphId)
=> fontMetrics.IsInMarkFilteringSet(markFilteringSet, glyphId);
/// <summary>
/// Matches a sequence of elements against glyphs using an increment-based approach.
/// </summary>
/// <typeparam name="T">The type of sequence elements to match.</typeparam>
/// <param name="increment">The initial increment from the iterator's current position.</param>
/// <param name="sequence">The array of elements to match.</param>
/// <param name="iterator">The skipping glyph iterator.</param>
/// <param name="condition">The condition function to test each element against glyph data.</param>
/// <param name="matches">A span to store matched glyph indices, or default if not needed.</param>
/// <returns><see langword="true"/> if all elements in the sequence were matched; otherwise, <see langword="false"/>.</returns>
private static bool Match<T>(
int increment,
T[] sequence,
SkippingGlyphIterator iterator,
Func<T, GlyphShapingData, bool> condition,
Span<int> 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;
}
/// <summary>
/// Matches a sequence of elements against glyphs using a directional (forward/backward) approach.
/// </summary>
/// <typeparam name="T">The type of sequence elements to match.</typeparam>
/// <param name="iterator">The skipping glyph iterator.</param>
/// <param name="startIndex">The starting index in the collection.</param>
/// <param name="sequence">The array of elements to match.</param>
/// <param name="direction">The direction to iterate (forward or backward).</param>
/// <param name="endExclusive">The exclusive end index in the collection.</param>
/// <param name="condition">The condition function to test each element against glyph data.</param>
/// <param name="matches">A span to store matched glyph indices, or default if not needed.</param>
/// <returns><see langword="true"/> if all elements in the sequence were matched; otherwise, <see langword="false"/>.</returns>
private static bool Match<T>(
SkippingGlyphIterator iterator,
int startIndex,
T[] sequence,
MatchDirection direction,
int endExclusive,
Func<T, GlyphShapingData, bool> condition,
Span<int> 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;
}
}
}