// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers { /// /// Hebrew shaper. Handles mark reordering (PATAH/QAMATS before SHEVA/HIRIQ before METEG) /// and presentation form composition for legacy fonts without GPOS mark positioning. /// Based on HarfBuzz: /// internal class HebrewShaper : DefaultShaper { /// /// Hebrew presentation forms with dagesh for U+05D0..U+05EA. /// A value of 0x0000 means no dagesh form exists for that letter. /// private static readonly ushort[] DageshForms = [ 0xFB30, // ALEF 0xFB31, // BET 0xFB32, // GIMEL 0xFB33, // DALET 0xFB34, // HE 0xFB35, // VAV 0xFB36, // ZAYIN 0x0000, // HET 0xFB38, // TET 0xFB39, // YOD 0xFB3A, // FINAL KAF 0xFB3B, // KAF 0xFB3C, // LAMED 0x0000, // FINAL MEM 0xFB3E, // MEM 0x0000, // FINAL NUN 0xFB40, // NUN 0xFB41, // SAMEKH 0x0000, // AYIN 0xFB43, // FINAL PE 0xFB44, // PE 0x0000, // FINAL TSADI 0xFB46, // TSADI 0xFB47, // QOF 0xFB48, // RESH 0xFB49, // SHIN 0xFB4A, // TAV ]; /// The font metrics used for glyph lookups during composition. private readonly FontMetrics fontMetrics; /// Whether the font has GSUB features for Hebrew. private readonly bool hasGsub; /// /// Initializes a new instance of the class. /// /// The script classification. /// The text options. /// The font metrics for glyph lookups. /// Whether the font has GSUB features for Hebrew. public HebrewShaper(ScriptClass script, TextOptions textOptions, FontMetrics fontMetrics, bool hasGsub) : base(script, MarkZeroingMode.PostGpos, textOptions) { this.fontMetrics = fontMetrics; this.hasGsub = hasGsub; } /// protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count) { base.AssignFeatures(collection, index, count); if (collection is not GlyphSubstitutionCollection substitutionCollection) { return; } // Step 1: Reorder Hebrew marks. // Swap SHEVA/HIRIQ with following METEG when preceded by PATAH/QAMATS. // https://bugzilla.mozilla.org/show_bug.cgi?id=728866 ReorderMarks(substitutionCollection, index, count); // Step 2: Compose Hebrew presentation forms for legacy fonts. // Only applied when the font lacks GSUB features (proxy for lacking GPOS mark). if (!this.hasGsub) { ComposeHebrewForms(substitutionCollection, this.fontMetrics, index, count); } } /// /// Reorders Hebrew combining marks to ensure correct rendering. /// /// Looks for the pattern [PATAH/QAMATS, SHEVA/HIRIQ, METEG/BELOW] and swaps /// the last two marks. This ensures correct visual stacking of vowel points /// and the meteg stress mark. /// /// /// The glyph substitution collection. /// The zero-based start index. /// The number of elements to process. private static void ReorderMarks(GlyphSubstitutionCollection collection, int index, int count) { int end = index + count; for (int i = index + 2; i < end; i++) { int c0 = collection[i - 2].CodePoint.Value; int c1 = collection[i - 1].CodePoint.Value; int c2 = collection[i].CodePoint.Value; // c0: PATAH (U+05B7) or QAMATS (U+05B8) // c1: SHEVA (U+05B0) or HIRIQ (U+05B4) // c2: METEG (U+05BD) or a below-class mark if (IsPatahOrQamats(c0) && IsShevaOrHiriq(c1) && IsMetegOrBelow(c2)) { // Swap positions i-1 and i. GlyphShapingData data1 = collection[i - 1]; GlyphShapingData data2 = collection[i]; // Swap codepoints and glyph IDs. (collection[i - 1].CodePoint, collection[i].CodePoint) = (data2.CodePoint, data1.CodePoint); (collection[i - 1].GlyphId, collection[i].GlyphId) = (data2.GlyphId, data1.GlyphId); break; } } } /// /// Composes Hebrew base + mark sequences into precomposed presentation forms. /// This is a fallback for legacy fonts that lack GPOS mark-to-base positioning. /// /// The glyph substitution collection. /// The font metrics for glyph lookups. /// The zero-based start index. /// The number of elements to process. private static void ComposeHebrewForms(GlyphSubstitutionCollection collection, FontMetrics fontMetrics, int index, int count) { int end = index + count; for (int i = index + 1; i < end; i++) { int a = collection[i - 1].CodePoint.Value; int b = collection[i].CodePoint.Value; int composed = TryCompose(a, b); if (composed != 0 && fontMetrics.TryGetGlyphId(new CodePoint(composed), out ushort composedGlyphId)) { // Replace the two glyphs with the composed form. collection.Replace(i - 1, 2, composedGlyphId, KnownFeatureTags.GlyphCompositionDecomposition); end--; i--; } } } /// /// Attempts to compose two Hebrew codepoints into a precomposed presentation form. /// Returns the composed codepoint, or 0 if no composition exists. /// /// The first (base) codepoint value. /// The second (combining mark) codepoint value. /// The composed codepoint, or 0 if no composition exists. private static int TryCompose(int a, int b) { switch (b) { case 0x05B4: // HIRIQ if (a == 0x05D9) { // YOD return 0xFB1D; } break; case 0x05B7: // PATAH if (a == 0x05F2) { // YIDDISH YOD YOD + PATAH return 0xFB1F; } if (a == 0x05D0) { // ALEF + PATAH return 0xFB2E; } break; case 0x05B8: // QAMATS if (a == 0x05D0) { // ALEF + QAMATS return 0xFB2F; } break; case 0x05B9: // HOLAM if (a == 0x05D5) { // VAV + HOLAM return 0xFB4B; } break; case 0x05BC: // DAGESH if (a is >= 0x05D0 and <= 0x05EA) { int form = DageshForms[a - 0x05D0]; return form != 0 ? form : 0; } if (a == 0xFB2A) { // SHIN WITH SHIN DOT + DAGESH return 0xFB2C; } if (a == 0xFB2B) { // SHIN WITH SIN DOT + DAGESH return 0xFB2D; } break; case 0x05BF: // RAFE if (a == 0x05D1) { // BET + RAFE return 0xFB4C; } if (a == 0x05DB) { // KAF + RAFE return 0xFB4D; } if (a == 0x05E4) { // PE + RAFE return 0xFB4E; } break; case 0x05C1: // SHIN DOT if (a == 0x05E9) { // SHIN + SHIN DOT return 0xFB2A; } if (a == 0xFB49) { // SHIN WITH DAGESH + SHIN DOT return 0xFB2C; } break; case 0x05C2: // SIN DOT if (a == 0x05E9) { // SHIN + SIN DOT return 0xFB2B; } if (a == 0xFB49) { // SHIN WITH DAGESH + SIN DOT return 0xFB2D; } break; } return 0; } /// Returns if the codepoint is PATAH (U+05B7) or QAMATS (U+05B8). /// The codepoint value to test. /// if the codepoint is PATAH or QAMATS. private static bool IsPatahOrQamats(int codepoint) => codepoint is 0x05B7 or 0x05B8; /// Returns if the codepoint is SHEVA (U+05B0) or HIRIQ (U+05B4). /// The codepoint value to test. /// if the codepoint is SHEVA or HIRIQ. private static bool IsShevaOrHiriq(int codepoint) => codepoint is 0x05B0 or 0x05B4; /// /// Returns if the codepoint is METEG (U+05BD) or a combining mark with CCC = Below (220). /// Currently checks METEG only; generic CCC=220 detection would require combining class data. /// /// The codepoint value to test. /// if the codepoint is METEG or a below-class mark. private static bool IsMetegOrBelow(int codepoint) => codepoint == 0x05BD; } }