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