From 218298bb16ef6c147aa6dafc3ea88f62c6fa6ef6 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 28 Sep 2026 15:07:51 +0100 Subject: Shape Thai and Lao as HarfBuzz does The Thai shaper, from HarfBuzz 10.2's source: SARA AM comes apart into NIKHAHIT and SARA AA, the NIKHAHIT moving back over the above-base marks (and counting as a mark when advances are zeroed). A font with no Thai script of its own (only DFLT or latn in GSUB) gets its marks shifted to its private-use glyphs, Windows' before the Mac's, as the stacking around the base asks; the planner records, as HarfBuzz's does, whether the font had the script under its own tag. Noto Sans Thai and Lao, TLWG Loma, FreeSerif's Thai, DejaVu Sans's Lao and Unifont shape as HarfBuzz does at 3,000 runs a plan, and Loma with its thai script taken out (so the private-use path runs) at 5,000. --- text/shape-build.c | 12 ++++-- text/shape.c | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++- text/shape.h | 11 ++--- 3 files changed, 132 insertions(+), 10 deletions(-) (limited to 'text') diff --git a/text/shape-build.c b/text/shape-build.c index a3b924e..96bc748 100644 --- a/text/shape-build.c +++ b/text/shape-build.c @@ -33,6 +33,7 @@ typedef struct { hb_face_t *face; unsigned script_index[2], language_index[2]; hb_tag_t chosen_script[2]; + bool found_script[2]; /* one of the script's own tags, not DFLT or latn */ FeatureInfo infos[MAX_FEATURES]; unsigned info_count, current_stage[2]; unsigned pause[2][MAX_STAGES]; /* what ends each stage: VEGA_PAUSE_..., or 0 */ @@ -332,7 +333,8 @@ static int categorize(uint32_t script, hb_tag_t gsub_script) { if (script == TAG("Hebr")) return VEGA_SHAPER_HEBREW; if (script == TAG("Hang")) return VEGA_SHAPER_HANGUL; if (script == TAG("Khmr")) return VEGA_SHAPER_KHMER; - if (script == TAG("Syrc") || script == TAG("Thai") || script == TAG("Laoo") || script == TAG("Qaag")) return -1; + if (script == TAG("Thai") || script == TAG("Laoo")) return VEGA_SHAPER_THAI; + if (script == TAG("Syrc") || script == TAG("Qaag")) return -1; if (listed(indic_scripts, sizeof(indic_scripts) / sizeof(*indic_scripts), script)) return dflt ? VEGA_SHAPER_DEFAULT : (gsub_script & 0xFF) == '3' ? VEGA_SHAPER_USE : VEGA_SHAPER_INDIC; if (listed(use_scripts, sizeof(use_scripts) / sizeof(*use_scripts), script)) @@ -430,8 +432,8 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan * hb_ot_tags_from_script_and_language(script, HB_LANGUAGE_INVALID, &script_count, script_tags, &language_count, language_tags); for (int t = 0; t < 2; ++t) { - hb_ot_layout_table_select_script(face, table_tags[t], script_count, script_tags, &m->script_index[t], - &m->chosen_script[t]); + m->found_script[t] = hb_ot_layout_table_select_script(face, table_tags[t], script_count, script_tags, + &m->script_index[t], &m->chosen_script[t]); hb_ot_layout_script_select_language(face, table_tags[t], m->script_index[t], language_count, language_tags, &m->language_index[t]); } @@ -610,7 +612,7 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan * /* Hangul's, Indic's and Khmer's shapers zero no marks (Myanmar's does, * before GPOS); none of these places marks without GPOS. */ bool zeroes = shaper != VEGA_SHAPER_HANGUL && shaper != VEGA_SHAPER_INDIC && shaper != VEGA_SHAPER_KHMER; - bool script_marks = zeroes && shaper != VEGA_SHAPER_MYANMAR && shaper != VEGA_SHAPER_USE; + bool script_marks = zeroes && shaper != VEGA_SHAPER_MYANMAR && shaper != VEGA_SHAPER_USE && shaper != VEGA_SHAPER_THAI; bool adjust = !apply_gpos && (!apply_kern || !kern_table.cross), fallback_marks = adjust && script_marks; if (shaper == VEGA_SHAPER_ARABIC) { if (one_mask(m, "stch")) FAIL("%s: the font stretches (stch)", name); @@ -655,6 +657,8 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan * } if (shaper == VEGA_SHAPER_KHMER) for (int i = 0; i < 5; ++i) plan->masks[VEGA_MASK_KHMER_PREF + i] = one_mask(m, khmer_basic[i]); + if (shaper == VEGA_SHAPER_THAI && script == HB_SCRIPT_THAI && !m->found_script[0]) + plan->shaper_flags |= VEGA_THAI_PUA; free(m); return true; fail: diff --git a/text/shape.c b/text/shape.c index 76d5409..20fea05 100644 --- a/text/shape.c +++ b/text/shape.c @@ -227,7 +227,7 @@ bool vega_shaping_open(VegaShaping *s, const unsigned char *bytes, size_t size, for (uint32_t i = 0; i < s->plan_count; ++i) { const unsigned char *p = s->plans + (size_t)i * VEGA_PLAN_BYTES; uint32_t ranges[2][2] = {{le32(p + 80), le32(p + 84)}, {le32(p + 88), le32(p + 92)}}; - if ((p[4] != VEGA_SHAPE_LTR && p[4] != VEGA_SHAPE_RTL) || p[5] > VEGA_SHAPE_RTL || p[6] > VEGA_SHAPER_USE) + if ((p[4] != VEGA_SHAPE_LTR && p[4] != VEGA_SHAPE_RTL) || p[5] > VEGA_SHAPE_RTL || p[6] > VEGA_SHAPER_THAI) REFUSE("plan %u", i); if ((p[7] & VEGA_PLAN_FALLBACK_MARKS) && !s->extents) REFUSE("plan %u: marks placed without extents", i); for (int table = 0; table < 2; ++table) { @@ -3510,6 +3510,122 @@ static bool use_reorder(const VegaShaping *s, VegaShaper *b) { return changed; } +/* ---- Thai and Lao (hb-ot-shaper-thai.cc): SARA AM taken apart into + * NIKHAHIT and SARA AA, the NIKHAHIT moved back over the above-base marks; + * in a font with no Thai script, marks shifted by the font's private-use + * glyphs (Windows' or the Mac's) as the stacking around the base asks. */ + +enum { THAI_NC, THAI_AC, THAI_RC, THAI_DC, THAI_NOT_CONSONANT }; +enum { THAI_AV, THAI_BV, THAI_T, THAI_NOT_MARK }; +enum { THAI_NOP, THAI_SD, THAI_SL, THAI_SDL, THAI_RD }; + +static unsigned thai_consonant_type(uint32_t u) { + if (u == 0x0E1B || u == 0x0E1D || u == 0x0E1F) return THAI_AC; + if (u == 0x0E0D || u == 0x0E10) return THAI_RC; + if (u == 0x0E0E || u == 0x0E0F) return THAI_DC; + if (u >= 0x0E01 && u <= 0x0E2E) return THAI_NC; + return THAI_NOT_CONSONANT; +} + +static unsigned thai_mark_type(uint32_t u) { + if (u == 0x0E31 || (u >= 0x0E34 && u <= 0x0E37) || u == 0x0E47 || (u >= 0x0E4D && u <= 0x0E4E)) return THAI_AV; + if (u >= 0x0E38 && u <= 0x0E3A) return THAI_BV; + if (u >= 0x0E48 && u <= 0x0E4C) return THAI_T; + return THAI_NOT_MARK; +} + +/* The private-use code point the font has for u shifted by action + * (Windows' before the Mac's), or u. */ +static uint32_t thai_pua_shape(const VegaShaping *s, uint32_t u, unsigned action) { + static const uint16_t sd[][3] = {{0x0E48, 0xF70A, 0xF88B}, {0x0E49, 0xF70B, 0xF88E}, {0x0E4A, 0xF70C, 0xF891}, + {0x0E4B, 0xF70D, 0xF894}, {0x0E4C, 0xF70E, 0xF897}, {0x0E38, 0xF718, 0xF89B}, + {0x0E39, 0xF719, 0xF89C}, {0x0E3A, 0xF71A, 0xF89D}, {0, 0, 0}}; + static const uint16_t sdl[][3] = {{0x0E48, 0xF705, 0xF88C}, {0x0E49, 0xF706, 0xF88F}, {0x0E4A, 0xF707, 0xF892}, + {0x0E4B, 0xF708, 0xF895}, {0x0E4C, 0xF709, 0xF898}, {0, 0, 0}}; + static const uint16_t sl[][3] = {{0x0E48, 0xF713, 0xF88A}, {0x0E49, 0xF714, 0xF88D}, {0x0E4A, 0xF715, 0xF890}, + {0x0E4B, 0xF716, 0xF893}, {0x0E4C, 0xF717, 0xF896}, {0x0E31, 0xF710, 0xF884}, + {0x0E34, 0xF701, 0xF885}, {0x0E35, 0xF702, 0xF886}, {0x0E36, 0xF703, 0xF887}, + {0x0E37, 0xF704, 0xF888}, {0x0E47, 0xF712, 0xF889}, {0x0E4D, 0xF711, 0xF899}, + {0, 0, 0}}; + static const uint16_t rd[][3] = {{0x0E0D, 0xF70F, 0xF89A}, {0x0E10, 0xF700, 0xF89E}, {0, 0, 0}}; + const uint16_t(*mappings)[3] = action == THAI_SD ? sd : action == THAI_SDL ? sdl : action == THAI_SL ? sl + : action == THAI_RD ? rd : NULL; + if (!mappings) return u; + uint32_t glyph; + for (; mappings[0][0]; mappings++) + if (mappings[0][0] == u) { + if (nominal(s, mappings[0][1], &glyph)) return mappings[0][1]; + if (nominal(s, mappings[0][2], &glyph)) return mappings[0][2]; + break; + } + return u; +} + +static void thai_pua_shaping(const VegaShaping *s, VegaShaper *b) { + /* above: T0..T3; below: B0..B2 (hb-ot-shaper-thai.cc's state machines) */ + static const uint8_t above_start[5] = {0, 1, 0, 0, 3}, below_start[5] = {0, 0, 1, 2, 2}; + static const uint8_t above[4][3][2] = {{{THAI_NOP, 3}, {THAI_NOP, 0}, {THAI_SD, 3}}, + {{THAI_SL, 2}, {THAI_NOP, 1}, {THAI_SDL, 2}}, + {{THAI_NOP, 3}, {THAI_NOP, 2}, {THAI_SL, 3}}, + {{THAI_NOP, 3}, {THAI_NOP, 3}, {THAI_NOP, 3}}}; + static const uint8_t below[3][3][2] = {{{THAI_NOP, 0}, {THAI_NOP, 2}, {THAI_NOP, 0}}, + {{THAI_NOP, 1}, {THAI_RD, 2}, {THAI_NOP, 1}}, + {{THAI_NOP, 2}, {THAI_SD, 2}, {THAI_NOP, 2}}}; + unsigned above_state = above_start[THAI_NOT_CONSONANT], below_state = below_start[THAI_NOT_CONSONANT]; + size_t base = 0; + for (size_t i = 0; i < b->len; ++i) { + unsigned mt = thai_mark_type(b->info[i].codepoint); + if (mt == THAI_NOT_MARK) { + unsigned ct = thai_consonant_type(b->info[i].codepoint); + above_state = above_start[ct]; + below_state = below_start[ct]; + base = i; + continue; + } + const uint8_t *a = above[above_state][mt], *w = below[below_state][mt]; + above_state = a[1]; + below_state = w[1]; + unsigned action = a[0] != THAI_NOP ? a[0] : w[0]; + if (action == THAI_RD) b->info[base].codepoint = thai_pua_shape(s, b->info[base].codepoint, action); + else b->info[i].codepoint = thai_pua_shape(s, b->info[i].codepoint, action); + } +} + +static bool thai_above_base_mark(uint32_t u) { + u &= ~0x80u; + return (u >= 0x0E34 && u <= 0x0E37) || (u >= 0x0E47 && u <= 0x0E4E) || u == 0x0E31 || u == 0x0E3B; +} + +static void preprocess_thai(const VegaShaping *s, const Plan *plan, VegaShaper *b) { + clear_output(b); + size_t count = b->len; + while (b->idx < count && b->successful) { + uint32_t u = b->info[b->idx].codepoint; + if ((u & ~0x80u) != 0x0E33) { /* not SARA AM */ + next_glyph(b); + continue; + } + output_glyph(b, u - 0x0E33 + 0x0E4D); /* NIKHAHIT, continuing the character before */ + if (b->out_len) b->out[b->out_len - 1].uprops |= UPROPS_CONTINUATION; + replace_glyph(b, u - 1); /* SARA AA */ + if (!b->successful) break; + size_t end = b->out_len; + /* The NIKHAHIT counts as a mark (of class 0) when widths are zeroed. */ + b->out[end - 2].uprops = (uint16_t)((b->out[end - 2].uprops & ~0x1Fu) | VEGA_MN); + size_t start = end - 2; + while (start > 0 && thai_above_base_mark(b->out[start - 1].codepoint)) start--; + if (start + 2 < end) { + merge_out_clusters(b, start, end); + Info t = b->out[end - 2]; + memmove(b->out + start + 1, b->out + start, (end - start - 2) * sizeof(Info)); + b->out[start] = t; + } else if (start) + merge_out_clusters(b, start - 1, end); + } + sync(b); + if (plan->shaper_flags & VEGA_THAI_PUA) thai_pua_shaping(s, b); +} + /* The shaper's part of the work at a pause in GSUB; whether it changed the * buffer. */ static bool shaper_pause(const VegaShaping *s, const Plan *plan, VegaShaper *b, unsigned pause) { @@ -3656,6 +3772,7 @@ long vega_shape(VegaShaper *b, const VegaShaping *s, const uint32_t *codes, size ensure_native_direction(b, &plan); if (plan.shaper == VEGA_SHAPER_HANGUL) preprocess_hangul(s, b); if (plan.shaper == VEGA_SHAPER_INDIC || plan.shaper == VEGA_SHAPER_USE) preprocess_vowel_constraints(&plan, b); + if (plan.shaper == VEGA_SHAPER_THAI) preprocess_thai(s, &plan, b); /* Mirror what the font mirrors; mark the rest for rtlm. */ if (direction == VEGA_SHAPE_RTL) diff --git a/text/shape.h b/text/shape.h index bd38d96..6e56819 100644 --- a/text/shape.h +++ b/text/shape.h @@ -6,8 +6,8 @@ /* Shaping (docs/design/004-text.md): a run of characters (one script, one * direction) to glyphs and their positions, as HarfBuzz 10 shapes it with - * its default, Arabic, Hebrew, Hangul, Indic, Khmer, Myanmar and Universal - * (USE) shapers, glyph for glyph and unit for unit. + * its default, Arabic, Hebrew, Hangul, Indic, Khmer, Myanmar, Universal + * (USE) and Thai shapers, glyph for glyph and unit for unit. * * What a font's shaping needs travels in a symbol file (its shaping * section), written by the build tool: @@ -48,7 +48,7 @@ enum { VEGA_SHAPING_HEADER = 80, VEGA_PLAN_BYTES = 144, VEGA_STEP_BYTES = 8, VEG VEGA_PLAN_STAGES = 5 }; enum { VEGA_SHAPE_LTR = 1, VEGA_SHAPE_RTL = 2 }; enum { VEGA_SHAPER_DEFAULT, VEGA_SHAPER_ARABIC, VEGA_SHAPER_HEBREW, VEGA_SHAPER_HANGUL, VEGA_SHAPER_INDIC, - VEGA_SHAPER_KHMER, VEGA_SHAPER_MYANMAR, VEGA_SHAPER_USE }; + VEGA_SHAPER_KHMER, VEGA_SHAPER_MYANMAR, VEGA_SHAPER_USE, VEGA_SHAPER_THAI }; /* Plan flags: GPOS runs; mark advances are zeroed after it (by GDEF); glyph * classes come from Unicode (the font has none); the font positions marks * (a 'mark' feature); automatic fractions; the kern table kerns; marks are @@ -71,8 +71,9 @@ enum { VEGA_MASK_USE_RPHF = VEGA_MASK_INIT + 1 }; /* USE: that, and Arab * position by whether they would substitute it. */ enum { VEGA_STAGE_RPHF, VEGA_STAGE_PREF, VEGA_STAGE_BLWF, VEGA_STAGE_PSTF, VEGA_STAGE_VATU }; /* Shaper flags: Indic's font follows the old specification (deva, not - * dev2). */ -enum { VEGA_INDIC_OLD_SPEC = 1 }; + * dev2); Thai's font has no Thai script of its own, so its marks take the + * font's private-use glyphs where it has them. */ +enum { VEGA_INDIC_OLD_SPEC = 1, VEGA_THAI_PUA = 2 }; /* Step flags; pauses, which the shaper tells apart. */ enum { VEGA_STEP_AUTO_ZWNJ = 1, VEGA_STEP_AUTO_ZWJ = 2, VEGA_STEP_RANDOM = 4, VEGA_STEP_PER_SYLLABLE = 8 }; enum { VEGA_STEP_PAUSE = 0xFFFF }; -- cgit v1.2.3