aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 13:54:31 +0100
committereunhhu <[email protected]>2026-09-28 13:54:31 +0100
commit88e16d6f971287afd0ab15d1cde73608cde43b1b (patch)
tree28a3108bb10d1b5de9e9c5a3ca50b73660d82155
parenta914fce69d3f2b12422d214380bfe6a086a7718c (diff)
downloadvega-88e16d6f971287afd0ab15d1cde73608cde43b1b.tar.gz
vega-88e16d6f971287afd0ab15d1cde73608cde43b1b.zip
Shape Hangul, kern with kern tables, and place marks without GPOS, as HarfBuzz does
The planner now picks who positions as HarfBuzz 10.2 does: GPOS, the old kern table when GPOS has no kern feature, and marks placed by combining class and ink boxes when there is no GPOS; the Hangul shaper composes syllables the font has whole, takes apart those it lacks into jamo for ljmo, vjmo and tjmo, and moves tone marks before their syllable. The section grows to an 80-byte header (the kern table, and each glyph's ink box when a plan places marks without GPOS) and 88-byte plans (the kern mask, the jamo masks). Kern tables are applied pair subtable by pair subtable with HarfBuzz's own binary search; state machines, class subtables and cross-stream kerning, which no installed font has, are refused. The check spells Hangul syllables every way text does. Every installed font Vega accepts shapes as HarfBuzz does (209 fonts, 1,520 plans, a thousand runs each), and the fonts refused before at 20,000 runs a plan: Noto Sans CJK KR and Baekmuk for Hangul, IPA Gothic and Noto Mono for marks without GPOS, FreeSans, FreeSerif, Liberation and DejaVu Sans ExtraLight for kern tables, Unifont's Latin, Greek, Cyrillic, Hebrew and Hangul.
-rw-r--r--docs/design/004-text.md37
-rw-r--r--text/shape-build.c160
-rw-r--r--text/shape-build.h11
-rw-r--r--text/shape.c436
-rw-r--r--text/shape.h31
5 files changed, 625 insertions, 50 deletions
diff --git a/docs/design/004-text.md b/docs/design/004-text.md
index 0a54f3e..6c8e09e 100644
--- a/docs/design/004-text.md
+++ b/docs/design/004-text.md
@@ -84,23 +84,31 @@ Sentence_Break, 동아시아 폭이 F·W·H인 여는·닫는 괄호)와 규칙�
이런 문맥은 규칙 자체를 옮기지 않고는 정확히 재현할 수 없다. 그래서 나눴다.
- 빌드할 때: 계획을 컴파일한다. 문자 체계와 방향마다 HarfBuzz의 계획기가 하는 일을 그대로 한다. 기능을 모으고(기본, 아랍,
- 히브리 셰이퍼의 목록과 단계), 중복을 합치고, 마스크 비트를 나누고, 글꼴의 문자 체계·언어 체계에서 기능을 찾고, 단계별로
- 조회를 모아 정렬해 합친다. 결과는 조회 번호, 마스크, ZWJ/ZWNJ 규칙의 목록이다. 글꼴의 문자 지도와 advance, GDEF·GSUB·GPOS
- 표는 그대로 싣는다([text/shape.h](../../text/shape.h), [text/shape-build.c](../../text/shape-build.c)).
+ 히브리, 한글 셰이퍼의 목록과 단계), 중복을 합치고, 마스크 비트를 나누고, 글꼴의 문자 체계·언어 체계에서 기능을 찾고,
+ 단계별로 조회를 모아 정렬해 합친다. 누가 자리를 잡는지도 여기서 정한다. GPOS, 옛 `kern` 표(GPOS에 `kern` 기능이 없을 때),
+ 부호의 대체 배치(GPOS가 없을 때) 가운데 무엇을 쓸지, 부호의 폭을 0으로 만들며 오프셋을 옮길지를 HarfBuzz의 조건 그대로
+ 고른다. 결과는 조회 번호, 마스크, ZWJ/ZWNJ 규칙, 이 깃발들의 목록이다. 글꼴의 문자 지도와 advance, GDEF·GSUB·GPOS와 `kern`
+ 표는 그대로 싣고, 대체 배치를 하는 계획이 있으면 글리프마다 외곽 상자(HarfBuzz의 glyph extents, 16비트 넷)를 싣는다
+ ([text/shape.h](../../text/shape.h), [text/shape-build.c](../../text/shape-build.c)).
- 실행 중에: [text/shape.c](../../text/shape.c)가 HarfBuzz 10의 절차를 함수 단위로 옮긴다. 유니코드 성질과 자소 묶음,
글꼴을 보는 정규화(없는 글자는 분해하고 있는 합성은 합치며, 부호는 HarfBuzz의 수정 결합 등급과 아랍 UTR #53, 히브리
순서로 정렬한다), 거울 글자, 아랍 연결 상태 기계, GSUB(유형 1–8과 확장, 문맥과 연쇄 문맥의 형식 1–3, 역연쇄), 합자의
성분 추적, GPOS(유형 1–8, 짝 형식 1·2, 필기체, 기저·합자·부호에 붙는 부호), 부착 오프셋의 전파, 보이지 않는 문자
- 숨기기를 한다. 표는 제자리에서 읽고, 읽을 때마다 표의 끝을 넘는지 확인한다. 망가진 표는 틀리게 셰이핑할 뿐 밖을 읽지
- 않는다.
+ 숨기기를 한다. 한글은 글꼴에 통째로 있는 음절은 자모를 합치고, 없는 음절은 자모로 풀어 `ljmo`·`vjmo`·`tjmo`를 걸고, 성조
+ 기호는 음절 앞으로 옮긴다(앞에 음절이 없으면 점선 원을 넣는다). `kern` 표는 OpenType(판 0)과 Apple(1.0)의 짝 하위 표를
+ 부호를 건너뛰며 적용하고 커닝을 두 글리프에 나눠 준다(HarfBuzz의 이진 탐색을 그대로 해서, 정렬이 어긋난 표도 같은 답을
+ 낸다). GPOS가 없는 글꼴은 부호를 결합 등급(태국·라오·히브리·아랍·티베트는 HarfBuzz처럼 다시 분류한 등급)과 외곽 상자로
+ 기저의 위·아래·양옆에 쌓는다. 합자 위의 부호는 제 성분의 몫에 놓는다. 표는 제자리에서 읽고, 읽을 때마다 표의 끝을
+ 넘는지 확인한다. 망가진 표는 틀리게 셰이핑할 뿐 밖을 읽지 않는다.
심볼 파일은 VEGASYM4가 됐다. 헤더의 깃발이 점들 뒤에 셰이핑 구역이 있다고 알린다. 셰이핑이 닿는 글리프(문자 지도,
분해와 합성, 거울, GSUB의 닫힘)는 `0x110000 + 글리프 번호`로 심볼이 된다. 같은 글리프의 심볼들은 점을 나눠 쓴다.
HarfBuzz와 다른 파일은 만들어지지 않는다. `vega-symbols build --shape`는 파일을 쓴 뒤 계획마다 무작위 실행을 HarfBuzz
10.2와 함께 셰이핑한다. 실행은 두 종류다. 하나는 낱말들(글꼴의 치환이 시작되는 글자를 자주 섞고, 부호를 하나에서
-셋까지 붙이고, ZWJ·ZWNJ를 끼운다)이고, 다른 하나는 아무 순서의 문자(맨 앞의 부호, 쌓인 부호)다. 글리프, 클러스터,
-advance, 오프셋 가운데 하나라도 다른 실행이 있으면 파일을 지운다.
+셋까지 붙이고, ZWJ·ZWNJ를 끼운다)이고, 다른 하나는 아무 순서의 문자(맨 앞의 부호, 쌓인 부호)다. 한글 낱말에는 음절을
+만드는 순서의 자모(L V, L V T, LV T)와 그렇지 않은 순서, 옛 자모, 성조 기호를 섞는다. 글리프, 클러스터, advance, 오프셋
+가운데 하나라도 다른 실행이 있으면 파일을 지운다.
결과(Pi 5):
@@ -113,14 +121,19 @@ advance, 오프셋 가운데 하나라도 다른 실행이 있으면 파일을 �
- 속도: 조회마다, 하위 표마다 시작 글리프의 요약(HarfBuzz의 set digest: 64비트 무늬 셋)을 열 때 만들어 두면, 버퍼에 없는
글리프로 시작하는 조회와 하위 표를 건너뛴다. 문자 지도는 BMP를 쪽마다 바로 찾는다. 이것으로 HarfBuzz의 1.05–1.18배
시간이다(문자당 라틴 422 대 404 ns, 아랍 428 대 371 ns, 히브리 313 대 274 ns). 요약 전에는 2.5배였다.
+- GPOS 없는 글꼴과 `kern` 표, 한글 셰이퍼를 더한 뒤(설치된 글꼴 209벌, 계획 1,520개에 각 1천 실행): 받아들인 글꼴은 모두
+ 다른 실행 0. 전에 거부하던 글꼴은 따로 계획마다 2만 실행으로 확인했다(Noto Sans CJK KR과 백묵 글꼴의 한글, IPA 고딕과
+ Noto Mono의 대체 부호 배치, FreeSans·FreeSerif·Liberation·DejaVu Sans ExtraLight의 `kern` 표, Unifont의 라틴·그리스·키릴·
+ 히브리·한글 12개 계획 24만 실행). Unifont에서 한 번 달랐다. 이디시 yod-yod와 patah를 FB1F로 합치는 히브리 표시형 규칙에
+ yod(05D9)를 적어 두었다(HarfBuzz는 05F2). 부호를 놓는 GPOS `mark`가 없는 글꼴에서만 걸리는 규칙이라 전에는 드러나지
+ 않았다. Unifont의 히브리는 HarfBuzz보다 2.3배 빠르다(문자당 116 대 272 ns). HarfBuzz는 부호마다 CFF 외곽을 풀어 상자를
+ 재고, Vega는 빌드할 때 잰 상자를 읽는다.
받지 않는 것(빌드 도구가 이유를 말하고 거부한다):
-- GPOS가 없는 글꼴. HarfBuzz는 결합 등급과 글리프 외곽 상자로 부호를 놓는다(fallback positioning). CJK 글꼴, Nimbus Mono,
- Noto Mono 등이 여기에 걸린다.
-- 옛 `kern` 표로 커닝하는 글꼴(Liberation Sans·Serif, FreeSans).
-- AAT 표(morx, kerx, trak), 늘이기(stch), 아랍 형태가 없어 HarfBuzz가 대체 셰이핑을 하는 글꼴.
-- 기본·아랍·히브리 셰이퍼가 아닌 문자 체계: 인도계, 태국·라오, 한글 자모, 크메르, 미얀마, USE 문자 체계들, 시리아.
+- `kern` 표의 상태 기계(Apple 형식 1), 등급 형식(2, 3), 교차 방향 커닝. 설치된 글꼴의 `kern` 표는 모두 짝 형식(0)이다.
+- AAT 표(morx, kerx, trak), 늘이기(stch), 아랍 형태가 없어 HarfBuzz가 표시형으로 대체 셰이핑을 하는 글꼴(Unifont의 아랍).
+- 기본·아랍·히브리·한글 셰이퍼가 아닌 문자 체계: 인도계, 태국·라오, 크메르, 미얀마, USE 문자 체계들, 시리아.
- 이체 선택자(cmap 형식 14)는 싣지 않는다. 선택자는 숨기고 앞 글자는 기본 글리프로 둔다.
## 비례폭 글줄
diff --git a/text/shape-build.c b/text/shape-build.c
index c5fd237..3910c5d 100644
--- a/text/shape-build.c
+++ b/text/shape-build.c
@@ -254,8 +254,9 @@ static int categorize(uint32_t script, hb_tag_t gsub_script) {
bool dflt = gsub_script == HB_OT_TAG_DEFAULT_SCRIPT || gsub_script == TAG("latn");
if (script == TAG("Arab")) return VEGA_SHAPER_ARABIC;
if (script == TAG("Hebr")) return VEGA_SHAPER_HEBREW;
- if (script == TAG("Syrc") || script == TAG("Thai") || script == TAG("Laoo") || script == TAG("Hang") ||
- script == TAG("Khmr") || script == TAG("Qaag"))
+ 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 (listed(indic_scripts, sizeof(indic_scripts) / sizeof(*indic_scripts), script) ||
listed(use_scripts, sizeof(use_scripts) / sizeof(*use_scripts), script))
@@ -274,11 +275,53 @@ static bool has_table(hb_face_t *face, hb_tag_t tag) {
typedef struct {
uint32_t script;
int direction, native, shaper, flags;
- uint32_t global_mask, masks[VEGA_PLAN_MASKS];
+ uint32_t global_mask, kern_mask, masks[VEGA_PLAN_MASKS];
LookupMap *lookups[2];
unsigned lookup_count[2];
} Plan;
+/* What HarfBuzz makes of the font's kern table (hb-ot-kern-table.hh): one
+ * with subtables that pass its checks (data), Apple's state machines among
+ * them (machine), cross-stream kerning (cross); and whether it has what
+ * Vega does not apply (other: a format but pairs, a version but 0 and 1). */
+typedef struct { bool data, machine, cross, other; } Kerning;
+
+static Kerning kerning_of(hb_face_t *face) {
+ Kerning k = {false, false, false, false};
+ hb_blob_t *blob = hb_face_reference_table(face, HB_TAG('k', 'e', 'r', 'n'));
+ unsigned length;
+ const unsigned char *p = (const unsigned char *)hb_blob_get_data(blob, &length);
+ #define U16(at) ((uint64_t)(at) + 2 <= length ? (uint32_t)p[at] << 8 | p[(at) + 1] : 0)
+ #define U32(at) ((uint64_t)(at) + 4 <= length ? U16(at) << 16 | U16((at) + 2) : 0)
+ uint32_t major = U16(0);
+ k.data = U32(0) != 0;
+ k.other = major > 1;
+ bool apple = major == 1, valid = length >= (apple ? 8u : 4u);
+ uint32_t count = apple ? U32(4) : U16(2), header = apple ? 8 : 6;
+ uint64_t at = apple ? 8 : 4;
+ for (uint32_t i = 0; k.data && !k.other && valid && i < count; ++i) {
+ /* each subtable within the table (the last one's pairs may run past
+ * the length it gives: HarfBuzz lets them, for fonts whose 16 bits
+ * overflowed) */
+ valid = at + header <= length;
+ if (!valid) break;
+ uint32_t size = apple ? U32(at) : U16(at + 2);
+ unsigned format = p[at + (apple ? 5 : 4)], coverage = p[at + (apple ? 4 : 5)];
+ valid = size >= header && at + size <= length;
+ uint64_t end = i + 1 < count ? at + size : length;
+ if (valid && format == 0) valid = at + header + 8 + 6 * (uint64_t)U16(at + header) <= end;
+ k.machine |= format == 1;
+ k.other |= format != 0;
+ k.cross |= apple ? coverage & 0x40 : coverage & 4;
+ at += size;
+ }
+ #undef U16
+ #undef U32
+ if (!valid) k = (Kerning){false, false, false, false}; /* HarfBuzz drops a table that fails its checks */
+ hb_blob_destroy(blob);
+ return k;
+}
+
#define FAIL(...) \
do { \
snprintf(error, error_size, __VA_ARGS__); \
@@ -305,6 +348,7 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan *
&m->language_index[t]);
}
int shaper = categorize(script_tag, m->chosen_script[0]);
+ Kerning kern_table = kerning_of(face);
if (shaper < 0) FAIL("%s: HarfBuzz shapes it with a shaper Vega does not have", name);
if (has_table(face, HB_TAG('m', 'o', 'r', 'x')) || has_table(face, HB_TAG('k', 'e', 'r', 'x')) ||
has_table(face, HB_TAG('t', 'r', 'a', 'k')))
@@ -349,6 +393,12 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan *
enable_feature(m, TAG("clig"), F_MANUAL_ZWJ, 1);
enable_feature(m, TAG("mset"), F_MANUAL_ZWJ, 1);
}
+ if (shaper == VEGA_SHAPER_HANGUL) {
+ m->is_simple = false;
+ add_feature(m, TAG("ljmo"), F_NONE, 1);
+ add_feature(m, TAG("vjmo"), F_NONE, 1);
+ add_feature(m, TAG("tjmo"), F_NONE, 1);
+ }
enable_feature(m, TAG("Buzz"), F_NONE, 1);
enable_feature(m, TAG("BUZZ"), F_NONE, 1);
static const char *const common[] = {"abvm", "blwm", "ccmp", "locl", "mark", "mkmk", "rlig"};
@@ -356,6 +406,8 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan *
add_feature(m, TAG(common[i]), F_GLOBAL | (i == 4 || i == 5 ? F_MANUAL_ZWNJ | F_MANUAL_ZWJ : 0), 1);
static const char *const horizontal[] = {"calt", "clig", "curs", "dist", "kern", "liga", "rclt"};
for (int i = 0; i < 7; ++i) add_feature(m, TAG(horizontal[i]), F_GLOBAL | (i == 4 ? F_HAS_FALLBACK : 0), 1);
+ /* Hangul turns calt off: fonts put jamo lookups there. */
+ if (shaper == VEGA_SHAPER_HANGUL) add_feature(m, TAG("calt"), F_GLOBAL, 0);
if (!compile_map(m)) FAIL("out of memory");
/* hb_ot_shape_planner_t::compile */
@@ -363,9 +415,12 @@ static bool plan_for(hb_face_t *face, uint32_t script_tag, int direction, Plan *
bool has_gpos_kern = kern && kern->index[1] != NO_FEATURE;
bool disable_gpos = shaper == VEGA_SHAPER_HEBREW && m->chosen_script[1] != TAG("hebr");
bool apply_gpos = !disable_gpos && hb_ot_layout_has_positioning(face);
- if ((!has_gpos_kern || !apply_gpos) && has_table(face, HB_TAG('k', 'e', 'r', 'n')))
- FAIL("%s: HarfBuzz would kern with the font's kern table", name);
- if (!apply_gpos) FAIL("%s: HarfBuzz would position without GPOS (fallback positioning)", name);
+ 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 shaper neither zeroes marks nor places them without GPOS. */
+ bool script_marks = shaper != VEGA_SHAPER_HANGUL;
+ 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);
bool fallback = script == HB_SCRIPT_ARABIC;
@@ -380,14 +435,21 @@ 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,
- VEGA_PLAN_GPOS | VEGA_PLAN_ZERO_MARKS | (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),
- m->global_mask, {0}, {m->lookups[0], m->lookups[1]}, {m->lookup_count[0], m->lookup_count[1]}};
+ (apply_gpos ? VEGA_PLAN_GPOS : 0) | (script_marks ? 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) |
+ (adjust ? VEGA_PLAN_ADJUST_ZEROING : 0),
+ m->global_mask, kern ? kern->one_mask : 0, {0}, {m->lookups[0], m->lookups[1]},
+ {m->lookup_count[0], m->lookup_count[1]}};
for (int i = 0; i < 7; ++i) plan->masks[VEGA_MASK_ISOL + i] = one_mask(m, arabic_features[i]);
plan->masks[VEGA_MASK_RTLM] = one_mask(m, "rtlm");
plan->masks[VEGA_MASK_FRAC] = frac;
plan->masks[VEGA_MASK_NUMR] = numr;
plan->masks[VEGA_MASK_DNOM] = dnom;
+ plan->masks[VEGA_MASK_LJMO] = one_mask(m, "ljmo");
+ plan->masks[VEGA_MASK_VJMO] = one_mask(m, "vjmo");
+ plan->masks[VEGA_MASK_TJMO] = one_mask(m, "tjmo");
free(m);
return true;
fail:
@@ -463,7 +525,7 @@ bool vega_shaping_compile(hb_face_t *face, const uint32_t *scripts, size_t scrip
put32(&o, 0);
put16(&o, hb_face_get_upem(face));
put16(&o, glyph_count);
- for (int i = 0; i < 13; ++i) put32(&o, 0);
+ for (int i = 0; i < 17; ++i) put32(&o, 0);
set32(&o, 12, cmap_count);
set32(&o, 16, (uint32_t)o.size);
for (hb_codepoint_t u = HB_SET_VALUE_INVALID; hb_set_next(unicodes, &u);) {
@@ -491,6 +553,38 @@ bool vega_shaping_compile(hb_face_t *face, const uint32_t *scripts, size_t scrip
put(&o, data, length);
hb_blob_destroy(tables[k]);
}
+ /* The kern table as it is, for the plans that kern with it; and the
+ * glyphs' ink boxes, for those that place marks without GPOS. */
+ bool kerns = false, boxes = false;
+ for (size_t i = 0; i < plan_count; ++i) {
+ kerns |= plans[i].flags & VEGA_PLAN_KERN;
+ boxes |= plans[i].flags & VEGA_PLAN_FALLBACK_MARKS;
+ }
+ if (kerns) {
+ hb_blob_t *kern = hb_face_reference_table(face, HB_TAG('k', 'e', 'r', 'n'));
+ unsigned length;
+ const char *data = hb_blob_get_data(kern, &length);
+ align4(&o);
+ set32(&o, 64, (uint32_t)o.size);
+ set32(&o, 68, length);
+ put(&o, data, length);
+ hb_blob_destroy(kern);
+ }
+ if (boxes) {
+ align4(&o);
+ set32(&o, 72, (uint32_t)o.size);
+ for (unsigned g = 0; g < glyph_count; ++g) {
+ hb_glyph_extents_t e;
+ bool known = hb_font_get_glyph_extents(font, g, &e);
+ if (known && (e.x_bearing < -32767 || e.x_bearing > 32767 || e.y_bearing < -32768 || e.y_bearing > 32767 ||
+ e.width < -32768 || e.width > 32767 || e.height < -32768 || e.height > 32767))
+ FAIL("glyph %u: an ink box outside 16 bits", g);
+ put16(&o, known ? (uint32_t)(e.x_bearing & 0xFFFF) : 0x8000);
+ put16(&o, (uint32_t)((known ? e.y_bearing : 0) & 0xFFFF));
+ put16(&o, (uint32_t)((known ? e.width : 0) & 0xFFFF));
+ put16(&o, (uint32_t)((known ? e.height : 0) & 0xFFFF));
+ }
+ }
align4(&o);
set32(&o, 48, (uint32_t)plan_count);
set32(&o, 52, (uint32_t)o.size);
@@ -502,6 +596,7 @@ bool vega_shaping_compile(hb_face_t *face, const uint32_t *scripts, size_t scrip
(unsigned char)p->flags};
put(&o, small, 4);
put32(&o, p->global_mask);
+ put32(&o, p->kern_mask);
for (int k = 0; k < VEGA_PLAN_MASKS; ++k) put32(&o, p->masks[k]);
for (int t = 0; t < 2; ++t) {
put32(&o, step);
@@ -604,6 +699,40 @@ static void pool_add(Pool *p, uint32_t code) {
}
static uint32_t pick(Random *r, const Pool *p) { return p->items[below(r, (uint32_t)p->count)]; }
+/* A jamo of one part of a Hangul syllable: half the time one that composes
+ * (whether the font has it or not), half the time any the font has. */
+static uint32_t jamo(Random *r, const Pool *p, uint32_t first, uint32_t count) {
+ return p->count && below(r, 2) ? pick(r, p) : first + below(r, count);
+}
+
+/* A Hangul syllable the ways text spells one (hb-ot-shaper-hangul.cc): jamo
+ * in the orders that compose (L V, L V T, LV T), a syllable whole, jamo in
+ * any order; now and then a tone mark after. jamo holds the font's L, V
+ * and T. */
+static size_t hangul_syllable(Random *r, const Pool *jamo_pools, uint32_t *text, size_t n) {
+ const Pool *l = &jamo_pools[0], *v = &jamo_pools[1], *t = &jamo_pools[2];
+ switch (below(r, 5)) {
+ case 0:
+ case 1:
+ text[n++] = jamo(r, l, 0x1100, 19);
+ text[n++] = jamo(r, v, 0x1161, 21);
+ if (below(r, 2)) text[n++] = jamo(r, t, 0x11A8, 27);
+ break;
+ case 2:
+ text[n++] = 0xAC00 + 28 * below(r, 19 * 21);
+ text[n++] = jamo(r, t, 0x11A8, 27);
+ break;
+ case 3: text[n++] = 0xAC00 + below(r, 11172); break;
+ default:
+ for (unsigned k = 1 + below(r, 3); k--;) {
+ unsigned part = below(r, 3);
+ text[n++] = jamo(r, &jamo_pools[part], part == 0 ? 0x1100 : part == 1 ? 0x1161 : 0x11A8, part == 0 ? 19 : part == 1 ? 21 : 27);
+ }
+ }
+ if (!below(r, 8)) text[n++] = 0x302E + below(r, 2);
+ return n;
+}
+
static void describe(FILE *report, const char *who, const VegaShaped *g, unsigned count) {
fprintf(report, " %-8s", who);
for (unsigned i = 0; i < count; ++i)
@@ -618,7 +747,8 @@ void vega_shaping_check(hb_font_t *font, const VegaShaping *s, uint32_t script,
/* Letters of the script, marks it can take (its own and inherited
* ones), the rest of its characters and Common's (spaces, digits,
* punctuation), and those whose glyphs some substitution starts from. */
- Pool letters = {0}, marks = {0}, others = {0}, hot = {0};
+ Pool letters = {0}, marks = {0}, others = {0}, hot = {0}, jamo_pools[3] = {{0}};
+ bool hangul = script == HB_TAG('H', 'a', 'n', 'g');
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)
@@ -629,6 +759,9 @@ void vega_shaping_check(hb_font_t *font, const VegaShaping *s, uint32_t script,
hb_codepoint_t glyph;
if (!hb_font_get_nominal_glyph(font, code, &glyph) || c == VEGA_CC || c == VEGA_CS || c == VEGA_CO) continue;
bool mark = c == VEGA_MN || c == VEGA_MC || c == VEGA_ME;
+ if (hangul && ((code >= 0x1100 && code <= 0x115F) || (code >= 0xA960 && code <= 0xA97C))) pool_add(&jamo_pools[0], code);
+ if (hangul && ((code >= 0x1160 && code <= 0x11A7) || (code >= 0xD7B0 && code <= 0xD7C6))) pool_add(&jamo_pools[1], code);
+ if (hangul && ((code >= 0x11A8 && code <= 0x11FF) || (code >= 0xD7CB && code <= 0xD7FB))) pool_add(&jamo_pools[2], code);
if (mark && (of == script || of == inherited)) pool_add(&marks, code);
else if (!mark && of == script && c >= VEGA_LU && c <= VEGA_LO) pool_add(&letters, code);
else if (!mark && (of == script || of == common) && c != VEGA_ZL && c != VEGA_ZP) pool_add(&others, code);
@@ -656,6 +789,10 @@ void vega_shaping_check(hb_font_t *font, const VegaShaping *s, uint32_t script,
}
unsigned length = 1 + below(&random, 8);
for (unsigned k = 0; k < length && n < 200; ++k) {
+ if (hangul && below(&random, 2)) {
+ n = hangul_syllable(&random, jamo_pools, text, n);
+ continue;
+ }
text[n++] = hot.count && below(&random, 3) == 0 ? pick(&random, &hot) : pick(&random, &letters);
if (marks.count && below(&random, 100) < mark_rate) {
text[n++] = pick(&random, &marks);
@@ -720,4 +857,5 @@ void vega_shaping_check(hb_font_t *font, const VegaShaping *s, uint32_t script,
free(marks.items);
free(others.items);
free(hot.items);
+ for (int k = 0; k < 3; ++k) free(jamo_pools[k].items);
}
diff --git a/text/shape-build.h b/text/shape-build.h
index dd93177..ad92085 100644
--- a/text/shape-build.h
+++ b/text/shape-build.h
@@ -10,12 +10,11 @@
typedef struct { unsigned char *bytes; size_t size; } VegaSection;
/* A section for face: plans for each script (ISO 15924 tags) and both
- * directions, compiled as
- * HarfBuzz's planner compiles it. Refuses a script HarfBuzz shapes with a
- * shaper other than the default, Arabic and Hebrew ones, or a font that
- * needs what those do without GPOS (fallback positioning, 'kern' tables,
- * AAT). glyphs, when given, gets every glyph shaping can reach from codes
- * (for their outlines). */
+ * directions, compiled as HarfBuzz's planner compiles it. Refuses a script
+ * HarfBuzz shapes with a shaper other than the default, Arabic, Hebrew and
+ * Hangul ones, and a font with AAT tables (morx, kerx, trak) or a kern
+ * table of state machines. glyphs, when given, gets every glyph shaping can
+ * reach from codes (for their outlines). */
bool vega_shaping_compile(hb_face_t *face, const uint32_t *scripts, size_t script_count, const uint32_t *codes,
size_t code_count, VegaSection *out, hb_set_t *glyphs, char *error, size_t error_size);
diff --git a/text/shape.c b/text/shape.c
index fbb53b6..0016fa0 100644
--- a/text/shape.c
+++ b/text/shape.c
@@ -201,6 +201,11 @@ bool vega_shaping_open(VegaShaping *s, const unsigned char *bytes, size_t size,
if (!within(size, at, n)) REFUSE("layout table %d", k);
*tables[k] = (VegaTable){n ? bytes + at : NULL, n};
}
+ uint32_t kern = le32(bytes + 64), kern_size = le32(bytes + 68), extents = le32(bytes + 72);
+ if (!within(size, kern, kern_size)) REFUSE("kern table");
+ s->kern = (VegaTable){kern_size ? bytes + kern : NULL, kern_size};
+ if (extents && !within(size, extents, 8 * (uint64_t)s->glyph_count)) REFUSE("extents");
+ s->extents = extents ? bytes + extents : NULL;
s->plan_count = le32(bytes + 48);
uint32_t plans = le32(bytes + 52), step_count = le32(bytes + 56), steps = le32(bytes + 60);
if (!within(size, plans, (uint64_t)s->plan_count * VEGA_PLAN_BYTES)) REFUSE("plans");
@@ -218,9 +223,10 @@ bool vega_shaping_open(VegaShaping *s, const unsigned char *bytes, size_t size,
REFUSE("out of memory");
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 + 56), le32(p + 60)}, {le32(p + 64), le32(p + 68)}};
- if ((p[4] != VEGA_SHAPE_LTR && p[4] != VEGA_SHAPE_RTL) || p[5] > VEGA_SHAPE_RTL || p[6] > VEGA_SHAPER_HEBREW)
+ uint32_t ranges[2][2] = {{le32(p + 72), le32(p + 76)}, {le32(p + 80), le32(p + 84)}};
+ if ((p[4] != VEGA_SHAPE_LTR && p[4] != VEGA_SHAPE_RTL) || p[5] > VEGA_SHAPE_RTL || p[6] > VEGA_SHAPER_HANGUL)
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) {
if (ranges[table][0] > step_count || ranges[table][1] > step_count - ranges[table][0])
REFUSE("plan %u: steps", i);
@@ -384,6 +390,21 @@ static void output_glyph(VegaShaper *b, uint32_t glyph) {
b->out[b->out_len++].codepoint = glyph;
}
+/* num_in glyphs become num_out, each a copy of the first with its own
+ * codepoint; their clusters merge. */
+static void merge_clusters(VegaShaper *b, size_t start, size_t end);
+static void replace_glyphs(VegaShaper *b, size_t num_in, size_t num_out, const uint32_t *glyphs) {
+ if (!ensure(b, b->out_len + num_out)) return;
+ merge_clusters(b, b->idx, b->idx + num_in);
+ Info original = b->idx < b->len ? b->info[b->idx] : b->out[b->out_len - 1];
+ for (size_t i = 0; i < num_out; ++i) {
+ b->out[b->out_len + i] = original;
+ b->out[b->out_len + i].codepoint = glyphs[i];
+ }
+ b->idx += num_in;
+ b->out_len += num_out;
+}
+
static void sync(VegaShaper *b) {
if (!b->successful) {
b->have_output = false;
@@ -654,7 +675,7 @@ static void form_clusters(VegaShaper *b) {
typedef struct {
uint32_t script;
int direction, native, shaper, flags;
- uint32_t global_mask, masks[VEGA_PLAN_MASKS];
+ uint32_t global_mask, kern_mask, masks[VEGA_PLAN_MASKS];
const unsigned char *gsub, *gpos;
uint32_t gsub_count, gpos_count;
} Plan;
@@ -667,10 +688,10 @@ static bool find_plan(const VegaShaping *s, uint32_t script, int direction, Plan
if (le32(p) == (pass ? 0x5A797979u : script) && p[4] == direction) found = p;
}
if (!found) return false;
- *plan = (Plan){le32(found), found[4], found[5], found[6], found[7], le32(found + 8), {0},
- s->steps + (size_t)le32(found + 56) * VEGA_STEP_BYTES,
- s->steps + (size_t)le32(found + 64) * VEGA_STEP_BYTES, le32(found + 60), le32(found + 68)};
- for (int k = 0; k < VEGA_PLAN_MASKS; ++k) plan->masks[k] = le32(found + 12 + 4 * k);
+ *plan = (Plan){le32(found), found[4], found[5], found[6], found[7], le32(found + 8), le32(found + 12), {0},
+ s->steps + (size_t)le32(found + 72) * VEGA_STEP_BYTES,
+ s->steps + (size_t)le32(found + 80) * VEGA_STEP_BYTES, le32(found + 76), le32(found + 84)};
+ for (int k = 0; k < VEGA_PLAN_MASKS; ++k) plan->masks[k] = le32(found + 16 + 4 * k);
return true;
}
@@ -952,8 +973,10 @@ static void reorder_marks_hebrew(VegaShaper *b, size_t start, size_t end) {
static void normalize(const VegaShaping *s, const Plan *plan, VegaShaper *b) {
if (!b->len) return;
Normalizer c = {s, plan, b};
- /* The shapers' preference is "auto": composed diacritics. */
- const bool might_short_circuit = true, always_short_circuit = false;
+ /* The shapers' preference is "auto" (composed diacritics); Hangul's is
+ * none: what the font has stays, and nothing is composed again. */
+ const bool none = plan->shaper == VEGA_SHAPER_HANGUL;
+ const bool might_short_circuit = true, always_short_circuit = none;
bool all_simple = true;
clear_output(b);
size_t count = b->len;
@@ -997,7 +1020,7 @@ static void normalize(const VegaShaping *s, const Plan *plan, VegaShaper *b) {
(!info_cc(&b->info[i + 1]) || info_cc(&b->info[i - 1]) <= info_cc(&b->info[i + 1])))
b->info[i].uprops &= (uint16_t)~UPROPS_HIDDEN;
- if (!all_simple && b->successful) {
+ if (!all_simple && b->successful && !none) {
clear_output(b);
count = b->len;
size_t starter = 0;
@@ -1090,6 +1113,118 @@ static void setup_masks_arabic(VegaShaper *b, const Plan *plan) {
if (b->info[i].action < NONE) b->info[i].mask |= plan->masks[VEGA_MASK_ISOL + b->info[i].action];
}
+/* ---- Hangul (hb-ot-shaper-hangul.cc): a syllable the font has whole is
+ * composed; one it lacks is taken apart into jamo, which the ljmo, vjmo and
+ * tjmo features then shape; a tone mark goes before its syllable. */
+
+enum { LJMO = 1, VJMO = 2, TJMO = 3 };
+static bool combining_l(uint32_t u) { return u >= LBASE && u < LBASE + LCOUNT; }
+static bool combining_v(uint32_t u) { return u >= VBASE && u < VBASE + VCOUNT; }
+static bool combining_t(uint32_t u) { return u > TBASE && u < TBASE + TCOUNT; }
+static bool jamo_l(uint32_t u) { return (u >= 0x1100 && u <= 0x115F) || (u >= 0xA960 && u <= 0xA97C); }
+static bool jamo_v(uint32_t u) { return (u >= 0x1160 && u <= 0x11A7) || (u >= 0xD7B0 && u <= 0xD7C6); }
+static bool jamo_t(uint32_t u) { return (u >= 0x11A8 && u <= 0x11FF) || (u >= 0xD7CB && u <= 0xD7FB); }
+static bool zero_width(const VegaShaping *s, uint32_t u) {
+ uint32_t glyph;
+ return nominal(s, u, &glyph) && advance_of(s, glyph) == 0;
+}
+
+static void preprocess_hangul(const VegaShaping *s, VegaShaper *b) {
+ clear_output(b);
+ size_t start = 0, end = 0; /* the last syllable, when start < end */
+ uint32_t glyph;
+ while (b->idx < b->len && b->successful) {
+ uint32_t u = b->info[b->idx].codepoint;
+ if (u == 0x302E || u == 0x302F) {
+ if (start < end && end == b->out_len) {
+ /* a tone mark goes before its syllable, unless it overstrikes */
+ next_glyph(b);
+ if (!zero_width(s, u)) {
+ merge_out_clusters(b, start, end + 1);
+ Info tone = b->out[end];
+ memmove(&b->out[start + 1], &b->out[start], (end - start) * sizeof(Info));
+ b->out[start] = tone;
+ }
+ } else if (nominal(s, 0x25CC, &glyph)) {
+ uint32_t chars[2] = {u, 0x25CC};
+ if (zero_width(s, u)) chars[0] = 0x25CC, chars[1] = u;
+ replace_glyphs(b, 1, 2, chars);
+ } else
+ next_glyph(b);
+ start = end = b->out_len;
+ continue;
+ }
+ start = b->out_len;
+ if (jamo_l(u) && b->idx + 1 < b->len) {
+ uint32_t l = u, v = b->info[b->idx + 1].codepoint;
+ if (jamo_v(v)) {
+ uint32_t t = 0, tindex = 0;
+ if (b->idx + 2 < b->len) {
+ t = b->info[b->idx + 2].codepoint;
+ if (jamo_t(t)) tindex = t - TBASE;
+ else t = 0;
+ }
+ if (combining_l(l) && combining_v(v) && (!t || combining_t(t))) {
+ uint32_t syllable = SBASE + (l - LBASE) * NCOUNT + (v - VBASE) * TCOUNT + tindex;
+ if (nominal(s, syllable, &glyph)) {
+ replace_glyphs(b, t ? 3 : 2, 1, &syllable);
+ end = start + 1;
+ continue;
+ }
+ }
+ b->info[b->idx].action = LJMO;
+ next_glyph(b);
+ b->info[b->idx].action = VJMO;
+ next_glyph(b);
+ if (t) {
+ b->info[b->idx].action = TJMO;
+ next_glyph(b);
+ end = start + 3;
+ } else
+ end = start + 2;
+ merge_out_clusters(b, start, end);
+ continue;
+ }
+ } else if (u - SBASE < SCOUNT) {
+ bool has = nominal(s, u, &glyph);
+ uint32_t lindex = (u - SBASE) / NCOUNT, nindex = (u - SBASE) % NCOUNT, vindex = nindex / TCOUNT,
+ tindex = nindex % TCOUNT;
+ if (!tindex && b->idx + 1 < b->len && combining_t(b->info[b->idx + 1].codepoint)) {
+ uint32_t whole = u + (b->info[b->idx + 1].codepoint - TBASE);
+ if (nominal(s, whole, &glyph)) {
+ replace_glyphs(b, 2, 1, &whole);
+ end = start + 1;
+ continue;
+ }
+ }
+ if (!has || (!tindex && b->idx + 1 < b->len && jamo_t(b->info[b->idx + 1].codepoint))) {
+ uint32_t parts[3] = {LBASE + lindex, VBASE + vindex, TBASE + tindex};
+ if (nominal(s, parts[0], &glyph) && nominal(s, parts[1], &glyph) && (!tindex || nominal(s, parts[2], &glyph))) {
+ size_t n = tindex ? 3 : 2;
+ replace_glyphs(b, 1, n, parts);
+ if (has && !tindex) { /* the T that did not combine is part of it */
+ next_glyph(b);
+ ++n;
+ }
+ end = start + n;
+ b->out[start].action = LJMO;
+ b->out[start + 1].action = VJMO;
+ if (start + 2 < end) b->out[start + 2].action = TJMO;
+ merge_out_clusters(b, start, end);
+ continue;
+ }
+ }
+ if (has) {
+ end = start + 1;
+ next_glyph(b);
+ continue;
+ }
+ }
+ next_glyph(b);
+ }
+ sync(b);
+}
+
/* ---- Applying lookups (hb-ot-layout-gsubgpos.hh) */
typedef struct {
@@ -1986,6 +2121,268 @@ static void apply_table(const VegaShaping *s, VegaShaper *b, int table, const un
}
}
+/* ---- The kern table (hb-ot-kern-table.hh, hb-kern.hh): OpenType's (version
+ * 0) and Apple's (1.0), their pair subtables (format 0), each a pass over
+ * the glyphs with marks skipped, the kerning split between the pair. (The
+ * build tool refuses the other formats, and cross-stream kerning, which no
+ * font installed here has.) */
+
+static uint32_t get8(VegaTable t, uint32_t at) { return at < t.size ? t.p[at] : 0; }
+
+static int32_t kern_pair(VegaTable t, uint32_t pairs, uint32_t left, uint32_t right) {
+ /* hb_bsearch's probes, so a table out of order answers as it does */
+ int low = 0, high = (int)get16(t, pairs) - 1;
+ uint32_t key = left << 16 | right;
+ while (low <= high) {
+ int middle = (int)(((unsigned)low + (unsigned)high) / 2);
+ uint32_t at = pairs + 8 + 6 * (uint32_t)middle, pair = get16(t, at) << 16 | get16(t, at + 2);
+ if (key < pair) high = middle - 1;
+ else if (key > pair) low = middle + 1;
+ else return gets16(t, at + 4);
+ }
+ return 0;
+}
+
+static void kern_table(const VegaShaping *s, VegaShaper *b, const Plan *plan) {
+ VegaTable t = s->kern;
+ uint32_t major = get16(t, 0);
+ if (major > 1 || !plan->kern_mask) return;
+ bool apple = major == 1;
+ uint32_t count = apple ? get32(t, 4) : get16(t, 2), at = apple ? 8 : 4;
+ for (uint32_t k = 0; k < count && at < t.size; ++k) {
+ uint32_t length = apple ? get32(t, at) : get16(t, at + 2);
+ unsigned format = get8(t, at + (apple ? 5 : 4)), coverage = get8(t, at + (apple ? 4 : 5));
+ bool horizontal = apple ? !(coverage & 0x80) : coverage & 1;
+ uint32_t pairs = at + (apple ? 8 : 6);
+ at += length ? length : t.size;
+ if ((apple && (coverage & 0x20)) || !horizontal || format != 0) continue;
+ bool reverse = b->direction == VEGA_SHAPE_RTL;
+ if (reverse) reverse_range(b, 0, b->len, true);
+ Apply c = {s, b, 1, s->gpos, plan->kern_mask, 0x08, true, true, false, s->glyph_classes != 0, MAX_NESTING_LEVEL, 1,
+ -1, 0, {{0, 0, 0}}};
+ for (size_t idx = 0; idx < b->len;) {
+ if (!(b->info[idx].mask & plan->kern_mask)) {
+ idx++;
+ continue;
+ }
+ Skippy it;
+ skippy_init(&it, &c, false, idx);
+ if (!skippy_next(&it, NULL)) {
+ idx++;
+ continue;
+ }
+ size_t i = idx, j = it.idx;
+ int32_t kern = kern_pair(t, pairs, b->info[i].codepoint, b->info[j].codepoint);
+ if (kern) {
+ int32_t first = kern >> 1, second = kern - first;
+ b->pos[i].x_advance += first;
+ b->pos[j].x_advance += second;
+ b->pos[j].x_offset += second;
+ }
+ idx = j;
+ }
+ if (reverse) reverse_range(b, 0, b->len, true);
+ }
+}
+
+/* ---- Marks without GPOS (hb-ot-shape-fallback.cc): each mark placed around
+ * its base by its combining class and the glyphs' ink boxes. */
+
+typedef struct { int32_t x_bearing, y_bearing, width, height; } Extents;
+
+static bool extents_of(const VegaShaping *s, uint32_t glyph, Extents *e) {
+ if (!s->extents || glyph >= s->glyph_count) return false;
+ const unsigned char *p = s->extents + 8 * (size_t)glyph;
+ int16_t x = (int16_t)le16(p);
+ if (x == INT16_MIN) return false;
+ *e = (Extents){x, (int16_t)le16(p + 2), (int16_t)le16(p + 4), (int16_t)le16(p + 6)};
+ return true;
+}
+
+enum { CC_ATTACHED_BELOW_LEFT = 200, CC_ATTACHED_BELOW = 202, CC_ATTACHED_ABOVE = 214, CC_ATTACHED_ABOVE_RIGHT = 216,
+ CC_BELOW_LEFT = 218, CC_BELOW = 220, CC_BELOW_RIGHT = 222, CC_ABOVE_LEFT = 228, CC_ABOVE = 230,
+ CC_ABOVE_RIGHT = 232, CC_DOUBLE_BELOW = 233, CC_DOUBLE_ABOVE = 234 };
+
+/* Where the classes put marks, for those HarfBuzz reorders by other classes. */
+static unsigned recategorize(uint32_t u, unsigned klass) {
+ if (klass >= 200) return klass;
+ if ((u & ~0xFFu) == 0x0E00) {
+ if (!klass) {
+ switch (u) {
+ case 0x0E31: case 0x0E34: case 0x0E35: case 0x0E36: case 0x0E37: case 0x0E47: case 0x0E4C: case 0x0E4D:
+ case 0x0E4E: klass = CC_ABOVE_RIGHT; break;
+ case 0x0EB1: case 0x0EB4: case 0x0EB5: case 0x0EB6: case 0x0EB7: case 0x0EBB: case 0x0ECC: case 0x0ECD:
+ klass = CC_ABOVE; break;
+ case 0x0EBC: klass = CC_BELOW; break;
+ }
+ } else if (u == 0x0E3A)
+ klass = CC_BELOW_RIGHT;
+ }
+ switch (klass) {
+ case 22: case 15: case 16: case 17: case 23: case 18: case 19: case 20: case 21: case 24: case 25:
+ return CC_BELOW; /* Hebrew points below, meteg */
+ case 13: return CC_ATTACHED_ABOVE; /* rafe */
+ case 10: return CC_ABOVE_RIGHT; /* shin dot */
+ case 11: case 14: return CC_ABOVE_LEFT; /* sin dot, holam */
+ case 26: return CC_ABOVE; /* point varika */
+ case 12: return klass; /* dagesh */
+ case 28: case 29: case 31: case 32: case 27: case 34: case 35: case 36:
+ return CC_ABOVE; /* Arabic and Syriac above */
+ case 30: case 33: return CC_BELOW; /* kasratan, kasra */
+ case 3: return CC_BELOW_RIGHT; /* Thai sara u, uu */
+ case 107: return CC_ABOVE_RIGHT; /* Thai mai */
+ case 118: return CC_BELOW; /* Lao sign u, uu */
+ case 122: return CC_ABOVE; /* Lao mai */
+ case 129: return CC_BELOW; /* Tibetan sign aa */
+ case 132: return CC_ABOVE; /* sign i */
+ case 131: return CC_BELOW; /* sign u */
+ default: return klass;
+ }
+}
+
+static void position_mark(const VegaShaping *s, int direction, uint32_t glyph, Position *pos, Extents *base,
+ unsigned klass) {
+ Extents mark;
+ if (!extents_of(s, glyph, &mark)) return;
+ int32_t y_gap = s->upem / 16;
+ pos->x_offset = pos->y_offset = 0;
+ switch (klass) {
+ case CC_DOUBLE_BELOW:
+ case CC_DOUBLE_ABOVE:
+ if (direction == VEGA_SHAPE_LTR) pos->x_offset += base->x_bearing + base->width - mark.width / 2 - mark.x_bearing;
+ else pos->x_offset += base->x_bearing - mark.width / 2 - mark.x_bearing;
+ break;
+ case CC_ATTACHED_BELOW_LEFT: case CC_BELOW_LEFT: case CC_ABOVE_LEFT:
+ pos->x_offset += base->x_bearing - mark.x_bearing;
+ break;
+ case CC_ATTACHED_ABOVE_RIGHT: case CC_BELOW_RIGHT: case CC_ABOVE_RIGHT:
+ pos->x_offset += base->x_bearing + base->width - mark.width - mark.x_bearing;
+ break;
+ default:
+ pos->x_offset += base->x_bearing + (base->width - mark.width) / 2 - mark.x_bearing;
+ break;
+ }
+ switch (klass) {
+ case CC_DOUBLE_BELOW: case CC_BELOW_LEFT: case CC_BELOW: case CC_BELOW_RIGHT:
+ base->height -= y_gap;
+ /* fall through */
+ case CC_ATTACHED_BELOW_LEFT: case CC_ATTACHED_BELOW:
+ pos->y_offset = base->y_bearing + base->height - mark.y_bearing;
+ if ((y_gap > 0) == (pos->y_offset > 0)) { /* below marks never go up */
+ base->height -= pos->y_offset;
+ pos->y_offset = 0;
+ }
+ base->height += mark.height;
+ break;
+ case CC_DOUBLE_ABOVE: case CC_ABOVE_LEFT: case CC_ABOVE: case CC_ABOVE_RIGHT:
+ base->y_bearing += y_gap;
+ base->height -= y_gap;
+ /* fall through */
+ case CC_ATTACHED_ABOVE: case CC_ATTACHED_ABOVE_RIGHT: {
+ pos->y_offset = base->y_bearing - (mark.y_bearing + mark.height);
+ if ((y_gap > 0) != (pos->y_offset > 0)) { /* nor above marks too far down */
+ int32_t correction = -pos->y_offset / 2;
+ base->y_bearing += correction;
+ base->height -= correction;
+ pos->y_offset += correction;
+ }
+ base->y_bearing -= mark.height;
+ base->height += mark.height;
+ break;
+ }
+ default: break;
+ }
+}
+
+static void zero_mark_advances(VegaShaper *b, size_t start, size_t end, bool adjust) {
+ for (size_t i = start; i < end; ++i)
+ if (gen_cat(&b->info[i]) == VEGA_MN) {
+ 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_around_base(const VegaShaping *s, const Plan *plan, VegaShaper *b, size_t base, size_t end,
+ bool adjust) {
+ Extents base_extents;
+ if (!extents_of(s, b->info[base].codepoint, &base_extents)) {
+ zero_mark_advances(b, base + 1, end, adjust);
+ return;
+ }
+ base_extents.y_bearing += b->pos[base].y_offset;
+ base_extents.x_bearing = 0;
+ base_extents.width = advance_of(s, b->info[base].codepoint);
+ unsigned id = lig_id(&b->info[base]);
+ int components = lig_num_comps(&b->info[base]);
+ int32_t x_offset = 0, y_offset = 0;
+ bool forward = b->direction == VEGA_SHAPE_LTR;
+ if (forward) {
+ x_offset -= b->pos[base].x_advance;
+ y_offset -= b->pos[base].y_advance;
+ }
+ Extents component = base_extents, cluster = base_extents;
+ int last_component = -1;
+ unsigned last_class = 255;
+ for (size_t i = base + 1; i < end; ++i) {
+ unsigned klass = info_cc(&b->info[i]);
+ if (klass) {
+ if (components > 1) {
+ unsigned this_id = lig_id(&b->info[i]);
+ int this_component = lig_comp(&b->info[i]) - 1;
+ if (!id || id != this_id || this_component >= components) this_component = components - 1;
+ if (last_component != this_component) {
+ last_component = this_component;
+ last_class = 255;
+ component = base_extents;
+ if (plan->direction == VEGA_SHAPE_LTR)
+ component.x_bearing += this_component * component.width / components;
+ else
+ component.x_bearing += (components - 1 - this_component) * component.width / components;
+ component.width /= components;
+ }
+ }
+ if (last_class != klass) {
+ last_class = klass;
+ cluster = component;
+ }
+ position_mark(s, b->direction, b->info[i].codepoint, &b->pos[i], &cluster, klass);
+ b->pos[i].x_advance = b->pos[i].y_advance = 0;
+ b->pos[i].x_offset += x_offset;
+ b->pos[i].y_offset += y_offset;
+ } else if (forward) {
+ x_offset -= b->pos[i].x_advance;
+ y_offset -= b->pos[i].y_advance;
+ } else {
+ x_offset += b->pos[i].x_advance;
+ y_offset += b->pos[i].y_advance;
+ }
+ }
+}
+
+static void position_cluster(const VegaShaping *s, const Plan *plan, VegaShaper *b, size_t start, size_t end, bool adjust) {
+ if (end - start < 2) return;
+ for (size_t i = start; i < end; ++i)
+ if (!is_unicode_mark(&b->info[i])) {
+ size_t j = i + 1;
+ while (j < end && is_unicode_mark(&b->info[j])) j++;
+ position_around_base(s, plan, b, i, j, adjust);
+ i = j - 1;
+ }
+}
+
+static void fallback_marks(const VegaShaping *s, const Plan *plan, VegaShaper *b, bool adjust) {
+ size_t start = 0;
+ for (size_t i = 1; i < b->len; ++i)
+ if (!is_unicode_mark(&b->info[i])) {
+ position_cluster(s, plan, b, start, i, adjust);
+ start = i;
+ }
+ position_cluster(s, plan, b, start, b->len, adjust);
+}
+
/* ---- Positions */
static void fallback_spaces(const VegaShaping *s, VegaShaper *b) {
@@ -2049,16 +2446,25 @@ static void position(const VegaShaping *s, const Plan *plan, VegaShaper *b) {
memset(b->pos, 0, b->len * sizeof(Position));
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;
if (plan->flags & VEGA_PLAN_GPOS) apply_table(s, b, 1, plan->gpos, plan->gpos_count);
+ 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) b->pos[i].x_advance = b->pos[i].y_advance = 0;
+ 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 (b->scratch & HAS_DEFAULT_IGNORABLES)
for (size_t i = 0; i < b->len; ++i)
if (is_default_ignorable(&b->info[i]))
b->pos[i].x_advance = b->pos[i].y_advance = b->pos[i].x_offset = b->pos[i].y_offset = 0;
if (b->scratch & HAS_GPOS_ATTACHMENT)
for (size_t i = 0; i < b->len; ++i) propagate_attachment_offsets(b->pos, b->len, i, b->direction, MAX_NESTING_LEVEL);
+ if (plan->flags & VEGA_PLAN_FALLBACK_MARKS) fallback_marks(s, plan, b, adjust);
if (b->direction == VEGA_SHAPE_RTL) reverse_range(b, 0, b->len, true);
}
@@ -2082,6 +2488,7 @@ long vega_shape(VegaShaper *b, const VegaShaping *s, const uint32_t *codes, size
set_unicode_props(b);
form_clusters(b);
ensure_native_direction(b, &plan);
+ if (plan.shaper == VEGA_SHAPER_HANGUL) preprocess_hangul(s, b);
/* Mirror what the font mirrors; mark the rest for rtlm. */
if (direction == VEGA_SHAPE_RTL)
@@ -2093,6 +2500,13 @@ long vega_shape(VegaShaper *b, const VegaShaping *s, const uint32_t *codes, size
normalize(s, &plan, b);
setup_masks_fraction(b, &plan);
if (plan.shaper == VEGA_SHAPER_ARABIC) setup_masks_arabic(b, &plan);
+ 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)
+ b->info[i].mask |= plan.masks[VEGA_MASK_LJMO + b->info[i].action - LJMO];
+ if (plan.flags & VEGA_PLAN_FALLBACK_MARKS)
+ for (size_t i = 0; i < b->len; ++i)
+ if (gen_cat(&b->info[i]) == VEGA_MN) set_info_cc(&b->info[i], recategorize(b->info[i].codepoint, info_cc(&b->info[i])));
for (size_t i = 0; i < b->len; ++i) {
Info *info = &b->info[i];
info->codepoint = info->glyph_index;
diff --git a/text/shape.h b/text/shape.h
index 5fe7c4e..fdec7ee 100644
--- a/text/shape.h
+++ b/text/shape.h
@@ -24,22 +24,33 @@
* u32 advances_offset (glyph_count u16),
* u32 offset, u32 size of GDEF, of GSUB and of GPOS (0 0 when absent),
* u32 plan_count, u32 plans_offset (VEGA_PLAN_BYTES each),
- * u32 step_count, u32 steps_offset (8 bytes each)
+ * u32 step_count, u32 steps_offset (8 bytes each),
+ * u32 offset, u32 size of the kern table (0 0 when absent),
+ * u32 extents_offset (glyph_count of i16 x_bearing,
+ * y_bearing, width, height; 0 when
+ * no plan positions marks without
+ * GPOS; x_bearing INT16_MIN: none),
+ * u32 0
* Offsets are from the section's start. A plan:
* u32 script (ISO 15924 as a tag: 'Latn'), u8 direction, u8 native
* direction (of its script; 0 when it has none), u8 shaper, u8 flags,
- * u32 global mask, u32 masks[VEGA_PLAN_MASKS], u32 first GSUB step,
- * u32 GSUB steps, u32 first GPOS step, u32 GPOS steps.
+ * u32 global mask, u32 kern mask, u32 masks[VEGA_PLAN_MASKS], u32 first
+ * GSUB step, u32 GSUB steps, u32 first GPOS step, u32 GPOS steps.
* A step, a lookup to run: u16 lookup index, u8 flags, u8 0, u32 mask. */
-enum { VEGA_SHAPING_HEADER = 64, VEGA_PLAN_BYTES = 72, VEGA_STEP_BYTES = 8, VEGA_PLAN_MASKS = 11 };
+enum { VEGA_SHAPING_HEADER = 80, VEGA_PLAN_BYTES = 88, VEGA_STEP_BYTES = 8, VEGA_PLAN_MASKS = 14 };
enum { VEGA_SHAPE_LTR = 1, VEGA_SHAPE_RTL = 2 };
-enum { VEGA_SHAPER_DEFAULT, VEGA_SHAPER_ARABIC, VEGA_SHAPER_HEBREW };
-/* Plan flags. */
+enum { VEGA_SHAPER_DEFAULT, VEGA_SHAPER_ARABIC, VEGA_SHAPER_HEBREW, VEGA_SHAPER_HANGUL };
+/* 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
+ * placed around their base without GPOS, and zeroing a mark's advance keeps
+ * it where it was. */
enum { VEGA_PLAN_GPOS = 1, VEGA_PLAN_ZERO_MARKS = 2, VEGA_PLAN_GUESS_CLASSES = 4, VEGA_PLAN_GPOS_MARK = 8,
- VEGA_PLAN_FRACTIONS = 16 };
+ VEGA_PLAN_FRACTIONS = 16, VEGA_PLAN_KERN = 32, VEGA_PLAN_FALLBACK_MARKS = 64, VEGA_PLAN_ADJUST_ZEROING = 128 };
/* The masks a plan names, for the features set glyph by glyph. */
enum { VEGA_MASK_ISOL, VEGA_MASK_FINA, VEGA_MASK_FIN2, VEGA_MASK_FIN3, VEGA_MASK_MEDI, VEGA_MASK_MED2,
- VEGA_MASK_INIT, VEGA_MASK_RTLM, VEGA_MASK_FRAC, VEGA_MASK_NUMR, VEGA_MASK_DNOM };
+ VEGA_MASK_INIT, VEGA_MASK_RTLM, VEGA_MASK_FRAC, VEGA_MASK_NUMR, VEGA_MASK_DNOM, VEGA_MASK_LJMO, VEGA_MASK_VJMO,
+ VEGA_MASK_TJMO };
/* Step flags. */
enum { VEGA_STEP_AUTO_ZWNJ = 1, VEGA_STEP_AUTO_ZWJ = 2, VEGA_STEP_RANDOM = 4, VEGA_STEP_PER_SYLLABLE = 8 };
@@ -64,8 +75,8 @@ typedef struct {
size_t size;
uint16_t upem, glyph_count;
uint32_t cmap_count, plan_count;
- const unsigned char *cmap, *advances, *plans, *steps;
- VegaTable gdef, gsub, gpos;
+ const unsigned char *cmap, *advances, *plans, *steps, *extents;
+ VegaTable gdef, gsub, gpos, kern;
uint32_t glyph_classes, attach_classes, mark_sets; /* in GDEF; 0 when absent */
VegaLookup *lookups[2]; /* GSUB, GPOS */
uint32_t lookup_count[2];