aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 16:04:05 +0100
committereunhhu <[email protected]>2026-09-28 16:04:05 +0100
commit960fa21762484d3014a6778a7673f15acf2d34bb (patch)
tree9e9a38a919b3cb8ed82c84524b7dd72b289ce49e
parentddbbc39cc65946f59298eab546c906ad3dea6841 (diff)
downloadvega-960fa21762484d3014a6778a7673f15acf2d34bb.tar.gz
vega-960fa21762484d3014a6778a7673f15acf2d34bb.zip
Map characters as HarfBuzz maps them, not as it lists them
The shaping section's character map came from hb_face_collect_unicodes. HarfBuzz's own suite showed where that list and its mapping part: it cuts a format 13 map short (AdobeBlank2's whole Unicode to one glyph), and a symbol map (3,0) it searches through the private use area, Latin and, in an old Arabic font, Arabic too. Now every code point is asked with hb_font_get_nominal_glyph (about 30 ms a font) and the map carried as groups: codes going to consecutive glyphs, or all to one (12 bytes each, so a format 13 map stays small). HarfBuzz's suite: 5011 of 5199 cases the same (1355 before). The installed fonts (213) and the Noto ones (21) still shape as HarfBuzz does on random runs.
-rw-r--r--docs/design/004-text.md21
-rw-r--r--text/harfbuzz-suite.known12
-rw-r--r--text/harfbuzz-suite.py6
-rw-r--r--text/shape-build.c48
-rw-r--r--text/shape.c44
-rw-r--r--text/shape.h8
6 files changed, 91 insertions, 48 deletions
diff --git a/docs/design/004-text.md b/docs/design/004-text.md
index 4e8e7fb..9e6f968 100644
--- a/docs/design/004-text.md
+++ b/docs/design/004-text.md
@@ -87,8 +87,10 @@ Sentence_Break, 동아시아 폭이 F·W·H인 여는·닫는 괄호)와 규칙�
히브리, 한글 셰이퍼의 목록과 단계), 중복을 합치고, 마스크 비트를 나누고, 글꼴의 문자 체계·언어 체계에서 기능을 찾고,
단계별로 조회를 모아 정렬해 합친다. 누가 자리를 잡는지도 여기서 정한다. GPOS, 옛 `kern` 표(GPOS에 `kern` 기능이 없을 때),
부호의 대체 배치(GPOS가 없을 때) 가운데 무엇을 쓸지, 부호의 폭을 0으로 만들며 오프셋을 옮길지를 HarfBuzz의 조건 그대로
- 고른다. 결과는 조회 번호, 마스크, ZWJ/ZWNJ 규칙, 이 깃발들의 목록이다. 글꼴의 문자 지도와 advance, GDEF·GSUB·GPOS와 `kern`
- 표는 그대로 싣고, 대체 배치를 하는 계획이 있으면 글리프마다 외곽 상자(HarfBuzz의 glyph extents, 16비트 넷)를 싣는다
+ 고른다. 결과는 조회 번호, 마스크, ZWJ/ZWNJ 규칙, 이 깃발들의 목록이다. 문자 지도는 HarfBuzz가 매기는 대로 싣는다. 모든
+ 코드 포인트를 `hb_font_get_nominal_glyph`로 물어, 이어지는 글리프로 가거나 한 글리프로 가는 묶음으로 적는다. 목록
+ (`hb_face_collect_unicodes`)으로 만들면 틀린다. 형식 13 지도는 목록이 잘리고, 기호 지도(3,0)의 라틴과 아랍 문자는 HarfBuzz가
+ 사용자 영역으로 옮겨 찾는다. 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, 히브리
@@ -232,6 +234,21 @@ U+180B–180E만 넣고 네 번째 자유 이체 선택자(U+180F)를 빠뜨렸�
- 미얀마 Zawgyi(표준이 아닌 인코딩, 문자 체계 태그 Qaag로만 오는 것).
- 이체 선택자(cmap 형식 14)는 싣지 않는다. 선택자는 숨기고 앞 글자는 기본 글리프로 둔다.
+### HarfBuzz의 시험 모음
+
+무작위 실행은 흔한 경우를 덮지만, 글꼴 하나가 드러낸 버릇은 그 글꼴과 그 입력이 있어야 보인다. HarfBuzz의 시험 모음
+(`test/shape/data`: in-house, text-rendering-tests, aots)이 그 목록이다. 버릇이 드러날 때마다 글꼴과 입력을 더해 온 것이다.
+[text/harfbuzz-suite.py](../../text/harfbuzz-suite.py)는 그 모든 경우를 `vega-symbols shape-cases`로 양쪽에서 셰이핑한다.
+경우들이 쓰는 문자 체계로 글꼴의 구역을 컴파일하고(문자 체계와 방향은 HarfBuzz가 추측하는 대로), 실행마다 HarfBuzz와 글리프·
+위치를 댄다. 기대 출력 대신 HarfBuzz를 그 자리에서 돌린다. 기록된 출력은 Vega에 없는 선택(글꼴 함수, 크기, 출력 형식)에
+딸려 있기 때문이다. Vega가 하지 않는 것을 묻는 경우(사용자 기능, 가변 글꼴, 언어, 앞뒤 문맥, 클러스터 수준, 버퍼 깃발, 세로쓰기)와
+다른 시스템의 글꼴은 이유별로 세고 뺀다. [text/harfbuzz-suite.known](../../text/harfbuzz-suite.known)에 적힌 것 밖의 차이는
+실패다. 적힌 것이 맞게 되면 알린다(`make check-harfbuzz HARFBUZZ_SRC=...`).
+
+처음 돌렸을 때 비교한 5,199건 가운데 1,355건만 같았다. 차이 대부분은 문자 지도였다(위의 형식 13과 기호 지도). 그것을 고치자
+5,011건이 같다. 남은 것은 AAT 글꼴(거부), 이체 선택자, 글리프를 나눠 쓰는 아랍 대체 셰이핑, `kern` 형식 2, Zawgyi, 계획이 없는
+실행 하나다. 설치된 글꼴 213개와 Noto 21개의 무작위 실행도 다시 돌려 차이가 없다.
+
## 비례폭 글줄
[text/layout.c](../../text/layout.c)는 검증한 부품을 UAX #9 3.4절과 UAX #14가 말하는 순서로 잇는다.
diff --git a/text/harfbuzz-suite.known b/text/harfbuzz-suite.known
index a0436bb..f6d9b6d 100644
--- a/text/harfbuzz-suite.known
+++ b/text/harfbuzz-suite.known
@@ -6,18 +6,6 @@
text-rendering-tests/MORX-* # AAT
in-house/aat-* # AAT
-# The character map made from what HarfBuzz lists (hb_face_collect_unicodes),
-# not from how it maps: a format 13 map it lists cut short, a symbol map
-# whose Latin and Arabic it maps to the private use area.
-in-house/emoji-clusters.tests:* # cmap format 13
-text-rendering-tests/CMAP-3.tests:1 # cmap
-text-rendering-tests/CMAP-3.tests:11 # cmap
-in-house/arabic-fallback-shaping.tests:7 # symbol cmap, Arabic PUA
-in-house/arabic-fallback-shaping.tests:8 # symbol cmap, Arabic PUA
-in-house/arabic-fallback-shaping.tests:9 # symbol cmap, Arabic PUA
-in-house/arabic-fallback-shaping.tests:10 # symbol cmap, Arabic PUA
-in-house/arabic-fallback-shaping.tests:11 # symbol cmap, Arabic PUA
-
# Variation selectors (cmap format 14).
text-rendering-tests/CMAP-1.tests:2 # cmap format 14
text-rendering-tests/CMAP-1.tests:3 # cmap format 14
diff --git a/text/harfbuzz-suite.py b/text/harfbuzz-suite.py
index aa25195..d132070 100644
--- a/text/harfbuzz-suite.py
+++ b/text/harfbuzz-suite.py
@@ -133,8 +133,10 @@ def main():
counts = Counter(verdict.split(' ', 1)[0] for verdict in outcome.values())
is_known = lambda case: any(fnmatch.fnmatchcase(case, pattern) for pattern in known)
unexpected = sorted(case for case, verdict in outcome.items() if verdict != 'same' and not is_known(case))
- now_same = sorted(case for case, verdict in outcome.items() if verdict == 'same' and is_known(case) and
- case in known)
+ # a listed case, or pattern, all of whose cases now agree
+ now_same = sorted(pattern for pattern in known
+ if any(fnmatch.fnmatchcase(case, pattern) for case in outcome) and
+ all(verdict == 'same' for case, verdict in outcome.items() if fnmatch.fnmatchcase(case, pattern)))
for case in unexpected[:args.show]:
print(f'{case}: {outcome[case]}', file=sys.stderr)
print(details.get(case, ''), end='', file=sys.stderr)
diff --git a/text/shape-build.c b/text/shape-build.c
index 7cb251d..fac333b 100644
--- a/text/shape-build.c
+++ b/text/shape-build.c
@@ -914,11 +914,40 @@ bool vega_shaping_compile(hb_face_t *face, const uint32_t *scripts, size_t scrip
}
unsigned glyph_count = hb_face_get_glyph_count(face);
if (glyph_count > 65535) FAIL("more than 65535 glyphs");
- hb_face_collect_unicodes(face, unicodes);
- uint32_t cmap_count = 0;
- for (hb_codepoint_t u = HB_SET_VALUE_INVALID; hb_set_next(unicodes, &u);) {
+ /* The character map as HarfBuzz maps, not as it lists (a format 13 map
+ * it lists cut short, a symbol map it maps into the private use area):
+ * every code point asked, in groups of codes going to consecutive
+ * glyphs, or all to one. */
+ uint32_t *groups = NULL, group_count = 0, allocated = 0;
+ for (hb_codepoint_t u = 0; u <= 0x10FFFF; ++u) {
hb_codepoint_t g;
- if (hb_font_get_nominal_glyph(font, u, &g) && g) cmap_count++;
+ if (!hb_font_get_nominal_glyph(font, u, &g) || !g) continue;
+ hb_set_add(unicodes, u);
+ if (group_count) {
+ uint32_t *last = groups + 3 * (group_count - 1), glyph = last[2] & 0x7FFFFFFF;
+ bool single = last[0] == last[1], same = last[2] >> 31;
+ if (last[1] + 1 == u && ((single || !same) && g == glyph + (u - last[0]))) {
+ last[1] = u;
+ continue;
+ }
+ if (last[1] + 1 == u && (single || same) && g == glyph) {
+ last[1] = u;
+ last[2] = glyph | 0x80000000u;
+ continue;
+ }
+ }
+ if (group_count == allocated) {
+ allocated = allocated ? 2 * allocated : 256;
+ uint32_t *grown = realloc(groups, 3 * (size_t)allocated * sizeof(*groups));
+ if (!grown) {
+ free(groups);
+ FAIL("out of memory");
+ }
+ groups = grown;
+ }
+ uint32_t *next = groups + 3 * group_count++;
+ next[0] = next[1] = u;
+ next[2] = g;
}
hb_blob_t *tables[3] = {hb_face_reference_table(face, HB_OT_TAG_GDEF), hb_face_reference_table(face, HB_OT_TAG_GSUB),
hb_face_reference_table(face, HB_OT_TAG_GPOS)};
@@ -928,15 +957,10 @@ bool vega_shaping_compile(hb_face_t *face, const uint32_t *scripts, size_t scrip
put16(&o, hb_face_get_upem(face));
put16(&o, glyph_count);
for (int i = 0; i < 17; ++i) put32(&o, 0);
- set32(&o, 12, cmap_count);
+ set32(&o, 12, group_count);
set32(&o, 16, (uint32_t)o.size);
- for (hb_codepoint_t u = HB_SET_VALUE_INVALID; hb_set_next(unicodes, &u);) {
- hb_codepoint_t g;
- if (hb_font_get_nominal_glyph(font, u, &g) && g) {
- put32(&o, u);
- put32(&o, g);
- }
- }
+ for (uint32_t i = 0; i < 3 * group_count; ++i) put32(&o, groups[i]);
+ free(groups);
set32(&o, 20, (uint32_t)o.size);
for (unsigned g = 0; g < glyph_count; ++g) {
hb_position_t advance = hb_font_get_glyph_h_advance(font, g);
diff --git a/text/shape.c b/text/shape.c
index 0ea2968..926632d 100644
--- a/text/shape.c
+++ b/text/shape.c
@@ -180,22 +180,29 @@ bool vega_shaping_open(VegaShaping *s, const unsigned char *bytes, size_t size,
s->size = size;
s->upem = le16(bytes + 8);
s->glyph_count = le16(bytes + 10);
- s->cmap_count = le32(bytes + 12);
+ s->cmap_groups = le32(bytes + 12);
uint32_t cmap = le32(bytes + 16), advances = le32(bytes + 20);
if (!s->upem) REFUSE("units per em");
- if (!within(size, cmap, (uint64_t)s->cmap_count * 8)) REFUSE("character map");
+ if (!within(size, cmap, (uint64_t)s->cmap_groups * 12)) REFUSE("character map");
s->cmap = bytes + cmap;
- for (uint32_t i = 0; i < s->cmap_count; ++i) {
- uint32_t code = le32(s->cmap + 8 * i), glyph = le32(s->cmap + 8 * i + 4);
- if ((i && code <= le32(s->cmap + 8 * i - 8)) || !glyph || glyph >= s->glyph_count)
- REFUSE("character map: entry %u", i);
- }
- for (uint32_t i = 0; i < s->cmap_count; ++i) {
- uint32_t code = le32(s->cmap + 8 * i);
- if (code > 0xFFFF) break;
- uint16_t **page = &s->bmp[code >> 8];
- if (!*page && !(*page = calloc(256, sizeof(**page)))) REFUSE("out of memory");
- (*page)[code & 255] = (uint16_t)le32(s->cmap + 8 * i + 4);
+ for (uint32_t i = 0; i < s->cmap_groups; ++i) {
+ const unsigned char *g = s->cmap + 12 * (size_t)i;
+ uint32_t first = le32(g), last = le32(g + 4), glyph = le32(g + 8) & 0x7FFFFFFF;
+ bool same = le32(g + 8) >> 31;
+ if ((i && first <= le32(g - 8)) || last < first || last > 0x10FFFF || !glyph ||
+ (uint64_t)glyph + (same ? 0 : last - first) >= s->glyph_count)
+ REFUSE("character map: group %u", i);
+ }
+ for (uint32_t i = 0; i < s->cmap_groups; ++i) {
+ const unsigned char *g = s->cmap + 12 * (size_t)i;
+ uint32_t first = le32(g), last = le32(g + 4), glyph = le32(g + 8) & 0x7FFFFFFF;
+ bool same = le32(g + 8) >> 31;
+ if (first > 0xFFFF) break;
+ for (uint32_t code = first; code <= last && code <= 0xFFFF; ++code) {
+ uint16_t **page = &s->bmp[code >> 8];
+ if (!*page && !(*page = calloc(256, sizeof(**page)))) REFUSE("out of memory");
+ (*page)[code & 255] = (uint16_t)(same ? glyph : glyph + (code - first));
+ }
}
if (!within(size, advances, 2 * (uint64_t)s->glyph_count)) REFUSE("advances");
s->advances = bytes + advances;
@@ -272,12 +279,13 @@ void vega_shaping_close(VegaShaping *s) {
uint32_t vega_shaping_glyph(const VegaShaping *s, uint32_t code) {
if (code <= 0xFFFF) return s->bmp[code >> 8] ? s->bmp[code >> 8][code & 255] : 0;
- uint32_t low = 0, high = s->cmap_count;
+ uint32_t low = 0, high = s->cmap_groups;
while (low < high) {
- uint32_t middle = (low + high) / 2, c = le32(s->cmap + 8 * (size_t)middle);
- if (c < code) low = middle + 1;
- else if (c > code) high = middle;
- else return le32(s->cmap + 8 * (size_t)middle + 4);
+ uint32_t middle = (low + high) / 2;
+ const unsigned char *g = s->cmap + 12 * (size_t)middle;
+ if (le32(g + 4) < code) low = middle + 1;
+ else if (le32(g) > code) high = middle;
+ else return le32(g + 8) >> 31 ? le32(g + 8) & 0x7FFFFFFF : le32(g + 8) + (code - le32(g));
}
return 0;
}
diff --git a/text/shape.h b/text/shape.h
index cafa9b4..3fe3ff8 100644
--- a/text/shape.h
+++ b/text/shape.h
@@ -21,7 +21,11 @@
*
* The section, little-endian:
* "VSHP", u32 size, u16 upem, u16 glyph_count,
- * u32 cmap_count, u32 cmap_offset (u32 code, u32 glyph; by code),
+ * u32 cmap_groups, u32 cmap_offset (u32 first, u32 last code, u32
+ * glyph: first's, the next codes'
+ * following it, or with the top bit
+ * set, the glyph of them all; by
+ * code, apart),
* 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),
@@ -109,7 +113,7 @@ typedef struct {
const unsigned char *bytes;
size_t size;
uint16_t upem, glyph_count;
- uint32_t cmap_count, plan_count;
+ uint32_t cmap_groups, plan_count;
const unsigned char *cmap, *advances, *plans, *steps, *extents;
VegaTable gdef, gsub, gpos, kern;
VegaTable fallback; /* Arabic fallback shaping's lookups, a GSUB table */