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

213 lines
9.5 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.Fonts.Unicode;
namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers {
/// <summary>
/// This is a shaper for Arabic, and other cursive scripts.
/// The shaping state machine was ported from fontkit.
/// <see href="https://github.com/foliojs/fontkit/blob/master/src/opentype/shapers/ArabicShaper.js"/>
/// </summary>
internal sealed class ArabicShaper : DefaultShaper
{
/// <summary>The 'mset' (mark positioning via substitution) feature tag.</summary>
private static readonly Tag MsetTag = Tag.Parse("mset");
/// <summary>The 'fina' (terminal forms) feature tag.</summary>
private static readonly Tag FinaTag = Tag.Parse("fina");
/// <summary>The 'fin2' (terminal forms #2) feature tag.</summary>
private static readonly Tag Fin2Tag = Tag.Parse("fin2");
/// <summary>The 'fin3' (terminal forms #3) feature tag.</summary>
private static readonly Tag Fin3Tag = Tag.Parse("fin3");
/// <summary>The 'isol' (isolated forms) feature tag.</summary>
private static readonly Tag IsolTag = Tag.Parse("isol");
/// <summary>The 'init' (initial forms) feature tag.</summary>
private static readonly Tag InitTag = Tag.Parse("init");
/// <summary>The 'medi' (medial forms) feature tag.</summary>
private static readonly Tag MediTag = Tag.Parse("medi");
/// <summary>The 'med2' (medial forms #2) feature tag.</summary>
private static readonly Tag Med2Tag = Tag.Parse("med2");
/// <summary>No joining action.</summary>
private const byte None = 0;
/// <summary>Isolated form action.</summary>
private const byte Isol = 1;
/// <summary>Final form action.</summary>
private const byte Fina = 2;
/// <summary>Final form #2 action (for ALAPH).</summary>
private const byte Fin2 = 3;
/// <summary>Final form #3 action (for ALAPH after DALATH RISH).</summary>
private const byte Fin3 = 4;
/// <summary>Medial form action.</summary>
private const byte Medi = 5;
/// <summary>Medial form #2 action (for ALAPH).</summary>
private const byte Med2 = 6;
/// <summary>Initial form action.</summary>
private const byte Init = 7;
/// <summary>
/// Arabic joining state machine table. Each entry is [prevAction, curAction, nextState].
/// Rows are states (0-6), columns are joining type categories.
/// </summary>
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 } },
};
/// <summary>
/// Initializes a new instance of the <see cref="ArabicShaper"/> class.
/// </summary>
/// <param name="script">The script classification.</param>
/// <param name="textOptions">The text options.</param>
public ArabicShaper(ScriptClass script, TextOptions textOptions)
: base(script, MarkZeroingMode.PostGpos, textOptions)
{
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
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;
}
}
}
/// <summary>
/// Maps an Arabic joining type to the corresponding column index in the state table.
/// </summary>
/// <param name="joiningType">The Arabic joining type.</param>
/// <returns>The state table column index.</returns>
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,
};
}
}