aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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];