From 9bb85cffb10e0d4bc007406d1487a6dede3db975 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 28 Sep 2026 14:53:40 +0100 Subject: Shape Khmer and Myanmar as HarfBuzz does Two more shapers on the syllabic groundwork, from HarfBuzz 10.2's source. Khmer reorders before any lookup: a coeng and Ro go before the base, masked for pref, what follows them for cfar, a left matra goes first; split matras decompose with U+17C1. Myanmar finds the base after a kinzi, gives each glyph its place around it and sorts by place, and zeroes mark advances before GPOS rather than after. Noto Sans Khmer and Noto Sans Myanmar shape as HarfBuzz does at 20,000 runs a plan, and so do Unifont's Khmer and Myanmar (no GSUB). --- docs/design/004-text.md | 13 +++- text/shape-build.c | 64 +++++++++++++--- text/shape.c | 194 +++++++++++++++++++++++++++++++++++++++++++----- text/shape.h | 9 ++- 4 files changed, 246 insertions(+), 34 deletions(-) diff --git a/docs/design/004-text.md b/docs/design/004-text.md index 065c4db..a681589 100644 --- a/docs/design/004-text.md +++ b/docs/design/004-text.md @@ -165,11 +165,22 @@ Malayalam은 psts와 rlig가 한 조회를 나눠 쓰는데, 안정 정렬로는 - 새 규격 글꼴(dev2 등): 인터넷에서 받은 Noto Sans 아홉 벌(데바나가리부터 말라얄람까지), 계획마다 5천 실행, 다른 실행 0. - 속도: HarfBuzz의 1.25–1.3배 시간(Noto Sans Devanagari 문자당 1,614 대 1,235 ns, Tamil 674 대 526 ns). +### 크메르와 미얀마 + +같은 틀에 두 셰이퍼를 더 올렸다(HarfBuzz 10.2의 `hb-ot-shaper-khmer.cc`, `-myanmar.cc`). 음절 기계는 앞의 생성기가 이미 +만들고 검증한 것이고, 범주는 같은 인도계 표에서 읽는다. 크메르는 음절을 찾은 뒤 조회 전에 재배치한다. coeng과 Ro를 기저 +앞으로 옮겨 pref로 표시하고 그 뒤를 cfar로 표시하며, 왼쪽 모음 부호를 맨 앞으로 옮긴다. 쪼개진 모음(U+17BE 등)은 유니코드에 +분해가 없어도 U+17C1과 함께 쪼갠다. 미얀마는 kinzi(Ra, asat, virama) 뒤의 기저를 찾고, 글리프마다 기저 둘레의 자리를 매겨 +안정 정렬한다. 미얀마는 부호의 폭을 GPOS 앞에서 0으로 만든다(다른 셰이퍼는 뒤에서). + +결과: Noto Sans Khmer와 Noto Sans Myanmar(인터넷에서 받은 OpenType 글꼴), 계획마다 2만 실행, 다른 실행 0. Unifont(GSUB +없음)의 두 문자도 0. 처음 돌린 검사에서 바로 맞았다. 음절 기계와 표를 HarfBuzz 원본에서 그대로 가져온 덕이다. + 받지 않는 것(빌드 도구가 이유를 말하고 거부한다): - `kern` 표의 상태 기계(Apple 형식 1), 등급 형식(2, 3), 교차 방향 커닝. 설치된 글꼴의 `kern` 표는 모두 짝 형식(0)이다. - AAT 표(morx, kerx, trak), 늘이기(stch), 아랍 형태가 없어 HarfBuzz가 표시형으로 대체 셰이핑을 하는 글꼴(Unifont의 아랍). -- 기본·아랍·히브리·한글·인도계 셰이퍼가 아닌 문자 체계: 태국·라오, 크메르, 미얀마, USE 문자 체계들(싱할라, 티베트, 자바 +- 기본·아랍·히브리·한글·인도계·크메르·미얀마 셰이퍼가 아닌 문자 체계: 태국·라오, USE 문자 체계들(싱할라, 티베트, 자바 등), 시리아. - 이체 선택자(cmap 형식 14)는 싣지 않는다. 선택자는 숨기고 앞 글자는 기본 글리프로 둔다. diff --git a/text/shape-build.c b/text/shape-build.c index 559517e..166ad22 100644 --- a/text/shape-build.c +++ b/text/shape-build.c @@ -331,13 +331,12 @@ static int categorize(uint32_t script, hb_tag_t gsub_script) { if (script == TAG("Arab")) return VEGA_SHAPER_ARABIC; if (script == TAG("Hebr")) return VEGA_SHAPER_HEBREW; if (script == TAG("Hang")) return VEGA_SHAPER_HANGUL; - if (script == TAG("Syrc") || script == TAG("Thai") || script == TAG("Laoo") || script == TAG("Khmr") || - script == TAG("Qaag")) - return -1; + if (script == TAG("Khmr")) return VEGA_SHAPER_KHMER; + if (script == TAG("Syrc") || script == TAG("Thai") || script == TAG("Laoo") || 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' ? -1 : VEGA_SHAPER_INDIC; if (listed(use_scripts, sizeof(use_scripts) / sizeof(*use_scripts), script)) return dflt ? VEGA_SHAPER_DEFAULT : -1; - if (script == TAG("Mymr")) return dflt || gsub_script == TAG("mymr") ? VEGA_SHAPER_DEFAULT : -1; + if (script == TAG("Mymr")) return dflt || gsub_script == TAG("mymr") ? VEGA_SHAPER_DEFAULT : VEGA_SHAPER_MYANMAR; return VEGA_SHAPER_DEFAULT; } @@ -515,6 +514,37 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan * add_gsub_pause(m, VEGA_PAUSE_FINAL_REORDERING); for (int i = 11; i < 17; ++i) add_feature(m, TAG(indic_features[i].tag), indic_features[i].flags, 1); } + /* hb-ot-shaper-khmer.cc: syllables and their reordering before any + * lookup; the basic features together, within syllables; the rest. */ + static const char *const khmer_basic[] = {"pref", "blwf", "abvf", "pstf", "cfar"}; + static const char *const khmer_other[] = {"pres", "abvs", "blws", "psts"}; + if (shaper == VEGA_SHAPER_KHMER) { + m->is_simple = false; + add_gsub_pause(m, VEGA_PAUSE_SYLLABLES); + add_gsub_pause(m, VEGA_PAUSE_INITIAL_REORDERING); + enable_feature(m, TAG("locl"), F_PER_SYLLABLE, 1); + enable_feature(m, TAG("ccmp"), F_PER_SYLLABLE, 1); + for (int i = 0; i < 5; ++i) add_feature(m, TAG(khmer_basic[i]), F_MANUAL_ZWNJ | F_MANUAL_ZWJ | F_PER_SYLLABLE, 1); + add_gsub_pause(m, VEGA_PAUSE_CLEAR_SYLLABLES); + for (int i = 0; i < 4; ++i) add_feature(m, TAG(khmer_other[i]), F_GLOBAL | F_MANUAL_ZWNJ | F_MANUAL_ZWJ, 1); + } + /* hb-ot-shaper-myanmar.cc: syllables; locl and ccmp; the reordering; + * the basic features one stage each; the rest. */ + static const char *const myanmar_basic[] = {"rphf", "pref", "blwf", "pstf"}; + static const char *const myanmar_other[] = {"pres", "abvs", "blws", "psts"}; + if (shaper == VEGA_SHAPER_MYANMAR) { + m->is_simple = false; + add_gsub_pause(m, VEGA_PAUSE_SYLLABLES); + enable_feature(m, TAG("locl"), F_PER_SYLLABLE, 1); + enable_feature(m, TAG("ccmp"), F_PER_SYLLABLE, 1); + add_gsub_pause(m, VEGA_PAUSE_INITIAL_REORDERING); + for (int i = 0; i < 4; ++i) { + enable_feature(m, TAG(myanmar_basic[i]), F_MANUAL_ZWJ | F_PER_SYLLABLE, 1); + add_gsub_pause(m, 0); + } + add_gsub_pause(m, VEGA_PAUSE_CLEAR_SYLLABLES); + for (int i = 0; i < 4; ++i) enable_feature(m, TAG(myanmar_other[i]), F_MANUAL_ZWJ, 1); + } if (shaper == VEGA_SHAPER_HANGUL) { m->is_simple = false; add_feature(m, TAG("ljmo"), F_NONE, 1); @@ -535,6 +565,11 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan * add_feature(m, TAG("liga"), F_GLOBAL, 0); add_gsub_pause(m, VEGA_PAUSE_CLEAR_SYLLABLES); } + /* Khmer asks for clig, and turns liga off. */ + if (shaper == VEGA_SHAPER_KHMER) { + enable_feature(m, TAG("clig"), F_NONE, 1); + add_feature(m, TAG("liga"), F_GLOBAL, 0); + } if (!compile_map(m)) FAIL("out of memory, or more than %d stages", MAX_STAGES); /* hb_ot_shape_planner_t::compile */ @@ -545,9 +580,10 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan * bool apply_kern = (!has_gpos_kern || !apply_gpos) && kern_table.data; if (apply_kern && (kern_table.other || kern_table.cross)) FAIL("%s: the font's kern table has more than pairs (state machines, classes, cross-stream kerning)", name); - /* Hangul's and Indic's shapers neither zero marks nor place them - * without GPOS. */ - bool script_marks = shaper != VEGA_SHAPER_HANGUL && shaper != VEGA_SHAPER_INDIC; + /* 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; 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); @@ -563,7 +599,7 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan * uint32_t frac = one_mask(m, "frac"), numr = one_mask(m, "numr"), dnom = one_mask(m, "dnom"); *plan = (Plan){script_tag, direction, native == HB_DIRECTION_LTR ? VEGA_SHAPE_LTR : native == HB_DIRECTION_RTL ? VEGA_SHAPE_RTL : 0, shaper, - (apply_gpos ? VEGA_PLAN_GPOS : 0) | (script_marks ? VEGA_PLAN_ZERO_MARKS : 0) | + (apply_gpos ? VEGA_PLAN_GPOS : 0) | (zeroes ? VEGA_PLAN_ZERO_MARKS : 0) | (hb_ot_layout_has_glyph_classes(face) ? 0 : VEGA_PLAN_GUESS_CLASSES) | (one_mask(m, "mark") ? VEGA_PLAN_GPOS_MARK : 0) | (frac || (numr && dnom) ? VEGA_PLAN_FRACTIONS : 0) | (apply_kern ? VEGA_PLAN_KERN : 0) | (fallback_marks ? VEGA_PLAN_FALLBACK_MARKS : 0) | @@ -589,6 +625,8 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan * for (int i = 0; i < VEGA_PLAN_STAGES; ++i) plan->stages[i] = stage_of(m, staged[i]); if ((m->chosen_script[0] & 0xFF) != '2') plan->shaper_flags |= VEGA_INDIC_OLD_SPEC; } + 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]); free(m); return true; fail: @@ -937,7 +975,8 @@ void vega_shaping_check(hb_font_t *font, const VegaShaping *s, uint32_t script, * punctuation), and those whose glyphs some substitution starts from. */ Pool letters = {0}, marks = {0}, others = {0}, hot = {0}, jamo_pools[3] = {{0}}, indic[INDIC_POOLS] = {{0}}; bool hangul = script == HB_TAG('H', 'a', 'n', 'g'); - bool syllabic = listed(indic_scripts, sizeof(indic_scripts) / sizeof(*indic_scripts), script); + bool syllabic = listed(indic_scripts, sizeof(indic_scripts) / sizeof(*indic_scripts), script) || + script == HB_TAG('K', 'h', 'm', 'r') || script == HB_TAG('M', 'y', 'm', 'r'); hb_set_t *inputs = hb_set_create(); uint32_t common = HB_TAG('Z', 'y', 'y', 'y'), inherited = HB_TAG('Z', 'i', 'n', 'h'); for (unsigned t = 0; t < s->lookup_count[0]; ++t) @@ -958,8 +997,11 @@ void vega_shaping_check(hb_font_t *font, const VegaShaping *s, uint32_t script, case VEGA_OT_RA: pool = INDIC_RA; break; case VEGA_OT_H: pool = INDIC_HALANTS; break; case VEGA_OT_N: pool = INDIC_NUKTAS; break; - case VEGA_OT_M: case VEGA_OT_MPST: pool = INDIC_MATRAS; break; - case VEGA_OT_SM: case VEGA_OT_A: pool = INDIC_MODIFIERS; break; + case VEGA_OT_M: case VEGA_OT_MPST: case VEGA_OT_VABV: case VEGA_OT_VBLW: case VEGA_OT_VPRE: + case VEGA_OT_VPST: pool = INDIC_MATRAS; break; + case VEGA_OT_SM: case VEGA_OT_A: case VEGA_OT_SMPST: case VEGA_OT_ROBATIC: case VEGA_OT_XGROUP: + case VEGA_OT_YGROUP: case VEGA_OT_AS: case VEGA_OT_MH: case VEGA_OT_MR: case VEGA_OT_MW: case VEGA_OT_MY: + case VEGA_OT_ML: case VEGA_OT_PT: case VEGA_OT_VS: pool = INDIC_MODIFIERS; break; case VEGA_OT_V: pool = INDIC_VOWELS; break; } if (pool >= 0) pool_add(&indic[pool], code); diff --git a/text/shape.c b/text/shape.c index 3443b0b..214d73b 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_INDIC) + if ((p[4] != VEGA_SHAPE_LTR && p[4] != VEGA_SHAPE_RTL) || p[5] > VEGA_SHAPE_RTL || p[6] > VEGA_SHAPER_MYANMAR) 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) { @@ -823,6 +823,7 @@ static bool compose(const Normalizer *c, uint32_t a, uint32_t b, uint32_t *ab) { switch (c->plan->shaper) { case VEGA_SHAPER_HEBREW: return compose_hebrew(c, a, b, ab); case VEGA_SHAPER_INDIC: return compose_indic(a, b, ab); + case VEGA_SHAPER_KHMER: return !is_mark_category(vega_category(a)) && compose_unicode(a, b, ab); default: return compose_unicode(a, b, ab); } } @@ -845,6 +846,13 @@ static void next_char(VegaShaper *b, uint32_t glyph) { static bool decompose_char(const Normalizer *c, uint32_t ab, uint32_t *a, uint32_t *b) { if (c->plan->shaper == VEGA_SHAPER_INDIC && (ab == 0x0931 || ab == 0x09DC || ab == 0x09DD || ab == 0x0B94)) return false; + /* Khmer's split matras, which Unicode does not decompose */ + if (c->plan->shaper == VEGA_SHAPER_KHMER && + (ab == 0x17BE || ab == 0x17BF || ab == 0x17C0 || ab == 0x17C4 || ab == 0x17C5)) { + *a = 0x17C1; + *b = ab; + return true; + } return decompose_unicode(ab, a, b); } @@ -1026,7 +1034,9 @@ static void normalize(const VegaShaping *s, const Plan *plan, VegaShaper *b) { * none: what the font has stays, and nothing is composed again; * Indic's decomposes what decomposes even when the font has it whole. */ const bool none = plan->shaper == VEGA_SHAPER_HANGUL; - const bool always_short_circuit = none, might_short_circuit = none || plan->shaper != VEGA_SHAPER_INDIC; + const bool syllabic = plan->shaper == VEGA_SHAPER_INDIC || plan->shaper == VEGA_SHAPER_KHMER || + plan->shaper == VEGA_SHAPER_MYANMAR; + const bool always_short_circuit = none, might_short_circuit = none || !syllabic; bool all_simple = true; clear_output(b); size_t count = b->len; @@ -2574,11 +2584,12 @@ static size_t next_syllable(const VegaShaper *b, size_t start) { /* A dotted circle at the start of each broken syllable (after a repha that * starts it), for its marks to sit on; false when none went in. */ static bool insert_dotted_circles(const VegaShaping *s, VegaShaper *b, unsigned broken, unsigned category, int repha, - unsigned position) { + int position) { uint32_t glyph; if (!(b->scratch & HAS_BROKEN_SYLLABLE) || !nominal(s, 0x25CC, &glyph)) return false; /* All of it zero but these: HarfBuzz's is a fresh glyph_info. */ - Info circle = {.codepoint = glyph, .uprops = VEGA_CC, .position = (uint8_t)position, .category = (uint8_t)category}; + Info circle = {.codepoint = glyph, .uprops = VEGA_CC, .position = (uint8_t)(position >= 0 ? position : 0), + .category = (uint8_t)category}; clear_output(b); unsigned last = 0; while (b->idx < b->len && b->successful) { @@ -3186,15 +3197,153 @@ static void indic_final_reordering(const Indic *d, VegaShaper *b) { } } +/* ---- Khmer (hb-ot-shaper-khmer.cc): a coeng and Ro moved before the base + * and masked for pref, what follows them for cfar; a left matra moved + * first; the post-base features on all but the first glyph. */ + +static void setup_masks_category(VegaShaper *b) { + for (size_t i = 0; i < b->len; ++i) b->info[i].category = vega_indic_categories(b->info[i].codepoint) & 0xFF; +} + +static void khmer_reorder_syllable(const Plan *plan, VegaShaper *b, size_t start, size_t end) { + Info *info = b->info; + const uint32_t *masks = plan->masks; + uint32_t post = masks[VEGA_MASK_KHMER_BLWF] | masks[VEGA_MASK_KHMER_ABVF] | masks[VEGA_MASK_KHMER_PSTF]; + for (size_t i = start + 1; i < end; ++i) info[i].mask |= post; + unsigned coengs = 0; + for (size_t i = start + 1; i < end; ++i) + if (info[i].category == VEGA_OT_H && coengs <= 2 && i + 1 < end) { + coengs++; + if (info[i + 1].category == VEGA_OT_RA) { + info[i].mask |= masks[VEGA_MASK_KHMER_PREF]; + info[i + 1].mask |= masks[VEGA_MASK_KHMER_PREF]; + merge_clusters(b, start, i + 2); + Info t0 = info[i], t1 = info[i + 1]; + memmove(&info[start + 2], &info[start], (i - start) * sizeof(Info)); + info[start] = t0; + info[start + 1] = t1; + if (masks[VEGA_MASK_KHMER_CFAR]) + for (size_t j = i + 2; j < end; ++j) info[j].mask |= masks[VEGA_MASK_KHMER_CFAR]; + coengs = 2; + } + } else if (info[i].category == VEGA_OT_VPRE) { + merge_clusters(b, start, i + 1); + Info t = info[i]; + memmove(&info[start + 1], &info[start], (i - start) * sizeof(Info)); + info[start] = t; + } +} + +static bool khmer_reorder(const VegaShaping *s, const Plan *plan, VegaShaper *b) { + bool changed = insert_dotted_circles(s, b, 1, VEGA_OT_DOTTEDCIRCLE, -1, -1); + for (size_t start = 0, end; start < b->len; start = end) { + end = next_syllable(b, start); + if ((b->info[start].syllable & 0x0F) <= 1) khmer_reorder_syllable(plan, b, start, end); + } + return changed; +} + +/* ---- Myanmar (hb-ot-shaper-myanmar.cc): the base (after a kinzi), each + * glyph given its place around it, and the syllable sorted by place. */ + +#define CONSONANT_FLAGS_MYANMAR \ + (FLAG(VEGA_OT_C) | FLAG(VEGA_OT_CS) | FLAG(VEGA_OT_RA) | FLAG(VEGA_OT_V) | FLAG(VEGA_OT_PLACEHOLDER) | \ + FLAG(VEGA_OT_DOTTEDCIRCLE)) + +/* hb_buffer_t::sort by position: stable; a glyph that moves back merges + * the clusters it passes. */ +static void sort_by_position(VegaShaper *b, size_t start, size_t end) { + for (size_t i = start + 1; i < end; ++i) { + size_t j = i; + while (j > start && b->info[j - 1].position > b->info[i].position) j--; + if (i == j) continue; + merge_clusters(b, j, i + 1); + Info t = b->info[i]; + memmove(&b->info[j + 1], &b->info[j], (i - j) * sizeof(Info)); + b->info[j] = t; + } +} + +static void myanmar_reorder_syllable(VegaShaper *b, size_t start, size_t end) { + Info *info = b->info; + size_t base = end, limit = start; + bool has_reph = false; + if (start + 3 <= end && info[start].category == VEGA_OT_RA && info[start + 1].category == VEGA_OT_AS && + info[start + 2].category == VEGA_OT_H) { + limit += 3; + base = start; + has_reph = true; + } + if (!has_reph) base = limit; + for (size_t i = limit; i < end; ++i) + if (is_one_of(&info[i], CONSONANT_FLAGS_MYANMAR)) { + base = i; + break; + } + size_t i = start; + for (; i < start + (has_reph ? 3 : 0); ++i) info[i].position = VEGA_POS_AFTER_MAIN; + for (; i < base; ++i) info[i].position = VEGA_POS_PRE_C; + if (i < end) info[i++].position = VEGA_POS_BASE_C; + uint8_t pos = VEGA_POS_AFTER_MAIN; + for (; i < end; ++i) { + unsigned c = info[i].category; + if (c == VEGA_OT_MR) info[i].position = VEGA_POS_PRE_C; /* pre-base reordering */ + else if (c == VEGA_OT_VPRE) info[i].position = VEGA_POS_PRE_M; /* left matra */ + else if (c == VEGA_OT_VS) info[i].position = info[i - 1].position; + else if (pos == VEGA_POS_AFTER_MAIN && c == VEGA_OT_VBLW) info[i].position = pos = VEGA_POS_BELOW_C; + else if (pos == VEGA_POS_BELOW_C && c == VEGA_OT_A) info[i].position = VEGA_POS_BEFORE_SUB; + else if (pos == VEGA_POS_BELOW_C && c == VEGA_OT_VBLW) info[i].position = pos; + else if (pos == VEGA_POS_BELOW_C) info[i].position = pos = VEGA_POS_AFTER_SUB; + else info[i].position = pos; + } + sort_by_position(b, start, end); + /* A run of left matras back in their order (variation selectors after + * theirs). */ + size_t first_left_matra = end, last_left_matra = end; + for (size_t k = start; k < end; ++k) + if (info[k].position == VEGA_POS_PRE_M) { + if (first_left_matra == end) first_left_matra = k; + last_left_matra = k; + } + if (first_left_matra < last_left_matra) { + reverse_range(b, first_left_matra, last_left_matra + 1, false); + size_t k = first_left_matra; + for (size_t j = k; j <= last_left_matra; ++j) + if (info[j].category == VEGA_OT_VPRE) { + reverse_range(b, k, j + 1, false); + k = j + 1; + } + } +} + +static bool myanmar_reorder(const VegaShaping *s, VegaShaper *b) { + bool changed = insert_dotted_circles(s, b, 1, VEGA_OT_DOTTEDCIRCLE, -1, -1); + for (size_t start = 0, end; start < b->len; start = end) { + end = next_syllable(b, start); + if ((b->info[start].syllable & 0x0F) <= 1) myanmar_reorder_syllable(b, start, end); + } + return changed; +} + /* 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) { - if (plan->shaper != VEGA_SHAPER_INDIC) return false; - Indic d = indic_of(s, plan); - switch (pause) { - case VEGA_PAUSE_SYLLABLES: find_syllables(b, &vega_indic_machine); return false; - case VEGA_PAUSE_INITIAL_REORDERING: return indic_initial_reordering(&d, b); - case VEGA_PAUSE_FINAL_REORDERING: indic_final_reordering(&d, b); return false; + switch (plan->shaper) { + case VEGA_SHAPER_INDIC: { + Indic d = indic_of(s, plan); + switch (pause) { + case VEGA_PAUSE_SYLLABLES: find_syllables(b, &vega_indic_machine); return false; + case VEGA_PAUSE_INITIAL_REORDERING: return indic_initial_reordering(&d, b); + case VEGA_PAUSE_FINAL_REORDERING: indic_final_reordering(&d, b); return false; + default: return false; + } + } + case VEGA_SHAPER_KHMER: + if (pause == VEGA_PAUSE_SYLLABLES) find_syllables(b, &vega_khmer_machine); + return pause == VEGA_PAUSE_INITIAL_REORDERING && khmer_reorder(s, plan, b); + case VEGA_SHAPER_MYANMAR: + if (pause == VEGA_PAUSE_SYLLABLES) find_syllables(b, &vega_myanmar_machine); + return pause == VEGA_PAUSE_INITIAL_REORDERING && myanmar_reorder(s, b); default: return false; } } @@ -3256,6 +3405,17 @@ static void propagate_attachment_offsets(Position *pos, size_t len, size_t i, in } } +static void zero_mark_widths(VegaShaper *b, bool adjust) { + for (size_t i = 0; i < b->len; ++i) + if (b->info[i].gprops & MARK) { + if (adjust) { + b->pos[i].x_offset -= b->pos[i].x_advance; + b->pos[i].y_offset -= b->pos[i].y_advance; + } + b->pos[i].x_advance = b->pos[i].y_advance = 0; + } +} + static void position(const VegaShaping *s, const Plan *plan, VegaShaper *b) { b->have_output = false; b->out_len = 0; @@ -3263,17 +3423,12 @@ static void position(const VegaShaping *s, const Plan *plan, VegaShaper *b) { for (size_t i = 0; i < b->len; ++i) b->pos[i].x_advance = advance_of(s, b->info[i].codepoint); if (b->scratch & HAS_SPACE_FALLBACK) fallback_spaces(s, b); bool adjust = (plan->flags & VEGA_PLAN_ADJUST_ZEROING) && b->direction == VEGA_SHAPE_LTR; + /* Marks' advances go by GDEF: before GPOS (Myanmar's shaper) or after. */ + bool early = plan->shaper == VEGA_SHAPER_MYANMAR; + if ((plan->flags & VEGA_PLAN_ZERO_MARKS) && early) zero_mark_widths(b, adjust); if (plan->flags & VEGA_PLAN_GPOS) apply_table(s, plan, b, 1); if (plan->flags & VEGA_PLAN_KERN) kern_table(s, b, plan); - if (plan->flags & VEGA_PLAN_ZERO_MARKS) - for (size_t i = 0; i < b->len; ++i) - if (b->info[i].gprops & MARK) { - if (adjust) { - b->pos[i].x_offset -= b->pos[i].x_advance; - b->pos[i].y_offset -= b->pos[i].y_advance; - } - b->pos[i].x_advance = b->pos[i].y_advance = 0; - } + if ((plan->flags & VEGA_PLAN_ZERO_MARKS) && !early) zero_mark_widths(b, adjust); if (b->scratch & HAS_DEFAULT_IGNORABLES) for (size_t i = 0; i < b->len; ++i) if (is_default_ignorable(&b->info[i])) @@ -3318,6 +3473,7 @@ long vega_shape(VegaShaper *b, const VegaShaping *s, const uint32_t *codes, size setup_masks_fraction(b, &plan); if (plan.shaper == VEGA_SHAPER_ARABIC) setup_masks_arabic(b, &plan); if (plan.shaper == VEGA_SHAPER_INDIC) setup_masks_indic(b); + if (plan.shaper == VEGA_SHAPER_KHMER || plan.shaper == VEGA_SHAPER_MYANMAR) setup_masks_category(b); if (plan.shaper == VEGA_SHAPER_HANGUL) for (size_t i = 0; i < b->len; ++i) if (b->info[i].action >= LJMO && b->info[i].action <= TJMO) diff --git a/text/shape.h b/text/shape.h index 7b12009..7f02729 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 and Indic shapers, glyph for glyph and - * unit for unit. + * its default, Arabic, Hebrew, Hangul, Indic, Khmer and Myanmar 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: @@ -47,7 +47,8 @@ enum { VEGA_SHAPING_HEADER = 80, VEGA_PLAN_BYTES = 144, VEGA_STEP_BYTES = 8, VEGA_PLAN_MASKS = 16, 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 }; +enum { VEGA_SHAPER_DEFAULT, VEGA_SHAPER_ARABIC, VEGA_SHAPER_HEBREW, VEGA_SHAPER_HANGUL, VEGA_SHAPER_INDIC, + VEGA_SHAPER_KHMER, VEGA_SHAPER_MYANMAR }; /* 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 @@ -63,6 +64,8 @@ enum { VEGA_MASK_ISOL = VEGA_MASK_SHAPER, VEGA_MASK_FINA, VEGA_MASK_FIN2, VEGA_M enum { VEGA_MASK_LJMO = VEGA_MASK_SHAPER, VEGA_MASK_VJMO, VEGA_MASK_TJMO }; /* Hangul */ enum { VEGA_MASK_RPHF = VEGA_MASK_SHAPER, VEGA_MASK_PREF, VEGA_MASK_BLWF, VEGA_MASK_ABVF, VEGA_MASK_HALF, VEGA_MASK_PSTF, VEGA_MASK_INDIC_INIT }; /* Indic */ +enum { VEGA_MASK_KHMER_PREF = VEGA_MASK_SHAPER, VEGA_MASK_KHMER_BLWF, VEGA_MASK_KHMER_ABVF, VEGA_MASK_KHMER_PSTF, + VEGA_MASK_KHMER_CFAR }; /* Khmer */ /* The stages a plan names: Indic's, whose features decide a consonant's * position by whether they would substitute it. */ enum { VEGA_STAGE_RPHF, VEGA_STAGE_PREF, VEGA_STAGE_BLWF, VEGA_STAGE_PSTF, VEGA_STAGE_VATU }; -- cgit v1.2.3