// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { /// /// This is a shaper for Arabic, and other cursive scripts. /// The shaping state machine was ported from fontkit. /// /// internal sealed class ArabicShaper : DefaultShaper { /// The 'mset' (mark positioning via substitution) feature tag. private static readonly Tag MsetTag = Tag.Parse("mset"); /// The 'fina' (terminal forms) feature tag. private static readonly Tag FinaTag = Tag.Parse("fina"); /// The 'fin2' (terminal forms #2) feature tag. private static readonly Tag Fin2Tag = Tag.Parse("fin2"); /// The 'fin3' (terminal forms #3) feature tag. private static readonly Tag Fin3Tag = Tag.Parse("fin3"); /// The 'isol' (isolated forms) feature tag. private static readonly Tag IsolTag = Tag.Parse("isol"); /// The 'init' (initial forms) feature tag. private static readonly Tag InitTag = Tag.Parse("init"); /// The 'medi' (medial forms) feature tag. private static readonly Tag MediTag = Tag.Parse("medi"); /// The 'med2' (medial forms #2) feature tag. private static readonly Tag Med2Tag = Tag.Parse("med2"); /// No joining action. private const byte None = 0; /// Isolated form action. private const byte Isol = 1; /// Final form action. private const byte Fina = 2; /// Final form #2 action (for ALAPH). private const byte Fin2 = 3; /// Final form #3 action (for ALAPH after DALATH RISH). private const byte Fin3 = 4; /// Medial form action. private const byte Medi = 5; /// Medial form #2 action (for ALAPH). private const byte Med2 = 6; /// Initial form action. private const byte Init = 7; /// /// Arabic joining state machine table. Each entry is [prevAction, curAction, nextState]. /// Rows are states (0-6), columns are joining type categories. /// private static readonly byte[,][] StateTable = { // # NonJoining, LeftJoining, RightJoining, DualJoining, ALAPH, DALATH RISH // State 0: prev was U, not willing to join. { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 6 } }, // State 1: prev was R or ISOL/ALAPH, not willing to join. { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 2 }, new byte[] { None, Fin2, 5 }, new byte[] { None, Isol, 6 } }, // State 2: prev was D/L in ISOL form, willing to join. { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Init, Fina, 1 }, new byte[] { Init, Fina, 3 }, new byte[] { Init, Fina, 4 }, new byte[] { Init, Fina, 6 } }, // State 3: prev was D in FINA form, willing to join. { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Medi, Fina, 1 }, new byte[] { Medi, Fina, 3 }, new byte[] { Medi, Fina, 4 }, new byte[] { Medi, Fina, 6 } }, // State 4: prev was FINA ALAPH, not willing to join. { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Med2, Isol, 1 }, new byte[] { Med2, Isol, 2 }, new byte[] { Med2, Fin2, 5 }, new byte[] { Med2, Isol, 6 } }, // State 5: prev was FIN2/FIN3 ALAPH, not willing to join. { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { Isol, Isol, 1 }, new byte[] { Isol, Isol, 2 }, new byte[] { Isol, Fin2, 5 }, new byte[] { Isol, Isol, 6 } }, // State 6: prev was DALATH/RISH, not willing to join. { new byte[] { None, None, 0 }, new byte[] { None, Isol, 2 }, new byte[] { None, Isol, 1 }, new byte[] { None, Isol, 2 }, new byte[] { None, Fin3, 5 }, new byte[] { None, Isol, 6 } }, }; /// /// Initializes a new instance of the class. /// /// The script classification. /// The text options. public ArabicShaper(ScriptClass script, TextOptions textOptions) : base(script, MarkZeroingMode.PostGpos, textOptions) { } /// protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count) { this.AddFeature(collection, index, count, CcmpTag); this.AddFeature(collection, index, count, LoclTag); this.AddFeature(collection, index, count, IsolTag, false); this.AddFeature(collection, index, count, FinaTag, false); this.AddFeature(collection, index, count, Fin2Tag, false); this.AddFeature(collection, index, count, Fin3Tag, false); this.AddFeature(collection, index, count, MediTag, false); this.AddFeature(collection, index, count, Med2Tag, false); this.AddFeature(collection, index, count, InitTag, false); // HarfBuzz plans these as Arabic-script features, independently of the // generic horizontal feature list. Horizontal runs already get them from // DefaultShaper; forced vertical Arabic needs them here as well. if (collection.TextOptions.LayoutMode.IsVertical()) { this.AddFeature(collection, index, count, CaltTag); this.AddFeature(collection, index, count, LigaTag); this.AddFeature(collection, index, count, CligTag); } this.AddFeature(collection, index, count, MsetTag); } /// protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) { base.AssignFeatures(collection, index, count); int prev = -1; int state = 0; byte[] actions = new byte[count]; // Apply the state machine to map glyphs to features. for (int i = 0; i < count; i++) { GlyphShapingData data = collection[i + index]; ArabicJoiningClass joiningClass = CodePoint.GetArabicJoiningClass(data.CodePoint); ArabicJoiningType joiningType = joiningClass.JoiningType; if (joiningType == ArabicJoiningType.Transparent) { actions[i] = None; continue; } int shapingClassIndex = GetShapingClassIndex(joiningType); byte[] actionsWithState = StateTable[state, shapingClassIndex]; byte prevAction = actionsWithState[0]; byte curAction = actionsWithState[1]; state = actionsWithState[2]; if (prevAction != None && prev != -1) { actions[prev] = prevAction; } actions[i] = curAction; prev = i; } // Apply the chosen features to their respective glyphs. for (int i = 0; i < actions.Length; i++) { switch (actions[i]) { case Fina: collection.EnableShapingFeature(i + index, FinaTag); break; case Fin2: collection.EnableShapingFeature(i + index, Fin2Tag); break; case Fin3: collection.EnableShapingFeature(i + index, Fin3Tag); break; case Isol: collection.EnableShapingFeature(i + index, IsolTag); break; case Init: collection.EnableShapingFeature(i + index, InitTag); break; case Medi: collection.EnableShapingFeature(i + index, MediTag); break; case Med2: collection.EnableShapingFeature(i + index, Med2Tag); break; } } } /// /// Maps an Arabic joining type to the corresponding column index in the state table. /// /// The Arabic joining type. /// The state table column index. private static int GetShapingClassIndex(ArabicJoiningType joiningType) => joiningType switch { ArabicJoiningType.NonJoining => 0, ArabicJoiningType.LeftJoining => 1, ArabicJoiningType.RightJoining => 2, ArabicJoiningType.DualJoining or ArabicJoiningType.JoinCausing => 3, // TODO: ALAPH: 4 // TODO: DALATH RISH': 5 ArabicJoiningType.Transparent => 6, _ => 0, }; } }