// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.Fonts.Unicode; using SixLabors.Fonts.Unicode.Resources; using System; using System.Linq; using UnicodeTrieGenerator.StateAutomation; namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { /// /// This shaper is an implementation of the Universal Shaping Engine, which /// uses Unicode data to shape a number of scripts without a dedicated shaping engine. /// . /// internal sealed class UniversalShaper : DefaultShaper { /// The state machine for Universal Shaping Engine syllable identification. private static readonly StateMachine StateMachine = new(UniversalShapingData.StateTable, UniversalShapingData.AcceptingStates, UniversalShapingData.Tags); /// The 'rphf' (reph forms) feature tag. private static readonly Tag RphfTag = Tag.Parse("rphf"); /// The 'nukt' (nukta forms) feature tag. private static readonly Tag NuktTag = Tag.Parse("nukt"); /// The 'akhn' (akhands) feature tag. private static readonly Tag AkhnTag = Tag.Parse("akhn"); /// The 'pref' (pre-base forms) feature tag. private static readonly Tag PrefTag = Tag.Parse("pref"); /// The 'rkrf' (rakar forms) feature tag. private static readonly Tag RkrfTag = Tag.Parse("rkrf"); /// The 'abvf' (above-base forms) feature tag. private static readonly Tag AbvfTag = Tag.Parse("abvf"); /// The 'blwf' (below-base forms) feature tag. private static readonly Tag BlwfTag = Tag.Parse("blwf"); /// The 'half' (half forms) feature tag. private static readonly Tag HalfTag = Tag.Parse("half"); /// The 'pstf' (post-base forms) feature tag. private static readonly Tag PstfTag = Tag.Parse("pstf"); /// The 'vatu' (vattu variants) feature tag. private static readonly Tag VatuTag = Tag.Parse("vatu"); /// The 'cjct' (conjunct forms) feature tag. private static readonly Tag CjctTag = Tag.Parse("cjct"); /// The 'abvs' (above-base substitutions) feature tag. private static readonly Tag AbvsTag = Tag.Parse("abvs"); /// The 'blws' (below-base substitutions) feature tag. private static readonly Tag BlwsTag = Tag.Parse("blws"); /// The 'pres' (pre-base substitutions) feature tag. private static readonly Tag PresTag = Tag.Parse("pres"); /// The 'psts' (post-base substitutions) feature tag. private static readonly Tag PstsTag = Tag.Parse("psts"); /// The 'dist' (distances) feature tag. private static readonly Tag DistTag = Tag.Parse("dist"); /// The 'abvm' (above-base mark positioning) feature tag. private static readonly Tag AbvmTag = Tag.Parse("abvm"); /// The 'blwm' (below-base mark positioning) feature tag. private static readonly Tag BlwmTag = Tag.Parse("blwm"); /// Dotted circle code point (U+25CC) used as a placeholder base. private const int DottedCircle = 0x25cc; /// The font metrics used for glyph lookups. private readonly FontMetrics fontMetrics; /// Whether any broken clusters were detected during syllable setup. private bool hasBrokenClusters; /// /// Initializes a new instance of the class. /// /// The script classification. /// The text options. /// The font metrics for glyph lookups. public UniversalShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics) : base(script, MarkZeroingMode.PreGPos, textOptions) => this.fontMetrics = fontMetrics; /// protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) { // Default glyph pre-processing group this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables); this.AddFeature(collection, index, count, CcmpTag); this.AddFeature(collection, index, count, NuktTag); this.AddFeature(collection, index, count, AkhnTag); // Reordering group this.AddFeature(collection, index, count, RphfTag, true, ClearSubstitutionFlags, RecordRhpf); this.AddFeature(collection, index, count, PrefTag, true, ClearSubstitutionFlags, RecordPref); // Orthographic unit shaping group this.AddFeature(collection, index, count, RkrfTag); this.AddFeature(collection, index, count, AbvfTag); this.AddFeature(collection, index, count, BlwfTag); this.AddFeature(collection, index, count, HalfTag); this.AddFeature(collection, index, count, PstfTag); this.AddFeature(collection, index, count, VatuTag); this.AddFeature(collection, index, count, CjctTag, postAction: this.Reorder); // Standard topographic presentation and positional feature application this.AddFeature(collection, index, count, AbvsTag); this.AddFeature(collection, index, count, BlwsTag); this.AddFeature(collection, index, count, PresTag); this.AddFeature(collection, index, count, PstsTag); this.AddFeature(collection, index, count, DistTag); this.AddFeature(collection, index, count, AbvmTag); this.AddFeature(collection, index, count, BlwmTag); } /// protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) => this.DecomposeSplitVowels(collection, index, count); /// /// Decomposes split vowels into their constituent parts if supported by the font. /// /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. private void DecomposeSplitVowels(IGlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { return; } FontMetrics fontMetrics = this.fontMetrics; Span buffer = stackalloc ushort[16]; int end = index + count; for (int i = end - 1; i >= index; i--) { GlyphShapingData data = substitutionCollection[i]; if (UniversalShapingData.Decompositions.TryGetValue(data.CodePoint.Value, out int[]? decompositions) && decompositions != null) { Span ids = buffer[..decompositions.Length]; bool shouldDecompose = true; for (int j = 0; j < decompositions.Length; j++) { if (!fontMetrics.TryGetGlyphId(new CodePoint(decompositions[j]), out ushort id)) { shouldDecompose = false; break; } ids[j] = id; } if (shouldDecompose) { substitutionCollection.Replace(i, ids, KnownFeatureTags.GlyphCompositionDecomposition); for (int j = 0; j < decompositions.Length; j++) { substitutionCollection[i + j].CodePoint = new(decompositions[j]); } } } } } /// /// Identifies syllables using the Universal Shaping Engine state machine and assigns shaping info to each glyph. /// /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. private void SetupSyllables(IGlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { return; } this.hasBrokenClusters = false; Span values = count <= 64 ? stackalloc int[count] : new int[count]; for (int i = index; i < index + count; i++) { CodePoint codePoint = substitutionCollection[i].CodePoint; values[i - index] = UnicodeData.GetUniversalShapingSymbolCount((uint)codePoint.Value); } int syllable = 0; foreach (StateMatch match in StateMachine.Match(values)) { ++syllable; // Create shaper info for (int i = match.StartIndex; i <= match.EndIndex; i++) { GlyphShapingData data = substitutionCollection[i + index]; CodePoint codePoint = data.CodePoint; string category = UniversalShapingData.Categories[UnicodeData.GetUniversalShapingSymbolCount((uint)codePoint.Value)]; string syllableType = match.Tags[0]; if (syllableType == "broken_cluster") { this.hasBrokenClusters = true; } data.UniversalShapingEngineInfo = new(category, syllableType, syllable); } // Assign rphf feature int limit = substitutionCollection[match.StartIndex + index].UniversalShapingEngineInfo!.Category == "R" ? 1 : Math.Min(3, match.EndIndex - match.StartIndex); for (int i = match.StartIndex; i < match.StartIndex + limit; i++) { substitutionCollection.AddShapingFeature(i + index, new TagEntry(RcltTag, true)); } } } /// /// Clears substitution flags on all glyphs in the range, preparing for the next substitution pass. /// /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. private static void ClearSubstitutionFlags(IGlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { return; } int end = index + count; for (int i = index; i < end; i++) { GlyphShapingData data = substitutionCollection[i]; data.IsSubstituted = false; } } /// /// Records glyphs substituted by the 'rphf' feature by marking their category as repha ("R"). /// /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. private static void RecordRhpf(IGlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { return; } int end = index + count; for (int i = index; i < end; i++) { GlyphShapingData data = substitutionCollection[i]; if (data.IsSubstituted && data.Features.Any(x => x.Tag == RphfTag)) { // Mark a substituted repha. if (data.UniversalShapingEngineInfo != null) { data.UniversalShapingEngineInfo.Category = "R"; } } } } /// /// Records glyphs substituted by the 'pref' feature by marking their category as pre-base vowel ("VPre"). /// /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. private static void RecordPref(IGlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { return; } int end = index + count; for (int i = index; i < end; i++) { GlyphShapingData data = substitutionCollection[i]; if (data.IsSubstituted) { // Mark a substituted pref as VPre, as they behave the same way. if (data.UniversalShapingEngineInfo != null) { data.UniversalShapingEngineInfo.Category = "VPre"; } } } } /// /// Reorders glyphs within syllables, handling repha movement, pre-base vowel movement, /// and dotted circle insertion for broken clusters. /// /// The glyph shaping collection. /// The zero-based start index. /// The number of elements to process. private void Reorder(IGlyphShapingCollection collection, int index, int count) { if (collection is not GlyphSubstitutionCollection substitutionCollection) { return; } FontMetrics fontMetrics = this.fontMetrics; int max = index + count; int start = index; int end = NextSyllable(substitutionCollection, index, max); if (this.hasBrokenClusters) { if (fontMetrics.TryGetGlyphId(new(DottedCircle), out ushort circleId)) { Span glyphs = stackalloc ushort[2]; while (start < max) { GlyphShapingData data = substitutionCollection[start]; UniversalShapingEngineInfo? info = data.UniversalShapingEngineInfo; string? type = info?.SyllableType; if (type == "broken_cluster") { // Insert after possible Repha. int i = start; for (i = start; i < end; i++) { if (substitutionCollection[i].UniversalShapingEngineInfo?.Category != "R") { break; } } GlyphShapingData current = substitutionCollection[i]; UniversalShapingEngineInfo currentInfo = current.UniversalShapingEngineInfo!; glyphs[0] = current.GlyphId; glyphs[1] = circleId; substitutionCollection.Replace(i, glyphs, KnownFeatureTags.GlyphCompositionDecomposition); // Update shaping info for newly inserted data. GlyphShapingData dotted = substitutionCollection[i + 1]; dotted.UniversalShapingEngineInfo!.Category = "B"; dotted.UniversalShapingEngineInfo.SyllableType = currentInfo.SyllableType; dotted.UniversalShapingEngineInfo.Syllable = currentInfo.Syllable; end++; max++; } start = end; end = NextSyllable(substitutionCollection, start, max); } start = index; end = NextSyllable(substitutionCollection, index, max); } } while (start < max) { GlyphShapingData data = substitutionCollection[start]; UniversalShapingEngineInfo? info = data.UniversalShapingEngineInfo; string? type = info?.SyllableType; // Only a few syllable types need reordering. if (type is not "virama_terminated_cluster" and not "standard_cluster" and not "broken_cluster") { // TODO: Check this. Harfbuzz seems to test more categories and returns. goto Increment; } // Move things forward if (info?.Category == "R" && end - start > 1) { // Got a repha. Reorder it to after first base, before first halant. for (int i = start + 1; i < end; i++) { GlyphShapingData current = substitutionCollection[i]; info = current.UniversalShapingEngineInfo; if (IsBase(info) || IsHalant(current)) { // If we hit a halant, move before it; otherwise it's a base: move to it's // place, and shift things in between backward. if (IsHalant(current)) { i--; } substitutionCollection.MoveGlyph(start, i); break; } } } // Move things back for (int i = start, j = start; i < end; i++) { GlyphShapingData current = substitutionCollection[i]; info = current.UniversalShapingEngineInfo; if (IsBase(info) || IsHalant(current)) { // If we hit a halant, move after it; otherwise move to the beginning, and // shift things in between forward. if (IsHalant(current)) { j = i + 1; } else { j = i; } } else if ((info?.Category == "VPre" || info?.Category == "VMPre") && current.LigatureComponent <= 0 // Only move the first component of a MultipleSubst && j < i) { substitutionCollection.MoveGlyph(i, j); } } Increment: start = end; end = NextSyllable(substitutionCollection, start, max); } } /// /// Finds the start index of the next syllable in the collection. /// /// The glyph substitution collection. /// The current index. /// The maximum index bound. /// The start index of the next syllable. private static int NextSyllable(GlyphSubstitutionCollection collection, int index, int count) { if (index >= count) { return index; } int? syllable = collection[index].UniversalShapingEngineInfo?.Syllable; while (++index < count) { if (collection[index].UniversalShapingEngineInfo?.Syllable != syllable) { break; } } return index; } /// /// Determines whether the glyph is a halant, halant-like, or invisible stacker character. /// /// The glyph shaping data. /// if the glyph is a halant or equivalent. private static bool IsHalant(GlyphShapingData data) => (data.UniversalShapingEngineInfo?.Category is "H" or "HVM" or "IS") && !data.IsLigated; /// /// Determines whether the shaping info represents a base consonant or generic base. /// /// The universal shaping engine info. /// if the glyph is a base. private static bool IsBase(UniversalShapingEngineInfo? info) => info?.Category is "B" or "GB"; } }