From 61bffbab03aaa7e4916d79251dc7ed7c6032a9c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ju=CC=88rg=20Lehni?= Date: Wed, 20 May 2026 21:10:32 +0200 Subject: [PATCH] Add Thai shaper, ported from HarfBuzz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Decompose SARA AM (U+0E33) into NIKHAHIT + SARA AA and reorder NIKHAHIT past above-base marks, matching `preprocess_text_thai` - Apply PUA tone/vowel shift fallback for legacy fonts without Thai GSUB, mirroring `do_thai_pua_shaping` (above/below state machines + Windows/Mac PUA mapping tables) - Fall back to the buffer's Unicode script in OTLayoutEngine when neither GSUB nor GPOS picks an OT script — lets script-specific shapers run on fonts without matching GSUB/GPOS so the Thai PUA fallback is actually reachable - Fix typo in `UnicodeLayoutEngine` Thai mark classification: `0x0E3D` (unassigned) → `0x0E4D` (NIKHAHIT) - Register `thai` and `'lao '` (the 4-char OT tag with trailing space), add Noto Sans Thai + Noto Sans Lao (OFL) as test fixtures with 8 shaping tests --- src/layout/UnicodeLayoutEngine.js | 2 +- src/opentype/OTLayoutEngine.js | 10 +- src/opentype/shapers/ThaiShaper.js | 286 ++++++++++++++++++ src/opentype/shapers/index.js | 4 + .../NotoSans/NotoSansLao-Regular.LICENSE.txt | 94 ++++++ test/data/NotoSans/NotoSansLao-Regular.ttf | Bin 0 -> 31340 bytes .../NotoSans/NotoSansThai-Regular.LICENSE.txt | 94 ++++++ test/data/NotoSans/NotoSansThai-Regular.ttf | Bin 0 -> 37752 bytes test/shaping.js | 40 +++ 9 files changed, 526 insertions(+), 4 deletions(-) create mode 100644 src/opentype/shapers/ThaiShaper.js create mode 100644 test/data/NotoSans/NotoSansLao-Regular.LICENSE.txt create mode 100644 test/data/NotoSans/NotoSansLao-Regular.ttf create mode 100644 test/data/NotoSans/NotoSansThai-Regular.LICENSE.txt create mode 100644 test/data/NotoSans/NotoSansThai-Regular.ttf diff --git a/src/layout/UnicodeLayoutEngine.js b/src/layout/UnicodeLayoutEngine.js index 68d5b9a0..27c5ffd7 100644 --- a/src/layout/UnicodeLayoutEngine.js +++ b/src/layout/UnicodeLayoutEngine.js @@ -149,7 +149,7 @@ export default class UnicodeLayoutEngine { case 0x0e37: case 0x0e47: case 0x0e4c: - case 0x0e3d: + case 0x0e4d: case 0x0e4e: return 'Above_Right'; diff --git a/src/opentype/OTLayoutEngine.js b/src/opentype/OTLayoutEngine.js index 15cf3627..ae5a1a4f 100644 --- a/src/opentype/OTLayoutEngine.js +++ b/src/opentype/OTLayoutEngine.js @@ -38,9 +38,13 @@ export default class OTLayoutEngine { } // Choose a shaper based on the script, and setup a shaping plan. - // This determines which features to apply to which glyphs. - this.shaper = Shapers.choose(script); - this.plan = new ShapingPlan(this.font, script, glyphRun.direction); + // This determines which features to apply to which glyphs. Fall back + // to the buffer's Unicode script when neither GSUB nor GPOS picked an + // OT script — script-specific shaping (e.g. Thai SARA AM decomp, the + // PUA fallback for fonts without Thai GSUB) still applies. + let shaperScript = script || glyphRun.script; + this.shaper = Shapers.choose(shaperScript); + this.plan = new ShapingPlan(this.font, shaperScript, glyphRun.direction); this.shaper.plan(this.plan, this.glyphInfos, glyphRun.features); // Assign chosen features to output glyph run diff --git a/src/opentype/shapers/ThaiShaper.js b/src/opentype/shapers/ThaiShaper.js new file mode 100644 index 00000000..46f450a3 --- /dev/null +++ b/src/opentype/shapers/ThaiShaper.js @@ -0,0 +1,286 @@ +import DefaultShaper from './DefaultShaper'; +import GlyphInfo from '../GlyphInfo'; + +/** + * Thai / Lao shaper, ported from HarfBuzz's hb-ot-shaper-thai.cc. + * + * 1. SARA AM decomposition + NIKHAHIT reorder (always-on) + * 2. PUA fallback shaping for legacy fonts without Thai GSUB + * + * Step 1 is needed by every modern Thai font — without it the GSUB chain + * rules for tone-mark shifting (e.g. `uni0E49.small`) never fire because + * the buffer ends up with `[base, tone, NIKHAHIT, SARA AA]` instead of + * `[base, NIKHAHIT, tone, SARA AA]`. + * + * SARA AM (U+0E33) -> NIKHAHIT (U+0E4D) + SARA AA (U+0E32) + * Lao SARA AM (U+0EB3) -> NIKHAHIT (U+0ECD) + SARA AA (U+0EB2) + * + * The NIKHAHIT then walks backward over any above-base marks so it sits + * between the base and the existing tone-mark stack. + * + * <0E14, 0E4B, 0E33> -> <0E14, 0E4D, 0E4B, 0E32> + * + * Step 2 walks an above/below state machine and remaps tone marks to PUA + * codepoints when the font ships those (older Microsoft / Apple Thai + * fonts). Modern fonts with GSUB don't need this; HarfBuzz gates it on + * the absence of Thai GSUB and so do we. + */ +export default class ThaiShaper extends DefaultShaper { + static assignFeatures(plan, glyphs) { + super.assignFeatures(plan, glyphs); + preprocessThai(glyphs, plan.font); + if (plan.script === 'thai' && !hasThaiGsub(plan.font)) { + applyThaiPuaShaping(glyphs, plan.font); + } + } +} + +// Thai SARA AM is U+0E33; Lao SARA AM is U+0EB3 — they only differ in the +// 0x80 bit so HarfBuzz uses a script-agnostic mask. We do the same. +function isSaraAm(u) { + return (u & ~0x0080) === 0x0E33; +} + +function nikhahitFromSaraAm(u) { + return u - 0x0E33 + 0x0E4D; +} + +function saraAaFromSaraAm(u) { + return u - 1; +} + +// Marks that sit above the base. The script-agnostic mask applies the +// same set for both Thai and Lao (Lao codepoints are offset by 0x80). +// Thai: U+0E31, U+0E34..U+0E37, U+0E47..U+0E4E, U+0E3B +// Lao: U+0EB1, U+0EB4..U+0EB7, U+0EC8..U+0ECE, U+0EBB +function isAboveBaseMark(u) { + const c = u & ~0x0080; + return c === 0x0E31 + || (c >= 0x0E34 && c <= 0x0E37) + || (c >= 0x0E47 && c <= 0x0E4E) + || c === 0x0E3B; +} + +function preprocessThai(glyphs, font) { + let i = 0; + while (i < glyphs.length) { + const u = glyphs[i].codePoints[0]; + if (!isSaraAm(u)) { + i++; + continue; + } + + // Decompose SARA AM in place into NIKHAHIT + SARA AA. Both new + // glyphs inherit the original GlyphInfo's feature flags. + const features = glyphs[i].features; + const nikhahit = makeGlyph(font, nikhahitFromSaraAm(u), features); + const saraAa = makeGlyph(font, saraAaFromSaraAm(u), features); + glyphs.splice(i, 1, nikhahit, saraAa); + + // Walk the NIKHAHIT backward over any above-base marks belonging to + // the same base. + let nikhahitIndex = i; + let target = nikhahitIndex; + while (target > 0 && isAboveBaseMark(glyphs[target - 1].codePoints[0])) { + target--; + } + if (target !== nikhahitIndex) { + const moved = glyphs.splice(nikhahitIndex, 1)[0]; + glyphs.splice(target, 0, moved); + } + + // Advance past NIKHAHIT + SARA AA. + i += 2; + } +} + +function makeGlyph(font, codePoint, features) { + const id = font.glyphForCodePoint(codePoint).id; + return new GlyphInfo(font, id, [codePoint], features); +} + +// ── PUA fallback shaping ──────────────────────────────────────────────── +// +// Walks an above-base state machine and a below-base state machine in +// parallel. Each tone/vowel mark may trigger one of the following +// actions: +// +// NOP — leave the glyph alone +// SD — shift the mark DOWN to clear a descender +// SL — shift the mark LEFT to clear another above-base mark +// SDL — shift the mark DOWN-LEFT (both) +// RD — remove the descender from the BASE consonant +// +// Each action is realised by replacing the mark (or base) codepoint with +// a private-use mapping, when the font ships that PUA glyph. + +const NOP = 0; +const SD = 1; +const SL = 2; +const SDL = 3; +const RD = 4; + +// Consonant types +const NC = 0; // normal consonant +const AC = 1; // consonant with ascender (1B/1D/1F) +const RC = 2; // consonant with removable descender (0D/10) +const DC = 3; // consonant with strict descender (0E/0F) +const NOT_CONSONANT = 4; + +// Mark types +const AV = 0; // above-base vowel/mark +const BV = 1; // below-base vowel/mark +const T = 2; // tone mark +const NOT_MARK = 3; + +function getConsonantType(u) { + if (u === 0x0E1B || u === 0x0E1D || u === 0x0E1F) return AC; + if (u === 0x0E0D || u === 0x0E10) return RC; + if (u === 0x0E0E || u === 0x0E0F) return DC; + if (u >= 0x0E01 && u <= 0x0E2E) return NC; + return NOT_CONSONANT; +} + +function getMarkType(u) { + if ( + u === 0x0E31 || + (u >= 0x0E34 && u <= 0x0E37) || + u === 0x0E47 || + (u >= 0x0E4D && u <= 0x0E4E) + ) { + return AV; + } + if (u >= 0x0E38 && u <= 0x0E3A) return BV; + if (u >= 0x0E48 && u <= 0x0E4C) return T; + return NOT_MARK; +} + +// Above-base cluster state (T0..T3 = increasing stack height). +const T0 = 0, T1 = 1, T2 = 2, T3 = 3; +const ABOVE_START_STATE = [T0, T1, T0, T0, T3]; +// NC AC RC DC NOT_CONSONANT +const ABOVE_STATE_MACHINE = [ + // AV BV T + [[NOP, T3], [NOP, T0], [SD, T3]], // T0 + [[SL, T2], [NOP, T1], [SDL, T2]], // T1 + [[NOP, T3], [NOP, T2], [SL, T3]], // T2 + [[NOP, T3], [NOP, T3], [NOP, T3]] // T3 +]; + +// Below-base state (B0=none, B1=removable, B2=strict). +const B0 = 0, B1 = 1, B2 = 2; +const BELOW_START_STATE = [B0, B0, B1, B2, B2]; +// NC AC RC DC NOT_CONSONANT +const BELOW_STATE_MACHINE = [ + // AV BV T + [[NOP, B0], [NOP, B2], [NOP, B0]], // B0 + [[NOP, B1], [RD, B2], [NOP, B1]], // B1 + [[NOP, B2], [SD, B2], [NOP, B2]] // B2 +]; + +// PUA mappings (Windows and Mac private-use codepoints for shifted marks +// and descender-less base consonants). For each action we try the +// Windows PUA first, then the Mac PUA, then leave the codepoint alone. +const PUA_MAPPINGS = { + [SD]: [ + [0x0E48, 0xF70A, 0xF88B], // MAI EK + [0x0E49, 0xF70B, 0xF88E], // MAI THO + [0x0E4A, 0xF70C, 0xF891], // MAI TRI + [0x0E4B, 0xF70D, 0xF894], // MAI CHATTAWA + [0x0E4C, 0xF70E, 0xF897], // THANTHAKHAT + [0x0E38, 0xF718, 0xF89B], // SARA U + [0x0E39, 0xF719, 0xF89C], // SARA UU + [0x0E3A, 0xF71A, 0xF89D] // PHINTHU + ], + [SDL]: [ + [0x0E48, 0xF705, 0xF88C], // MAI EK + [0x0E49, 0xF706, 0xF88F], // MAI THO + [0x0E4A, 0xF707, 0xF892], // MAI TRI + [0x0E4B, 0xF708, 0xF895], // MAI CHATTAWA + [0x0E4C, 0xF709, 0xF898] // THANTHAKHAT + ], + [SL]: [ + [0x0E48, 0xF713, 0xF88A], // MAI EK + [0x0E49, 0xF714, 0xF88D], // MAI THO + [0x0E4A, 0xF715, 0xF890], // MAI TRI + [0x0E4B, 0xF716, 0xF893], // MAI CHATTAWA + [0x0E4C, 0xF717, 0xF896], // THANTHAKHAT + [0x0E31, 0xF710, 0xF884], // MAI HAN-AKAT + [0x0E34, 0xF701, 0xF885], // SARA I + [0x0E35, 0xF702, 0xF886], // SARA II + [0x0E36, 0xF703, 0xF887], // SARA UE + [0x0E37, 0xF704, 0xF888], // SARA UEE + [0x0E47, 0xF712, 0xF889], // MAITAIKHU + [0x0E4D, 0xF711, 0xF899] // NIKHAHIT + ], + [RD]: [ + [0x0E0D, 0xF70F, 0xF89A], // YO YING + [0x0E10, 0xF700, 0xF89E] // THO THAN + ] +}; + +function thaiPuaShape(u, action, font) { + if (action === NOP) return u; + const mappings = PUA_MAPPINGS[action]; + if (!mappings) return u; + for (const [orig, winPua, macPua] of mappings) { + if (orig !== u) continue; + if (font.hasGlyphForCodePoint(winPua)) return winPua; + if (font.hasGlyphForCodePoint(macPua)) return macPua; + break; + } + return u; +} + +function applyThaiPuaShaping(glyphs, font) { + let aboveState = ABOVE_START_STATE[NOT_CONSONANT]; + let belowState = BELOW_START_STATE[NOT_CONSONANT]; + let baseIndex = 0; + + for (let i = 0; i < glyphs.length; i++) { + const u = glyphs[i].codePoints[0]; + const mt = getMarkType(u); + + if (mt === NOT_MARK) { + const ct = getConsonantType(u); + aboveState = ABOVE_START_STATE[ct]; + belowState = BELOW_START_STATE[ct]; + baseIndex = i; + continue; + } + + const [aboveAction, aboveNext] = ABOVE_STATE_MACHINE[aboveState][mt]; + const [belowAction, belowNext] = BELOW_STATE_MACHINE[belowState][mt]; + aboveState = aboveNext; + belowState = belowNext; + + // At least one of the two actions is NOP; the other wins. + const action = aboveAction !== NOP ? aboveAction : belowAction; + if (action === NOP) continue; + + if (action === RD) { + const target = glyphs[baseIndex]; + const newCp = thaiPuaShape(target.codePoints[0], action, font); + if (newCp !== target.codePoints[0]) { + target.id = font.glyphForCodePoint(newCp).id; + target.codePoints = [newCp]; + } + } else { + const target = glyphs[i]; + const newCp = thaiPuaShape(u, action, font); + if (newCp !== u) { + target.id = font.glyphForCodePoint(newCp).id; + target.codePoints = [newCp]; + } + } + } +} + +// HarfBuzz gates PUA shaping on the font lacking a Thai GSUB script +// (`plan->map.found_script[0]` is false). For fontkit we check whether +// the GSUB script list contains `thai` or `thai2`. +function hasThaiGsub(font) { + const gsub = font.GSUB; + if (!gsub || !gsub.scriptList) return false; + return gsub.scriptList.some(entry => entry.tag === 'thai' || entry.tag === 'tha2'); +} diff --git a/src/opentype/shapers/index.js b/src/opentype/shapers/index.js index b22ebdd3..6f01c758 100644 --- a/src/opentype/shapers/index.js +++ b/src/opentype/shapers/index.js @@ -2,6 +2,7 @@ import DefaultShaper from './DefaultShaper'; import ArabicShaper from './ArabicShaper'; import HangulShaper from './HangulShaper'; import IndicShaper from './IndicShaper'; +import ThaiShaper from './ThaiShaper'; import UniversalShaper from './UniversalShaper'; const SHAPERS = { @@ -36,6 +37,9 @@ const SHAPERS = { tel2: IndicShaper, // Telugu khmr: IndicShaper, // Khmer + thai: ThaiShaper, // Thai + 'lao ': ThaiShaper, // Lao (4-char OT tag with trailing space) + bali: UniversalShaper, // Balinese batk: UniversalShaper, // Batak brah: UniversalShaper, // Brahmi diff --git a/test/data/NotoSans/NotoSansLao-Regular.LICENSE.txt b/test/data/NotoSans/NotoSansLao-Regular.LICENSE.txt new file mode 100644 index 00000000..c82d72e4 --- /dev/null +++ b/test/data/NotoSans/NotoSansLao-Regular.LICENSE.txt @@ -0,0 +1,94 @@ +Copyright 2018 The Noto Project Authors (github.com/googlei18n/noto-fonts) + +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to +provide a free and open framework in which fonts may be shared and +improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software +components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, +deleting, or substituting -- in part or in whole -- any of the +components of the Original Version, by changing formats or by porting +the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, +modify, redistribute, and sell modified and unmodified copies of the +Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the +corresponding Copyright Holder. This restriction only applies to the +primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/test/data/NotoSans/NotoSansLao-Regular.ttf b/test/data/NotoSans/NotoSansLao-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..02148887ba0f3dd56bc02fda0d0be0dad3de07e2 GIT binary patch literal 31340 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�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 0` short-circuit. + test('should decompose a bare SARA AM', + 'NotoSans/NotoSansThai-Regular.ttf', 'ำ', + '59+0|86+406'); + + // SARA AM after only an above-base mark (no consonant base). NIKHAHIT walks back over the above mark to index 0. + test('should reorder NIKHAHIT past a leading above-mark with no base', + 'NotoSans/NotoSansThai-Regular.ttf', 'ัำ', + '59+0|45@-29,236+0|86+406'); + + // Two SARA AMs in one run — exercise the loop entering decomposition twice and re-positioning the cursor correctly between them. + test('should decompose multiple SARA AMs in one word', + 'NotoSans/NotoSansThai-Regular.ttf', 'น้ำน้ำ', + '71+613|59+0|49@-29,0+0|86+406|71+613|59+0|49@-29,0+0|86+406'); + + // Lao SARA AM (U+0EB3) — the script-agnostic mask handles both Thai and Lao. This requires the 'lao ' OT tag (with trailing space) in the shaper registry. + test('should decompose Lao SARA AM and reorder NIKHAHIT past an above-mark', + 'NotoSans/NotoSansLao-Regular.ttf', 'ຫັຳ', + '28+726|72@-28,0+0|58@-152,278+0|34+324'); + }); + describe('hangul shaper', function () { let font = fontkit.openSync(new URL('data/NotoSansCJK/NotoSansCJKkr-Regular.otf', import.meta.url));