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
|
# 002 — 심볼: 글리프와 아이콘을 한 바이너리 형식으로
2026-09-27. Pure [로드맵](/srv/git/pure.git) 중기 3 "단일 심볼 벡터 규격".
착수 기준:
- 바꿀 결정: 글자와 아이콘을 그릴 때 실행 중에 폰트 파일(TrueType 테이블, 힌팅 프로그램)과 SVG(XML과 경로 문자열)를
해석할지, 빌드할 때 한 번 곡선 레코드로 바꿔 두고 실행 중에는 한 번 검증한 뒤 제자리에서(매핑 그대로) 쓸지.
- 남은 질문: 2차 곡선 레코드만으로, GPU 셰이더로 옮길 수 있는 픽셀별 알고리즘이 FreeType만큼의 커버리지를 내는가?
같은 글리프를 담는 크기와 여는 비용은 폰트·SVG와 비교해 어떤가?
- 최소 확인: DejaVu Sans의 글리프를 여러 크기로 참조 래스터라이저와 FreeType(힌팅 없음, 8비트 회색)으로 그려 픽셀 차이를
재고, 크기와 여는 시간을 잰다. SVG 아이콘 몇 개를 같은 형식으로 바꿔 그려 본다.
- 중단 기준: FreeType과의 평균 차이가 256단계 중 8을 넘으면 형식이나 알고리즘을 다시 본다.
## 형식 (`VEGASYM1`, 리틀 엔디언)
| 오프셋 | 내용 |
|---|---|
| 0 | `VEGASYM1` |
| 8 | u16 단위(em당), u16 0 |
| 12 | u32 심볼 수, u32 곡선 수, u32 파일 크기, u32 0 |
| 28 | 심볼 × 24 B, 코드 순: u32 코드(글리프는 코드 포인트, 아이콘은 빌드 도구가 준 번호), u32 첫 곡선, u32 곡선 수, i16 경계 왼쪽·아래·오른쪽·위, i16 advance, u16 플래그(0) |
| 28 + 24n | 곡선 × 12 B: i16 x0 y0 x1 y1 x2 y2, 2차 베지에. 직선은 가운데 제어점을 끝점과 같게 둔다 |
- 좌표는 단위 공간, y가 위로 는다(폰트와 같다). 곡선은 심볼마다 닫힌 윤곽들을 이루고, 채우기는 0이 아닌 감김수다.
- 여는 쪽은 심볼마다 한 번 검증한다. 크기가 맞고, 코드가 오름차순이며, 곡선 범위가 곡선 표 안이고, 경계가 뒤집히지 않았다.
그다음 읽기는 파일 밖으로 나가지 않는다. 곡선 하나하나는 보지 않는다(경계를 벗어난 곡선은 잘못 그려질 뿐이다). 그래서 여는
비용은 곡선 수가 아니라 심볼 수에 비례한다. 곡선 표는 그대로 GPU의 저장 버퍼가 될 수 있는 고정 레코드다(해석 없이 올린다).
- 3차 곡선(OpenType CFF, SVG의 `C`)은 빌드할 때 2차 곡선들로 나눈다(단위 공간에서 오차 0.5 이하).
## 그리기
참조 래스터라이저(`vega_symbol_coverage`)는 GPU에서 픽셀마다 돌릴 계산을 그대로 C로 쓴 것이다. 픽셀이 덮인 넓이를 정확히 구한다.
곡선마다 픽셀 [0, 1]² 안에 든 부분에서 clamp(1 − x, 0, 1) dy를 적분해 더하면, 닫힌 윤곽 전체의 합이 덮인 넓이가 된다(그린 정리,
반시계면 음수). 곡선은 픽셀의 네 변을 지나는 t(2차 방정식의 근, 최대 8개)에서 나뉘고, 조각마다 픽셀의 왼쪽이면 dy를 통째로,
오른쪽이면 0을, 걸쳐 있으면 t의 3차 다항식을 닫힌 꼴로 적분한다. 겹친 윤곽은 감김수를 넓이로 더한 뒤 절댓값을 1로 자른다
(FreeType과 같다).
처음 계획은 Slug(Lengyel, JCGT 2017)처럼 픽셀 중심에서 +x와 +y로 광선을 쏘아 교차 위치를 픽셀 폭 안에서 잘라 더하고 두 값을
평균하는 방식이었다. DejaVu Sans ASCII에서 FreeType과의 평균 차이가 12px 15.4, 16px 12.3, 32px 6.8(최악 79–82)로 중단 기준을
넘었다. 한 줄의 1차원 커버리지 둘의 평균은 가는 가로·세로 획이 픽셀 중심을 지나느냐에 따라 크게 틀린다. 같은 정렬로 16×16
슈퍼샘플링을 재면 2.5, 정확 넓이는 1.8이어서, 정렬이 아니라 알고리즘 문제였다. 정확 넓이는 광선 방식보다 느리지도 않았다
(12px 글리프당 20 µs 대 21 µs, CPU 참조 구현).
## 측정 (Raspberry Pi 5, FreeType 힌팅 없음 8비트 회색, 폰트 원점에 맞춤)
| 글꼴 | 8px | 12px | 16px | 32px | 64px |
|---|---|---|---|---|---|
| DejaVu Sans ASCII (TrueType) 평균 차이 / 최악 | 1.86 / 15 | 1.82 / 16 | 1.50 / 19 | 0.69 / 21 | 0.40 / 26 |
| Nimbus Sans ASCII (CFF) | 3.48 / 31 | 3.19 / 33 | 2.67 / 37 | 1.53 / 47 | 0.80 / 46 |
| Baekmuk Batang 한글 11,172자 (TrueType) | | | 1.21 / 24 | | |
| Noto Sans CJK 한글 11,172자 (CFF) | | | 1.69 / 46 | | |
차이가 큰 픽셀을 64×64 슈퍼샘플링(곡선 256분할)과 대 보면 우리 값은 0.5단계 안에서 맞고 FreeType 값이 벗어나 있다. 남은
차이는 FreeType 쪽 곡선 근사(특히 CFF)다.
변환은 손실이 없다. 글꼴 단위의 두 배를 단위로 쓰면(TrueType의 암시 점, 두 제어점의 중점이 정수에 떨어진다) 글리프 넓이가
fontTools가 원본 glyf에서 잰 넓이와 0.03 단위² 안에서 같다. FreeType은 암시 점을 정수 나눗셈으로 구하므로 윤곽을 두 배로 늘린
뒤에 걷는다. CFF는 3차 곡선을 나누므로 em² 대비 평균 1.7e-5 차이다. SVG는 Adwaita 기호 아이콘 575개 중 458개를 변환했고
(불투명도 82, 변환 23, evenodd 11, 획 1은 거부), fontTools의 SVG 경로 해석과 넓이 차이가 아이콘 픽셀² 평균 0.035, 최악 0.22다
(16×16 아이콘에서 가장자리가 1/140 픽셀 움직인 정도).
크기와 읽는 비용(윤곽 표는 fontTools로 해당 글자만 남기고 힌팅을 뺀 부분 폰트의 glyf+loca 또는 CFF):
| 경우 | 곡선 | 심볼 파일 | 윤곽 표 | gzip 심볼 / 부분 폰트 | 윤곽 하나 (우리 / FreeType) |
|---|---|---|---|---|---|
| DejaVu ASCII | 1,463 | 19,864 B | 7,860 B | 10,059 / 6,466 B | 119 / 544 ns |
| Nimbus ASCII (CFF) | 1,720 | 22,948 B | 5,960 B | 12,160 / 5,851 B | 133 / 3,523 ns |
| Baekmuk 한글 | 1,927,588 | 23.4 MB | 9.55 MB | 6.85 / 1.98 MB | 836 / 4,208 ns |
| Noto CJK 한글 (CFF) | 569,159 | 7.10 MB | 1.08 MB | 2.43 / 0.80 MB | 324 / 9,352 ns |
| Adwaita 아이콘 458개 | 24,341 | 303 KB | SVG 452 KB | 115 / 205 KB | |
윤곽에 닿는 비용은 FreeType보다 4.6–29배 싸다(해석할 것이 없다). SVG보다는 작다. 그러나 폰트보다 2.1–6.6배 크다. 곡선마다
12바이트를 따로 두어 이웃 곡선이 끝점을 두 번 적고, 직선(Noto 한글은 66%)도 제어점을 가진다. 이것이 다음 질문이다. 윤곽을 점
사슬로 적어 끝점을 나누고 직선의 제어점을 빼면(TrueType의 점 모델을 압축 없이) 추정 크기는 DejaVu 11.7 KB, Noto 한글 3.5 MB다.
## 도구
`vega-symbols build [--font 폰트 --codes 0x20-0x7e,…] [--svg 번호=아이콘.svg …] [--units N] -o 심볼.vsym`: FreeType으로 글리프
윤곽을 단위 그대로(`FT_LOAD_NO_SCALE`) 읽고, SVG는 `path`의 `d`를 읽어 viewBox(없으면 width·height)의 높이를 em으로 맞춘다.
호(`A`)는 4분의 1 바퀴 이하의 3차 곡선으로, 3차 곡선은 2차 곡선으로 나눈다. 칠해진 경로만 받는다. 변환, 불투명도, evenodd,
획, 경로 밖의 도형을 쓰는 아이콘은 이름을 대고 거부한다(빠뜨리고 그리면 틀린 그림이 된다). 해석은 여기서 한 번이다.
`compare`(FreeType과 픽셀 차이, 여는 시간, 윤곽 하나에 닿는 시간), `info`(심볼마다 곡선 수, 경계, 넓이), `render`(PGM)는
검증과 측정용이다. `tests/check.py`의 10번이 이것들로 형식을 지킨다.
|