diff options
Diffstat (limited to 'docs/design')
| -rw-r--r-- | docs/design/002-symbol-format.md | 90 |
1 files changed, 54 insertions, 36 deletions
diff --git a/docs/design/002-symbol-format.md b/docs/design/002-symbol-format.md index 3f2c2a8..362798b 100644 --- a/docs/design/002-symbol-format.md +++ b/docs/design/002-symbol-format.md @@ -12,25 +12,34 @@ 재고, 크기와 여는 시간을 잰다. SVG 아이콘 몇 개를 같은 형식으로 바꿔 그려 본다. - 중단 기준: FreeType과의 평균 차이가 256단계 중 8을 넘으면 형식이나 알고리즘을 다시 본다. -## 형식 (`VEGASYM1`, 리틀 엔디언) +## 형식 (`VEGASYM2`, 리틀 엔디언) | 오프셋 | 내용 | |---|---| -| 0 | `VEGASYM1` | +| 0 | `VEGASYM2` | | 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 이하). +| 12 | u32 심볼 수, u32 점 수, u32 파일 크기, u32 0 | +| 28 | 심볼 × 24 B, 코드 순: u32 코드(글리프는 코드 포인트, 아이콘은 빌드 도구가 준 번호), u32 첫 점, u32 점 수, i16 경계 왼쪽·아래·오른쪽·위, i16 advance, u16 플래그(0) | +| 28 + 24n | 점 × 4 B: i16 2x + 곡선 밖, i16 2y + 윤곽 시작 | + +- 좌표는 단위 공간, y가 위로 는다(폰트와 같다). 채우기는 0이 아닌 감김수다. +- 윤곽은 점의 사슬이다(TrueType의 점 모델을 압축 없이). 윤곽은 곡선 위의 시작 점으로 열고, 그 점으로 돌아온 점으로 닫는다. 곡선 밖 + 점은 2차 곡선의 제어점이고, 곡선 밖 점 둘이 이어지면 그 가운데에 곡선 위 점이 있는 것으로 친다(적지 않는다). 곡선 위 점 둘이 + 이어지면 직선이다(제어점이 없다). +- 그래서 곡선 하나는 점 하나와 그 이웃 둘로 정해진다. 곡선 밖 점은 한 곡선의 제어점이고, 같은 윤곽의 곡선 위 점이 뒤따르는 곡선 + 위 점은 한 직선의 시작이다. GPU는 곡선 번호 대신 점 번호로 곡선을 가리키고 4바이트 셋을 읽는다. 점 하나가 32비트 한 워드라서 + 점 표가 그대로 `RG16I` 텍스처나 저장 버퍼가 된다(해석 없이 올린다). +- 여는 쪽은 심볼마다 한 번 검증한다. 크기가 맞고, 코드가 오름차순이며, 점 범위가 점 표 안이고, 경계가 뒤집히지 않았고, 첫 점이 + 곡선 위의 윤곽 시작이며 마지막 점이 곡선 위다. 그러면 곡선 밖 점의 이웃은 늘 같은 심볼 안에 있어서 그다음 읽기는 심볼 밖으로 + 나가지 않는다. 점 하나하나는 보지 않는다(경계를 벗어난 점은 잘못 그려질 뿐이다). 여는 비용은 점 수가 아니라 심볼 수에 비례한다. +- 좌표는 ±16,383 단위 안이다(플래그가 한 비트씩 쓴다). 빌드 도구는 모든 점이 짝수인 동안 단위를 반으로 줄이므로 TrueType + 글꼴은 제 단위로 손실 없이 담긴다. +- 3차 곡선(OpenType CFF, SVG의 `C`와 호)은 빌드할 때 2차 곡선 사슬로 바꾼다. cu2qu(fontTools)의 방법으로, 이음점이 이웃 제어점의 + 가운데에 오게 해서(적지 않는다) 오차 1단위 안에 드는 가장 적은 조각을 고른다. ## 그리기 -참조 래스터라이저(`vega_symbol_coverage`)는 GPU에서 픽셀마다 돌릴 계산을 그대로 C로 쓴 것이다. 픽셀이 덮인 넓이를 정확히 구한다. +참조 래스터라이저(`vega_coverage`)는 GPU에서 픽셀마다 돌릴 계산을 그대로 C로 쓴 것이다. 픽셀이 덮인 넓이를 정확히 구한다. 곡선마다 픽셀 [0, 1]² 안에 든 부분에서 clamp(1 − x, 0, 1) dy를 적분해 더하면, 닫힌 윤곽 전체의 합이 덮인 넓이가 된다(그린 정리, 반시계면 음수). 곡선은 픽셀의 네 변을 지나는 t(2차 방정식의 근, 최대 8개)에서 나뉘고, 조각마다 픽셀의 왼쪽이면 dy를 통째로, 오른쪽이면 0을, 걸쳐 있으면 t의 3차 다항식을 닫힌 꼴로 적분한다. 겹친 윤곽은 감김수를 넓이로 더한 뒤 절댓값을 1로 자른다 @@ -46,39 +55,48 @@ | 글꼴 | 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 | +| DejaVu Sans ASCII (TrueType) 평균 차이 / 최악 | 1.86 / 15 | 1.83 / 17 | 1.51 / 20 | 0.70 / 23 | 0.40 / 26 | +| Nimbus Sans ASCII (CFF) | 3.50 / 31 | 3.22 / 33 | 2.71 / 38 | 1.57 / 47 | 0.83 / 49 | | Baekmuk Batang 한글 11,172자 (TrueType) | | | 1.21 / 24 | | | -| Noto Sans CJK 한글 11,172자 (CFF) | | | 1.69 / 46 | | | +| Noto Sans CJK 한글 11,172자 (CFF) | | | 1.71 / 45 | | | 차이가 큰 픽셀을 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다. +변환은 TrueType에서 손실이 없다. 글리프 넓이가 fontTools가 원본 glyf에서 잰 넓이와 1단위² 안에서 같고 advance가 hmtx와 같다 +(검사가 지킨다). FreeType은 암시 점을 정수 나눗셈으로 구하므로 윤곽을 두 배로 늘린 뒤에 걷고, 다 쓴 뒤 단위를 되돌린다. CFF는 +em² 대비 평균 2.6e-5, 최악 1.5e-4 차이다. SVG는 Adwaita 기호 아이콘 575개 중 458개를 변환했고(불투명도 82, 변환 23, evenodd +11, 획 1은 거부), fontTools의 SVG 경로 해석과 넓이 차이가 아이콘 픽셀² 평균 0.013, 최악 0.19다. + +크기와 읽는 비용. 윤곽 표는 fontTools로 해당 글자만 남기고 힌팅을 뺀 부분 폰트의 glyf+loca 또는 CFF다. 1판은 곡선마다 12바이트 +레코드였다. + +| 경우 | 점 / 곡선 | 1판 | 2판 | 윤곽 표 | gzip 2판 / 부분 폰트 | 윤곽 하나 (우리 / FreeType) | +|---|---|---|---|---|---|---| +| DejaVu ASCII | 1,933 / 1,463 | 19,864 B | 10,040 B | 7,860 B | 6,944 / 6,465 B | 271 / 539 ns | +| Nimbus ASCII (CFF) | 2,444 / 1,873 | 22,948 B | 12,084 B | 5,960 B | 8,726 / 5,851 B | 334 / 3,529 ns | +| Baekmuk 한글 | 3.28M / 1.93M | 23.4 MB | 13.4 MB | 9.55 MB | 5.50 / 1.98 MB | 3.6 / 4.2 µs | +| Noto CJK 한글 (CFF) | 723K / 598K | 7.10 MB | 3.16 MB | 1.08 MB | 1.37 / 0.80 MB | 1.0 / 9.3 µs | +| Adwaita 아이콘 458개 | | 303 KB | 173 KB | SVG 452 KB | 97 / 205 KB | | + +- 여는 비용: 2판은 DejaVu ASCII 2.5 µs(FreeType 12.7 µs), 한글 11,172자 0.34 ms(FreeType 0.03–0.39 ms). 1판은 곡선마다 경계를 + 검사해 한글에서 19 ms였다. +- 크기: 점을 나누고 직선의 제어점과 암시 점을 빼서 1판의 절반 안팎이 되었다. TrueType 원본의 1.3–1.4배(부분 폰트 파일 전체와는 + 비슷하다), CFF의 2.0–2.9배다. CFF는 3차 곡선과 서브루틴, 가변 길이 정수로 적으므로 고정 크기 점으로는 따라가기 어렵다. 전송은 + gzip이 차이를 줄인다. SVG보다는 2.6배 작다. +- 윤곽 하나에 닿는 비용: 점마다 이웃을 보고 암시 점을 만들어야 해서 1판(곡선을 그대로 읽는다)보다 느려졌다. 곡선 위·밖 플래그가 + 데이터에 따라 바뀌어 분기 예측이 틀리므로 해독은 분기 없이 산술로 한다(점당 9.7 ns). 그래도 TrueType보다 1.2–2배, CFF보다 + 9–10배 싸다. GPU는 해독하지 않고 점을 그대로 읽는다. +- 참조 래스터라이저: 16px 글리프당 24 µs(CPU, 픽셀마다 모든 곡선). GPU에서 띠(band)로 곡선을 추리는 것이 다음이다. + +처음 계획은 12바이트 곡선 레코드(1판)였다. 윤곽에 닿는 비용은 더 쌌지만(119 ns) 폰트의 2.1–6.6배 크기였고, 여는 비용이 곡선 수에 +비례했다. "초경량"이라는 목표에 맞지 않아서 점 사슬로 바꿨다. ## 도구 `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으로 맞춘다. +윤곽을 단위 그대로(`FT_LOAD_NO_SCALE`) 읽고(단위는 글꼴 단위의 두 배에서 시작해 줄인다. 아이콘만이면 4,096), SVG는 `path`의 `d`를 읽어 viewBox(없으면 width·height)의 높이를 em으로 맞춘다. 호(`A`)는 4분의 1 바퀴 이하의 3차 곡선으로, 3차 곡선은 2차 곡선으로 나눈다. 칠해진 경로만 받는다. 변환, 불투명도, evenodd, 획, 경로 밖의 도형을 쓰는 아이콘은 이름을 대고 거부한다(빠뜨리고 그리면 틀린 그림이 된다). 해석은 여기서 한 번이다. -`compare`(FreeType과 픽셀 차이, 여는 시간, 윤곽 하나에 닿는 시간), `info`(심볼마다 곡선 수, 경계, 넓이), `render`(PGM)는 +`compare`(FreeType과 픽셀 차이, 여는 시간, 윤곽 하나에 닿는 시간), `info`(심볼마다 점·곡선 수, 경계, 넓이), `render`(PGM)는 검증과 측정용이다. `tests/check.py`의 10번이 이것들로 형식을 지킨다. |
