diff options
| -rw-r--r-- | docs/design/004-text.md | 19 | ||||
| -rw-r--r-- | text/shape-build.c | 12 | ||||
| -rw-r--r-- | text/shape.c | 119 | ||||
| -rw-r--r-- | text/shape.h | 11 |
4 files changed, 147 insertions, 14 deletions
diff --git a/docs/design/004-text.md b/docs/design/004-text.md index ec681e1..c62ff65 100644 --- a/docs/design/004-text.md +++ b/docs/design/004-text.md @@ -159,9 +159,9 @@ Malayalam은 psts와 rlig가 한 조회를 나눠 쓰는데, 안정 정렬로는 결과(Pi 5): -- 설치된 글꼴 213벌, 계획 1,748개에 각 1천 실행: 다른 실행 0(글자 모양이 비트맵뿐인 Noto Color Emoji는 심볼 단계에서 - 빠진다). 인터넷에서 받은 Noto 21벌(인도계 아홉, 싱할라, 크메르, 미얀마, 티베트, 자바, 발리, 몽골 등), 계획 80개에 각 3천 - 실행: 다른 실행 0. 인도계가 있는 글꼴은 따로 계획마다 2만 실행: FreeSerif(여덟 문자), FreeSans, 굵은 꼴들, FreeMono, Unifont(아홉 +- 설치된 글꼴 213벌, 계획 1,804개에 각 1천 실행: 다른 실행 0(글자 모양이 비트맵뿐인 Noto Color Emoji는 심볼 단계에서 + 빠진다). 인터넷에서 받은 Noto 21벌(인도계 아홉, 싱할라, 크메르, 미얀마, 티베트, 자바, 발리, 몽골, 태국, 라오 등), 계획 + 84개에 각 2천 실행: 다른 실행 0. 인도계가 있는 글꼴은 따로 계획마다 2만 실행: FreeSerif(여덟 문자), FreeSans, 굵은 꼴들, FreeMono, Unifont(아홉 문자, GSUB 없음) 130개 계획 260만 실행, 다른 실행 0. - 새 규격 글꼴(dev2 등): 인터넷에서 받은 Noto Sans 아홉 벌(데바나가리부터 말라얄람까지), 계획마다 5천 실행, 다른 실행 0. - 속도: HarfBuzz의 1.25–1.3배 시간(Noto Sans Devanagari 문자당 1,614 대 1,235 ns, Tamil 674 대 526 ns). @@ -197,11 +197,22 @@ Malayalam은 psts와 rlig가 한 조회를 나눠 쓰는데, 안정 정렬로는 계획마다 5천 실행, 다른 실행 0. Unifont의 몽골에서 한 번 달랐다. HarfBuzz 10.2의 기본 무시 문자 목록은 몽골 블록에서 U+180B–180E만 넣고 네 번째 자유 이체 선택자(U+180F)를 빠뜨렸다. Vega는 유니코드대로 넣었었는데, HarfBuzz를 따라 뺐다. +### 태국과 라오 + +`hb-ot-shaper-thai.cc`: SARA AM을 NIKHAHIT와 SARA AA로 풀고 NIKHAHIT를 위 부호들 앞으로 옮긴다(NIKHAHIT는 폭을 없앨 때 +부호로 셈한다). 글꼴에 태국 문자 체계가 없으면(GSUB가 DFLT나 latn만 가지면) 부호를 쌓임에 따라 글꼴의 사용자 영역 +글리프(Windows의 F700대, 없으면 Mac의 F880대)로 바꾼다. 계획기가 HarfBuzz처럼 글꼴이 문자 체계를 제 태그로 가졌는지 +기록한다. + +결과: Noto Sans Thai·Lao, TLWG Loma 네 벌, FreeSerif 계열의 태국, DejaVu Sans 계열의 라오, Unifont, 계획마다 3천 실행, 다른 +실행 0. 사용자 영역 경로는 설치된 글꼴에 쓸 일이 없어서, Loma에서 GSUB의 thai 문자 체계만 지운 글꼴을 만들어 5천 실행으로 +확인했다(다른 실행 0). + 받지 않는 것(빌드 도구가 이유를 말하고 거부한다): - `kern` 표의 상태 기계(Apple 형식 1), 등급 형식(2, 3), 교차 방향 커닝. 설치된 글꼴의 `kern` 표는 모두 짝 형식(0)이다. - AAT 표(morx, kerx, trak), 늘이기(stch), 아랍 형태가 없어 HarfBuzz가 표시형으로 대체 셰이핑을 하는 글꼴(Unifont의 아랍). -- 기본·아랍·히브리·한글·인도계·크메르·미얀마·USE 셰이퍼가 아닌 문자 체계: 태국·라오, 시리아. +- 시리아 문자(아랍 셰이퍼의 Alaph·Dalath-Rish 연결과 fin2·fin3·med2), 미얀마 Zawgyi. - 이체 선택자(cmap 형식 14)는 싣지 않는다. 선택자는 숨기고 앞 글자는 기본 글리프로 둔다. ## 비례폭 글줄 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 }; |
