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

508 lines
20 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.Fonts.Unicode;
using System;
using UnicodeTrieGenerator.StateAutomation;
using static SixLabors.Fonts.Unicode.Resources.IndicShapingData;
namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers {
/// <summary>
/// Shaper for the Myanmar script. Handles syllable identification, reordering,
/// and application of Myanmar-specific OpenType features.
/// </summary>
internal sealed class MyanmarShaper : DefaultShaper
{
/// <summary>The state machine for Myanmar syllable identification.</summary>
private static readonly StateMachine StateMachine =
new(
Unicode.Resources.MyanmarShapingData.StateTable,
Unicode.Resources.MyanmarShapingData.AcceptingStates,
Unicode.Resources.MyanmarShapingData.Tags);
/// <summary>Maps Myanmar shaping category codes to compact DFA symbol indices.</summary>
private static readonly int[] CategoryToSymbolId = BuildCategoryToSymbolId();
/// <summary>The 'rphf' (reph forms) feature tag.</summary>
private static readonly Tag RphfTag = Tag.Parse("rphf");
/// <summary>The 'pref' (pre-base forms) feature tag.</summary>
private static readonly Tag PrefTag = Tag.Parse("pref");
/// <summary>The 'blwf' (below-base forms) feature tag.</summary>
private static readonly Tag BlwfTag = Tag.Parse("blwf");
/// <summary>The 'pstf' (post-base forms) feature tag.</summary>
private static readonly Tag PstfTag = Tag.Parse("pstf");
/// <summary>The 'pres' (pre-base substitutions) feature tag.</summary>
private static readonly Tag PresTag = Tag.Parse("pres");
/// <summary>The 'abvs' (above-base substitutions) feature tag.</summary>
private static readonly Tag AbvsTag = Tag.Parse("abvs");
/// <summary>The 'blws' (below-base substitutions) feature tag.</summary>
private static readonly Tag BlwsTag = Tag.Parse("blws");
/// <summary>The 'psts' (post-base substitutions) feature tag.</summary>
private static readonly Tag PstsTag = Tag.Parse("psts");
/// <summary>Dotted circle code point (U+25CC) used as a placeholder base.</summary>
private const int DottedCircle = 0x25cc;
/// <summary>The text options.</summary>
private readonly TextOptions textOptions;
/// <summary>The font metrics used for glyph lookups.</summary>
private readonly FontMetrics fontMetrics;
/// <summary>Whether any broken clusters were detected during syllable setup.</summary>
private bool hasBrokenClusters;
/// <summary>
/// Initializes a new instance of the <see cref="MyanmarShaper"/> class.
/// </summary>
/// <param name="script">The script classification.</param>
/// <param name="textOptions">The text options.</param>
/// <param name="fontMetrics">The font metrics for glyph lookups.</param>
public MyanmarShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics)
: base(script, MarkZeroingMode.PreGPos, textOptions)
{
this.textOptions = textOptions;
this.fontMetrics = fontMetrics;
}
/// <inheritdoc />
protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count)
{
this.AddFeature(collection, index, count, LoclTag, preAction: this.SetupSyllables);
this.AddFeature(collection, index, count, CcmpTag);
this.AddFeature(collection, index, count, RphfTag, preAction: this.InitialReorder);
this.AddFeature(collection, index, count, PrefTag);
this.AddFeature(collection, index, count, BlwfTag);
this.AddFeature(collection, index, count, PstfTag);
this.AddFeature(collection, index, count, PresTag);
this.AddFeature(collection, index, count, AbvsTag);
this.AddFeature(collection, index, count, BlwsTag);
this.AddFeature(collection, index, count, PstsTag);
}
/// <summary>
/// Identifies Myanmar syllables using the state machine and assigns shaping info to each glyph.
/// </summary>
/// <param name="collection">The glyph shaping collection.</param>
/// <param name="index">The zero-based start index.</param>
/// <param name="count">The number of elements to process.</param>
private void SetupSyllables(IGlyphShapingCollection collection, int index, int count)
{
if (collection is not GlyphSubstitutionCollection substitutionCollection)
{
return;
}
this.hasBrokenClusters = false;
Span<int> values = count <= 64 ? stackalloc int[count] : new int[count];
for (int i = index; i < index + count; i++)
{
// Convert HarfBuzz-style Myanmar shaping categories into the compact
// DFA symbol indices used by the generated state machine.
//
// HarfBuzz category codes (C=1, V=2, MR=36, VBlw=21, etc.) are sparse
// and can be larger than the alphabet size of the DFA. Our state
// machine expects its input alphabet to be dense 0..N-1, matching the
// sequential IDs assigned in GenerateMyanmarShapingData.
//
// CategoryToSymbolId[(int)my] performs this mapping, ensuring that
// every codepoint is presented to the DFA using the correct compact
// symbol index.
CodePoint codePoint = substitutionCollection[i].CodePoint;
MyanmarCategories my = (MyanmarCategories)IndicShapingCategory(codePoint);
values[i - index] = CategoryToSymbolId[(int)my];
}
int syllable = 0;
int last = 0;
foreach (StateMatch match in StateMachine.Match(values))
{
if (match.StartIndex > last)
{
++syllable;
for (int i = last; i < match.StartIndex; i++)
{
GlyphShapingData data = substitutionCollection[i + index];
data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable);
}
}
++syllable;
// Create shaper info.
for (int i = match.StartIndex; i <= match.EndIndex; i++)
{
GlyphShapingData data = substitutionCollection[i + index];
CodePoint codePoint = data.CodePoint;
string syllableType = match.Tags[0];
if (syllableType == "broken_cluster")
{
this.hasBrokenClusters = true;
}
data.IndicShapingEngineInfo = new(
(Categories)IndicShapingCategory(codePoint),
(Positions)IndicShapingPosition(codePoint),
syllableType,
syllable);
}
last = match.EndIndex + 1;
}
if (last < count)
{
++syllable;
for (int i = last; i < count; i++)
{
GlyphShapingData data = substitutionCollection[i + index];
data.IndicShapingEngineInfo = new(Categories.X, Positions.End, "non_indic_cluster", syllable);
}
}
}
/// <summary>
/// Performs the initial reordering pass for Myanmar consonant syllables, including
/// dotted circle insertion for broken clusters.
/// </summary>
/// <param name="collection">The glyph shaping collection.</param>
/// <param name="index">The zero-based start index.</param>
/// <param name="count">The number of elements to process.</param>
private void InitialReorder(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<ushort> glyphs = stackalloc ushort[2];
while (start < max)
{
GlyphShapingData data = substitutionCollection[start];
IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo;
string? type = dataInfo?.SyllableType;
if (type == "broken_cluster")
{
// Insert after possible Repha.
int i = start;
for (i = start; i < end; i++)
{
if (substitutionCollection[i].IndicShapingEngineInfo?.Category != Categories.Repha)
{
break;
}
}
GlyphShapingData current = substitutionCollection[i];
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.IndicShapingEngineInfo!.Category = Categories.Dotted_Circle;
end++;
max++;
}
start = end;
end = NextSyllable(substitutionCollection, start, max);
}
start = index;
end = NextSyllable(substitutionCollection, index, max);
}
}
while (start < max)
{
GlyphShapingData data = substitutionCollection[start];
IndicShapingEngineInfo? dataInfo = data.IndicShapingEngineInfo;
string? type = dataInfo?.SyllableType;
switch (type)
{
// We already inserted dotted-circles, so just call the consonant_syllable.
case "broken_cluster":
case "consonant_syllable":
ReorderConsonantSyllable(substitutionCollection, start, end);
break;
default:
break;
}
start = end;
end = NextSyllable(substitutionCollection, start, max);
}
}
/// <summary>
/// Reorders glyphs within a single Myanmar consonant syllable according to the Myanmar shaping spec.
/// </summary>
/// <param name="substitutionCollection">The glyph substitution collection.</param>
/// <param name="start">The start index of the syllable.</param>
/// <param name="end">The exclusive end index of the syllable.</param>
private static void ReorderConsonantSyllable(GlyphSubstitutionCollection substitutionCollection, int start, int end)
{
int basePosition = end;
bool hasReph = false;
{
int limit = start;
if (start + 3 <= end &&
substitutionCollection[start].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.Ra &&
substitutionCollection[start + 1].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.As &&
substitutionCollection[start + 2].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.H)
{
limit += 3;
basePosition = start;
hasReph = true;
}
{
if (!hasReph)
{
basePosition = limit;
}
for (int i = limit; i < end; i++)
{
if (IsConsonant(substitutionCollection[i]))
{
basePosition = i;
break;
}
}
}
}
// Reorder
{
int i = start;
for (; i < start + (hasReph ? 3 : 0); i++)
{
substitutionCollection[i].IndicShapingEngineInfo!.Position = Positions.After_Main;
}
for (; i < basePosition; i++)
{
substitutionCollection[i].IndicShapingEngineInfo!.Position = Positions.Pre_C;
}
if (i < end)
{
substitutionCollection[i].IndicShapingEngineInfo!.Position = Positions.Base_C;
i++;
}
Positions pos = Positions.After_Main;
// The following loop may be ugly, but it implements all of Myanmar reordering!
for (; i < end; i++)
{
GlyphShapingData data = substitutionCollection[i];
IndicShapingEngineInfo info = data.IndicShapingEngineInfo!;
// Pre-base reordering
if (info.MyanmarCategory == MyanmarCategories.MR)
{
info.Position = Positions.Pre_C;
continue;
}
// Left matra
if (info.MyanmarCategory == MyanmarCategories.VPre)
{
info.Position = Positions.Pre_M;
continue;
}
if (info.MyanmarCategory == MyanmarCategories.VS)
{
info.Position = substitutionCollection[i - 1].IndicShapingEngineInfo!.Position;
continue;
}
if (pos == Positions.After_Main && info.MyanmarCategory == MyanmarCategories.VBlw)
{
pos = Positions.Below_C;
info.Position = pos;
continue;
}
if (pos == Positions.Below_C && info.MyanmarCategory == MyanmarCategories.A)
{
info.Position = Positions.Before_Sub;
continue;
}
if (pos == Positions.Below_C && info.MyanmarCategory == MyanmarCategories.VBlw)
{
info.Position = pos;
continue;
}
if (pos == Positions.Below_C && info.MyanmarCategory != MyanmarCategories.A)
{
pos = Positions.After_Sub;
info.Position = pos;
continue;
}
info.Position = pos;
}
}
substitutionCollection.Sort(start, end, (a, b) =>
{
int pa = a.IndicShapingEngineInfo?.Position != null ? (int)a.IndicShapingEngineInfo.Position : 0;
int pb = b.IndicShapingEngineInfo?.Position != null ? (int)b.IndicShapingEngineInfo.Position : 0;
return pa - pb;
});
// Flip left-matra sequence.
int firstLeftMatra = end;
int lastLeftMatra = end;
for (int i = start; i < end; i++)
{
if (substitutionCollection[i].IndicShapingEngineInfo?.Position == Positions.Pre_M)
{
if (firstLeftMatra == end)
{
firstLeftMatra = i;
}
lastLeftMatra = i;
}
}
// https://github.com/harfbuzz/harfbuzz/issues/3863
if (firstLeftMatra < lastLeftMatra)
{
// No need to merge clusters, done already?
substitutionCollection.ReverseRange(firstLeftMatra, lastLeftMatra + 1);
// Reverse back VS, etc.
int i = firstLeftMatra;
for (int j = i; j <= lastLeftMatra; j++)
{
if (substitutionCollection[j].IndicShapingEngineInfo?.MyanmarCategory == MyanmarCategories.VPre)
{
substitutionCollection.ReverseRange(i, j + 1);
i = j + 1;
}
}
}
}
/// <summary>
/// Determines whether the glyph data represents a Myanmar consonant.
/// </summary>
/// <param name="data">The glyph shaping data.</param>
/// <returns><see langword="true"/> if the glyph is a consonant.</returns>
private static bool IsConsonant(GlyphShapingData data)
=> data.IndicShapingEngineInfo != null && (FlagUnsafe(data.IndicShapingEngineInfo.MyanmarCategory) & MyanmarConsonantFlags) != 0;
/// <summary>
/// Finds the start index of the next syllable in the collection.
/// </summary>
/// <param name="collection">The glyph substitution collection.</param>
/// <param name="index">The current index.</param>
/// <param name="count">The maximum index bound.</param>
/// <returns>The start index of the next syllable.</returns>
private static int NextSyllable(GlyphSubstitutionCollection collection, int index, int count)
{
if (index >= count)
{
return index;
}
int? syllable = collection[index].IndicShapingEngineInfo?.Syllable;
while (++index < count)
{
if (collection[index].IndicShapingEngineInfo?.Syllable != syllable)
{
break;
}
}
return index;
}
/// <summary>
/// Gets the Indic shaping category for a code point (upper 8 bits of the shaping properties).
/// </summary>
/// <param name="codePoint">The code point.</param>
/// <returns>The shaping category value.</returns>
private static int IndicShapingCategory(CodePoint codePoint)
=> UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) >> 8;
/// <summary>
/// Gets the Indic shaping position for a code point (lower 8 bits as a bit flag).
/// </summary>
/// <param name="codePoint">The code point.</param>
/// <returns>The shaping position as a bit flag.</returns>
private static int IndicShapingPosition(CodePoint codePoint)
=> 1 << (UnicodeData.GetIndicShapingProperties((uint)codePoint.Value) & 0xFF);
/// <summary>
/// Builds a lookup table mapping Myanmar shaping category codes to compact DFA symbol indices.
/// </summary>
/// <returns>An array mapping category codes to symbol IDs.</returns>
private static int[] BuildCategoryToSymbolId()
{
// Get all enum values in declared order (important!)
MyanmarCategories[] values = Enum.GetValues<MyanmarCategories>();
// Determine maximum underlying numeric category so we can index safetly
int maxCategoryValue = 0;
foreach (MyanmarCategories v in values)
{
int val = (int)v;
if (val > maxCategoryValue)
{
maxCategoryValue = val;
}
}
// Allocate mapping table indexed by Harfbuzz category code
int[] map = new int[maxCategoryValue + 1];
// Assign compact DFA symbol indices 0..N-1 in enum order
for (int symbolId = 0; symbolId < values.Length; symbolId++)
{
MyanmarCategories cat = values[symbolId];
int categoryCode = (int)cat; // Harfbuzz-style category code
map[categoryCode] = symbolId; // DFA symbol id
}
return map;
}
}
}