aboutsummaryrefslogtreecommitdiffstats
path: root/docs/design/004-text.md
blob: b95a205fc46a490754fd80a45dbba386c625c8b6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# 004 — 글자: 양방향, 합성, 셰이핑을 빌드할 때 컴파일한 표로

2026-09-28. Pure 외부 피드백 002의 셋째 항목. [002](002-symbol-format.md)는 FreeType을 실행 중에서 빼고 글꼴을
빌드할 때 곡선 레코드로 바꿨다. 그 대가로 Vega는 코드 포인트 하나를 글리프 하나로 그렸다. 합자, 커닝, 아랍 문자의 위치별
형태, 결합 부호의 자리, 오른쪽에서 왼쪽으로 쓰는 글(양방향)이 없었다.

착수 기준:

- 바꿀 결정: 셰이핑을 실행 중에 OpenType 표(GSUB, GPOS)를 해석해서 할지(HarfBuzz의 방식), 빌드할 때 글꼴의 행동을
  단순한 표로 컴파일해 두고 실행 중에는 검증한 표를 제자리에서 읽기만 할지. 심볼과 같은 원칙이면 뒤쪽이다. (결과: 둘을
  나눴다. 계획은 빌드할 때 컴파일하고, 조회는 OpenType 그대로 읽는다. 아래 셰이핑 절.)
- 남은 질문: 컴파일한 표만으로 HarfBuzz와 같은 글리프와 자리가 나오는가? 어느 문자 체계까지인가? 양방향과 합성은 유니코드의
  적합성 시험을 모두 통과하는가?
- 최소 확인: 유니코드 15.1의 `BidiTest.txt`(49만 줄)와 `BidiCharacterTest.txt`(9.6만 줄), `NormalizationTest.txt`를 모두
  통과한다. 셰이핑은 라틴(커닝, 합자), 아랍(위치별 형태, lam-alef, 결합 부호), 히브리(결합 부호) 문장에서 HarfBuzz 10.2와
  글리프와 자리가 같다.
- 중단 기준: 적합성 시험에 틀린 줄이 하나라도 있으면 그 단계를 쓰지 않는다. 셰이핑이 HarfBuzz와 다른 문장은 원인을 찾을
  때까지 그 문자 체계를 받지 않는다(빌드 도구가 거부한다).

## 단계

1. 유니코드 성질 표: UCD에서 생성한 두 단계 표(코드 포인트 → 값). [text/unicode.py](../../text/unicode.py)가
   [text/unicode-data.c](../../text/unicode-data.c)를 만든다.
2. 양방향(UAX #9): 문단 수준, 명시적 수준과 격리(X1–X10), 약한 형(W1–W7), 괄호 쌍(N0), 중립(N1–N2), 암시적 수준(I1–I2),
   줄(L1)과 보이는 순서(L2).
3. 정규화: 한글 음절과 정준 합성(NFC).
4. 셰이핑: 빌드 도구가 글꼴의 셰이핑 계획을 컴파일해 배치 표와 함께 심볼 파일에 넣는다. 실행 중에는 HarfBuzz의 절차를
   그대로 옮긴 엔진이 그것을 읽는다. 빌드 도구는 HarfBuzz와 결과가 다른 파일을 만들지 않는다.
5. 비례폭 글줄: 셰이핑한 글리프를 제 advance로 늘어놓아 그린다.
6. 접근성.

## 양방향

[text/bidi.c](../../text/bidi.c)는 UAX #9를 규칙 이름 그대로 짠다. 문단 수준(P2–P3), 명시적 수준과 방향 상태 스택(깊이
125, 넘친 격리·내장·유효 격리의 계수, X1–X8), 제거(X9)와 격리 실행 열(X10: 격리 시작자로 끝나는 실행은 짝 PDI로 시작하는
실행으로 이어지고, sos와 eos는 명시적 수준으로 정한다), 약한 형(W1–W7), 괄호 쌍(N0: 깊이 63의 스택, U+2329·232A와
U+3008·3009의 정준 동치, 바뀐 괄호 뒤의 결합 부호), 중립(N1–N2), 암시적 수준(I1–I2), 그리고 한 줄로서의 L1과 보이는
순서(L2)다. 성질은 UCD에서 만든 표다(양방향 클래스는 두 단계 표 37 KiB, 괄호 128개, 거울 글자 428개).

유니코드 15.1의 적합성 시험을 모두 통과한다. `BidiTest.txt`의 770,241 경우(클래스 열마다 비트 집합이 가리키는 문단 방향)와
`BidiCharacterTest.txt`의 91,707 경우(코드 포인트, 괄호 포함)에서 수준과 보이는 순서가 하나도 틀리지 않는다. Pi 5에서 전체가
0.74초다. 처음 짠 것은 148 경우가 틀렸다. 앞선 격리 실행 열의 암시적 수준(I1–I2)이 바꾼 수준으로 다음 열의 sos를 정했기
때문이다(sos와 eos는 명시적 수준으로 정해야 한다).

## 정규화

[text/normalize.c](../../text/normalize.c)는 NFD와 NFC다. 코드 포인트를 끝까지 분해하고(표에 다 풀어 둔 정준 분해 2,061개,
한글 음절은 산술로), 결합 등급으로 부호를 안정 정렬한 뒤, 마지막 기저 문자와 막히지 않은 문자를 합친다(원시 합성 941쌍,
합성 제외 목록 반영, 한글 L+V와 LV+T는 산술로). 코드 포인트 하나는 많아야 넷으로 풀린다(UCD 전체를 확인했다).

`NormalizationTest.txt`의 19,074줄(줄마다 NFC와 NFD 16가지 대조)과, 그 1부에 없는 코드 포인트 1,095,035개가 제 자신의
NFC이자 NFD라는 것을 모두 지킨다. 적합성 시험 셋을 합쳐 Pi 5에서 0.9초다.

## 줄 바꿈과 분절

글줄을 채우려면 어디서 줄을 바꿀 수 있는지(UAX #14)가 필요하다. 커서와 선택, 읽어 주기에는 자소 묶음, 낱말, 문장의
경계(UAX #29)가 필요하다. 모두 성질 표(Line_Break, Grapheme_Cluster_Break와 Indic_Conjunct_Break, Word_Break,
Sentence_Break, 동아시아 폭이 F·W·H인 여는·닫는 괄호)와 규칙을 순서대로 적용하는 코드다. 각 경계는 규칙 가운데 그것을 처음으로
정하는 것이 정한다.

- [text/linebreak.c](../../text/linebreak.c): LB1–LB31. 결합 부호를 앞 글자에 붙이고(LB9), 공백을 건너 뒤를 보는
  규칙(LB8, LB14, LB15a, LB16, LB17), 숫자 규칙(LB25는 유니코드의 시험처럼 8.2절 예 7로 맞춘 것), 브라흐미 계열
  음절(LB28a), 지역 표시 짝(LB30a)을 한 번 앞에서 뒤로 훑어 만든 배열로 본다.
- [text/grapheme.c](../../text/grapheme.c): GB1–GB999. 인도계 결합 자음(GB9c), 이모지 ZWJ 열(GB11), 지역 표시 짝(GB12–13)은
  훑으며 상태 셋으로 본다.
- [text/word.c](../../text/word.c): WB1–WB999. 확장·서식·ZWJ를 앞 글자에 접고(WB4), 앞뒤로 한 단위씩 더 본다.
- [text/sentence.c](../../text/sentence.c): SB1–SB998. 문장 끝 뒤의 닫는 부호와 공백은 훑으며 기억하고, SB8의 앞보기는
  뒤에서 앞으로 한 번 훑어 위치마다 미리 구한다. 모두 선형 시간이다.

유니코드 15.1의 적합성 시험을 모두 통과한다. `LineBreakTest.txt` 10,274 경우, `GraphemeBreakTest.txt` 1,187 경우,
`WordBreakTest.txt` 1,826 경우, `SentenceBreakTest.txt` 512 경우에서 틀린 경계가 하나도 없다.

## 셰이핑

처음 계획은 HarfBuzz가 표본 문자열(글자 쌍, 부호 하나 붙은 글자, 위치별 형태)을 셰이핑한 결과를 표로 뽑는 것이었다. 글꼴을
열어 보고 버렸다. DejaVu Sans 한 벌에만 표본으로 잡히지 않는 규칙이 여럿이다.

- 라틴의 `ccmp`: i와 j는 위 부호가 뒤에 오면 점 없는 꼴이 된다. 사이에 아래 부호가 둘까지 있어도 그렇고, 셋이면 안 된다.
  대문자 뒤의 위 부호는 대문자용 꼴이 된다.
- 히브리: ayin 뒤에 아래 부호가 오면 ayin이 다른 꼴이 된다. yod와 hiriq는 한 글리프가 된다.
- 아랍: 부호끼리의 합자가 있다(shadda와 fatha). lam-alef는 사이의 부호를 건너뛰어 합쳐지고, 그 부호는 합자의 어느 성분에
  붙었는지를 따라간다(mark-to-ligature).

이런 문맥은 규칙 자체를 옮기지 않고는 정확히 재현할 수 없다. 그래서 나눴다.

- 빌드할 때: 계획을 컴파일한다. 문자 체계와 방향마다 HarfBuzz의 계획기가 하는 일을 그대로 한다. 기능을 모으고(기본, 아랍,
  히브리 셰이퍼의 목록과 단계), 중복을 합치고, 마스크 비트를 나누고, 글꼴의 문자 체계·언어 체계에서 기능을 찾고, 단계별로
  조회를 모아 정렬해 합친다. 결과는 조회 번호, 마스크, ZWJ/ZWNJ 규칙의 목록이다. 글꼴의 문자 지도와 advance, GDEF·GSUB·GPOS
  표는 그대로 싣는다([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, 필기체, 기저·합자·부호에 붙는 부호), 부착 오프셋의 전파, 보이지 않는 문자
  숨기기를 한다. 표는 제자리에서 읽고, 읽을 때마다 표의 끝을 넘는지 확인한다. 망가진 표는 틀리게 셰이핑할 뿐 밖을 읽지
  않는다.

심볼 파일은 VEGASYM4가 됐다. 헤더의 깃발이 점들 뒤에 셰이핑 구역이 있다고 알린다. 셰이핑이 닿는 글리프(문자 지도,
분해와 합성, 거울, GSUB의 닫힘)는 `0x110000 + 글리프 번호`로 심볼이 된다. 같은 글리프의 심볼들은 점을 나눠 쓴다.

HarfBuzz와 다른 파일은 만들어지지 않는다. `vega-symbols build --shape`는 파일을 쓴 뒤 계획마다 무작위 실행을 HarfBuzz
10.2와 함께 셰이핑한다. 실행은 두 종류다. 하나는 낱말들(글꼴의 치환이 시작되는 글자를 자주 섞고, 부호를 하나에서
셋까지 붙이고, ZWJ·ZWNJ를 끼운다)이고, 다른 하나는 아무 순서의 문자(맨 앞의 부호, 쌓인 부호)다. 글리프, 클러스터,
advance, 오프셋 가운데 하나라도 다른 실행이 있으면 파일을 지운다.

결과(Pi 5):

- DejaVu Sans, 라틴·그리스·키릴·히브리·아랍·Common, 양방향 12개 계획에 각 5만 실행(60만 실행, 1,270만 문자): 다른
  실행 0.
- 설치된 글꼴 전부(Free, Liberation, Lato, Noto Sans Mono, URW, DejaVu 계열 등 60여 벌), 계획마다 1천 실행: 받아들인
  글꼴은 모두 다른 실행 0. 한 번 달랐다. 짝 위치 형식 2에서 두 번째 글리프의 등급이 0이면 다음 하위 표로 넘어가게 짰는데,
  HarfBuzz 10.2는 그 하위 표를 적용하고 멈춘다(FreeSerif의 y와 마침표 커닝이 40 단위 달랐다). 고친 뒤 FreeSerif, FreeMono,
  DejaVu Sans Bold, Liberation Mono, Lato에 계획마다 2만 실행으로 다시 확인했다.
- 속도: 조회마다, 하위 표마다 시작 글리프의 요약(HarfBuzz의 set digest: 64비트 무늬 셋)을 열 때 만들어 두면, 버퍼에 없는
  글리프로 시작하는 조회와 하위 표를 건너뛴다. 문자 지도는 BMP를 쪽마다 바로 찾는다. 이것으로 HarfBuzz의 1.05–1.18배
  시간이다(문자당 라틴 422 대 404 ns, 아랍 428 대 371 ns, 히브리 313 대 274 ns). 요약 전에는 2.5배였다.

받지 않는 것(빌드 도구가 이유를 말하고 거부한다):

- GPOS가 없는 글꼴. HarfBuzz는 결합 등급과 글리프 외곽 상자로 부호를 놓는다(fallback positioning). CJK 글꼴, Nimbus Mono,
  Noto Mono 등이 여기에 걸린다.
- 옛 `kern` 표로 커닝하는 글꼴(Liberation Sans·Serif, FreeSans).
- AAT 표(morx, kerx, trak), 늘이기(stch), 아랍 형태가 없어 HarfBuzz가 대체 셰이핑을 하는 글꼴.
- 기본·아랍·히브리 셰이퍼가 아닌 문자 체계: 인도계, 태국·라오, 한글 자모, 크메르, 미얀마, USE 문자 체계들, 시리아.
- 이체 선택자(cmap 형식 14)는 싣지 않는다. 선택자는 숨기고 앞 글자는 기본 글리프로 둔다.