aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-27 08:44:42 +0100
committereunhhu <[email protected]>2026-09-27 08:44:42 +0100
commit523b810d6a9c60342c7f5d74f36fe66f3f2057e9 (patch)
treec716fb00013264fdbfe7a95dc3f4a43c50b01ecc
parent65a208f7a88e0aeea0d9db4666fd171f59b2a1d2 (diff)
downloadvega-523b810d6a9c60342c7f5d74f36fe66f3f2057e9.tar.gz
vega-523b810d6a9c60342c7f5d74f36fe66f3f2057e9.zip
Draw glyphs and icons from one symbol format, by exact pixel area
Symbol files (VEGASYM1) hold glyphs and icons as quadratic curves, verified once per symbol and then read in place. vega-symbols builds them from fonts (through FreeType, losslessly: twice the font's units keeps TrueType's implied points whole) and from solid SVG paths (arcs included), refusing by name what an icon asks for that symbols cannot hold. The reference rasterizer computes each pixel's covered area exactly: every curve's integral of clamp(1 - x) dy inside the pixel, split where it crosses the pixel's sides. The Slug-style two-ray average first tried missed FreeType by 15.4 levels on average at 12 px; exact area is within 1.2-3.5 at 8-16 px, and where it differs most it matches 64x64 supersampling, not FreeType. Reaching an outline costs 4.6-29x less than through FreeType, but the files are 2.1-6.6x larger than glyf or CFF: design 002 has the numbers and the next question (points shared along contours, lines without control points).
-rw-r--r--Makefile12
-rw-r--r--README.md9
-rw-r--r--docs/design/002-symbol-format.md84
-rw-r--r--src/symbols-tool.c628
-rw-r--r--src/symbols.c128
-rw-r--r--src/symbols.h38
-rw-r--r--tests/check.py92
-rw-r--r--tests/symbols/circle.svg3
-rw-r--r--tests/symbols/frame.svg3
-rw-r--r--tests/symbols/rounded.svg1
-rw-r--r--tests/symbols/turned.svg3
11 files changed, 998 insertions, 3 deletions
diff --git a/Makefile b/Makefile
index bc73568..8cbdff8 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ PURE_SOURCES = $(PURE)/module.c $(PURE)/module-list.c $(PURE)/module-save.c $(PU
SOURCES = src/screen.c src/draw.c src/width.c src/web.c src/main.c
.PHONY: all check
-all: $(BUILD)/vega
+all: $(BUILD)/vega $(BUILD)/vega-symbols
$(BUILD):
mkdir -p $@
@@ -19,7 +19,13 @@ $(BUILD):
$(BUILD)/vega: $(SOURCES) src/vega.h $(PURE_SOURCES) | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) -I$(PURE) $(SOURCES) $(PURE_SOURCES) $(LDFLAGS) -lm $(LDLIBS) -o $@
-check: $(BUILD)/vega
+# Building symbol files reads fonts through FreeType; reading them does not.
+FREETYPE_CFLAGS ?= $(shell pkg-config --cflags freetype2)
+FREETYPE_LIBS ?= $(shell pkg-config --libs freetype2)
+$(BUILD)/vega-symbols: src/symbols-tool.c src/symbols.c src/symbols.h | $(BUILD)
+ $(CC) $(CPPFLAGS) $(CFLAGS) $(FREETYPE_CFLAGS) src/symbols-tool.c src/symbols.c $(LDFLAGS) $(FREETYPE_LIBS) -lm $(LDLIBS) -o $@
+
+check: $(BUILD)/vega $(BUILD)/vega-symbols
$(MAKE) -C $(AUP) build/aup
$(MAKE) -C $(PURE) build/pure-serve
- python3 tests/check.py --vega $(BUILD)/vega --aup $(AUP)/build/aup --serve $(PURE)/build/pure-serve
+ python3 tests/check.py --vega $(BUILD)/vega --symbols $(BUILD)/vega-symbols --aup $(AUP)/build/aup --serve $(PURE)/build/pure-serve
diff --git a/README.md b/README.md
index 0ab0430..6e92981 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,15 @@ curl -s 'localhost:8767/views/cards_list?from=0&count=20'
한 저장소는 한 번에 한 호스트가 연다(상태의 소유자는 하나다).
+글자와 아이콘은 하나의 바이너리 형식(심볼 파일)으로 그린다. 폰트와 SVG는 빌드할 때 한 번만 해석한다
+([설계 002](docs/design/002-symbol-format.md)).
+
+```sh
+build/vega-symbols build --font DejaVuSans.ttf --codes 0x20-0x7e --svg 0xE000=icon.svg -o ui.vsym
+build/vega-symbols compare DejaVuSans.ttf ui.vsym --sizes 12,16,32 # FreeType과 픽셀 차이, 읽는 비용
+build/vega-symbols render ui.vsym 0xE000 48 icon.pgm
+```
+
## 위치
| 층 | 역할 | 비유 |
diff --git a/docs/design/002-symbol-format.md b/docs/design/002-symbol-format.md
new file mode 100644
index 0000000..3f2c2a8
--- /dev/null
+++ b/docs/design/002-symbol-format.md
@@ -0,0 +1,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번이 이것들로 형식을 지킨다.
diff --git a/src/symbols-tool.c b/src/symbols-tool.c
new file mode 100644
index 0000000..bec8656
--- /dev/null
+++ b/src/symbols-tool.c
@@ -0,0 +1,628 @@
+#define _POSIX_C_SOURCE 200809L
+/* vega-symbols: build symbol files from fonts and SVG icons, and measure them
+ * against FreeType (docs/design/002-symbol-format.md).
+ * vega-symbols build [--font FONT.ttf --codes 0x20-0x7e,...] [--svg CODE=ICON.svg ...] [--units N] -o OUT.vsym
+ * vega-symbols compare FONT.ttf OUT.vsym --sizes 12,16,32,64
+ * vega-symbols info OUT.vsym
+ * vega-symbols render OUT.vsym CODE PIXELS OUT.pgm */
+#include "symbols.h"
+#include <ft2build.h>
+#include FT_FREETYPE_H
+#include FT_OUTLINE_H
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#define PI 3.14159265358979323846
+
+typedef struct { double x, y; } Point;
+typedef struct { Point p[3]; } Quad;
+typedef struct { Quad *items; size_t count, capacity; Point at, start; bool open; double font_scale; } Path;
+typedef struct { uint32_t code; size_t first, count; int16_t advance; } Built;
+
+static void fail(const char *why) {
+ fprintf(stderr, "vega-symbols: %s\n", why);
+ exit(1);
+}
+
+static void add_quad(Path *path, Point a, Point b, Point c) {
+ if (path->count == path->capacity) {
+ path->capacity = path->capacity ? 2 * path->capacity : 64;
+ if (!(path->items = realloc(path->items, path->capacity * sizeof(*path->items)))) fail("out of memory");
+ }
+ path->items[path->count++] = (Quad){{a, b, c}};
+}
+
+/* A line is a quadratic whose control point is its middle. */
+static void line_to(Path *path, Point to) {
+ if (to.x != path->at.x || to.y != path->at.y)
+ add_quad(path, path->at, (Point){(path->at.x + to.x) / 2, (path->at.y + to.y) / 2}, to);
+ path->at = to;
+}
+
+static void quad_to(Path *path, Point control, Point to) {
+ add_quad(path, path->at, control, to);
+ path->at = to;
+}
+
+/* A cubic as quadratics, within half a unit: split in n equal pieces, each
+ * approximated by the quadratic through its ends with the control point
+ * (3(c1 + c2) - (p0 + p3)) / 4. The error shrinks with the cube of n. */
+static void cubic_to(Path *path, Point c1, Point c2, Point to) {
+ Point p0 = path->at;
+ double dx = to.x - 3 * c2.x + 3 * c1.x - p0.x, dy = to.y - 3 * c2.y + 3 * c1.y - p0.y;
+ double error = sqrt(3) / 36 * sqrt(dx * dx + dy * dy);
+ int n = (int)ceil(cbrt(error / 0.5));
+ if (n < 1) n = 1;
+ if (n > 64) n = 64;
+ for (int i = 0; i < n; ++i) {
+ double t0 = (double)i / n, t1 = (double)(i + 1) / n;
+ Point q[4];
+ double ts[2] = {t0, t1};
+ /* The piece's points by de Casteljau at t0 and t1, and its control points. */
+ Point e[2];
+ for (int k = 0; k < 2; ++k) {
+ double t = ts[k], u = 1 - t;
+ e[k] = (Point){u * u * u * p0.x + 3 * u * u * t * c1.x + 3 * u * t * t * c2.x + t * t * t * to.x,
+ u * u * u * p0.y + 3 * u * u * t * c1.y + 3 * u * t * t * c2.y + t * t * t * to.y};
+ }
+ /* Derivatives give the piece's cubic control points. */
+ double h = t1 - t0;
+ for (int k = 0; k < 2; ++k) {
+ double t = ts[k], u = 1 - t;
+ Point d = {3 * (u * u * (c1.x - p0.x) + 2 * u * t * (c2.x - c1.x) + t * t * (to.x - c2.x)),
+ 3 * (u * u * (c1.y - p0.y) + 2 * u * t * (c2.y - c1.y) + t * t * (to.y - c2.y))};
+ q[k ? 2 : 1] = k ? (Point){e[1].x - d.x * h / 3, e[1].y - d.y * h / 3} : (Point){e[0].x + d.x * h / 3, e[0].y + d.y * h / 3};
+ }
+ q[0] = e[0];
+ q[3] = e[1];
+ Point control = {(3 * (q[1].x + q[2].x) - (q[0].x + q[3].x)) / 4, (3 * (q[1].y + q[2].y) - (q[0].y + q[3].y)) / 4};
+ path->at = q[0];
+ quad_to(path, control, q[3]);
+ }
+ path->at = to;
+}
+
+static void close_path(Path *path) {
+ if (path->open) line_to(path, path->start);
+ path->open = false;
+}
+
+static void move_to(Path *path, Point to) {
+ close_path(path);
+ path->at = path->start = to;
+ path->open = true;
+}
+
+/* ---- Fonts, through FreeType's outline walk (in its units, times font_scale). */
+
+static Point point_of(const Path *path, const FT_Vector *v) {
+ return (Point){(double)v->x * path->font_scale, (double)v->y * path->font_scale};
+}
+static int ft_move(const FT_Vector *to, void *user) { move_to(user, point_of(user, to)); return 0; }
+static int ft_line(const FT_Vector *to, void *user) { line_to(user, point_of(user, to)); return 0; }
+static int ft_conic(const FT_Vector *c, const FT_Vector *to, void *user) {
+ quad_to(user, point_of(user, c), point_of(user, to));
+ return 0;
+}
+static int ft_cubic(const FT_Vector *c1, const FT_Vector *c2, const FT_Vector *to, void *user) {
+ cubic_to(user, point_of(user, c1), point_of(user, c2), point_of(user, to));
+ return 0;
+}
+
+/* ---- SVG icons: the d of each path, the viewBox's height as the em. */
+
+static const char *number(const char *p, double *out) {
+ while (*p == ' ' || *p == ',' || *p == '\n' || *p == '\t' || *p == '\r') ++p;
+ char *end;
+ *out = strtod(p, &end);
+ return end == p ? NULL : end;
+}
+
+/* An arc's flags are single digits, perhaps with no space between them. */
+static const char *flag(const char *p, double *out) {
+ while (*p == ' ' || *p == ',' || *p == '\n' || *p == '\t' || *p == '\r') ++p;
+ if (*p != '0' && *p != '1') return NULL;
+ *out = *p - '0';
+ return p + 1;
+}
+
+typedef struct { double x, y, width, height, scale; } Box;
+
+static Point svg_point(const Box *box, double x, double y) {
+ return (Point){(x - box->x) * box->scale, (box->y + box->height - y) * box->scale};
+}
+
+/* An elliptical arc from (x1, y1) to (x2, y2) as cubics of at most a quarter
+ * turn each (SVG 1.1, F.6.5: from its endpoints to its center). */
+static void arc_to(Path *path, const Box *box, double x1, double y1, double rx, double ry, double degrees, bool large,
+ bool sweep, double x2, double y2) {
+ rx = fabs(rx);
+ ry = fabs(ry);
+ if ((x1 == x2 && y1 == y2)) return;
+ if (!rx || !ry) {
+ line_to(path, svg_point(box, x2, y2));
+ return;
+ }
+ double phi = degrees * PI / 180, cp = cos(phi), sp = sin(phi);
+ double hx = (x1 - x2) / 2, hy = (y1 - y2) / 2, px = cp * hx + sp * hy, py = -sp * hx + cp * hy;
+ double lambda = px * px / (rx * rx) + py * py / (ry * ry);
+ if (lambda > 1) { rx *= sqrt(lambda); ry *= sqrt(lambda); }
+ double num = rx * rx * ry * ry - rx * rx * py * py - ry * ry * px * px, den = rx * rx * py * py + ry * ry * px * px;
+ double k = (large == sweep ? -1 : 1) * sqrt(fmax(num / den, 0));
+ double ccx = k * rx * py / ry, ccy = -k * ry * px / rx;
+ double cx = cp * ccx - sp * ccy + (x1 + x2) / 2, cy = sp * ccx + cp * ccy + (y1 + y2) / 2;
+ double start = atan2((py - ccy) / ry, (px - ccx) / rx), turn = atan2((-py - ccy) / ry, (-px - ccx) / rx) - start;
+ if (sweep && turn < 0) turn += 2 * PI;
+ if (!sweep && turn > 0) turn -= 2 * PI;
+ int n = (int)ceil(fabs(turn) / (PI / 2) - 1e-9);
+ double step = turn / n, kappa = 4.0 / 3 * tan(step / 4);
+ for (int i = 0; i < n; ++i) {
+ double a = start + i * step, b = a + step;
+ double u[4][2] = {{cos(a), sin(a)}, {cos(a) - kappa * sin(a), sin(a) + kappa * cos(a)},
+ {cos(b) + kappa * sin(b), sin(b) - kappa * cos(b)}, {cos(b), sin(b)}};
+ Point q[4];
+ for (int j = 0; j < 4; ++j)
+ q[j] = svg_point(box, cx + rx * cp * u[j][0] - ry * sp * u[j][1], cy + rx * sp * u[j][0] + ry * cp * u[j][1]);
+ if (i == n - 1) q[3] = svg_point(box, x2, y2);
+ cubic_to(path, q[1], q[2], q[3]);
+ }
+}
+
+static void svg_path(Path *path, const Box *box, const char *d) {
+ double cx = 0, cy = 0, sx = 0, sy = 0, lx = 0, ly = 0; /* current, subpath start, last control */
+ char command = 0, last = 0;
+ for (const char *p = d; *p;) {
+ while (*p == ' ' || *p == ',' || *p == '\n' || *p == '\t' || *p == '\r') ++p;
+ if (!*p) break;
+ if (strchr("MmLlHhVvQqTtCcSsZzAa", *p)) command = *p++;
+ else if (!command) fail("SVG path data without a command");
+ bool relative = command >= 'a';
+ double ox = relative ? cx : 0, oy = relative ? cy : 0, v[7];
+ char upper = (char)(command & ~32);
+ int want = upper == 'M' || upper == 'L' || upper == 'T' ? 2 : upper == 'H' || upper == 'V' ? 1 :
+ upper == 'Q' || upper == 'S' ? 4 : upper == 'C' ? 6 : upper == 'A' ? 7 : 0;
+ for (int i = 0; i < want; ++i)
+ if (!(p = (upper == 'A' && (i == 3 || i == 4) ? flag : number)(p, &v[i]))) fail("SVG path data");
+ switch (upper) {
+ case 'Z':
+ close_path(path);
+ cx = sx; cy = sy;
+ path->at = svg_point(box, cx, cy);
+ command = 0; /* a number after closepath is an error */
+ break;
+ case 'A': {
+ double x = ox + v[5], y = oy + v[6];
+ arc_to(path, box, cx, cy, v[0], v[1], v[2], v[3] != 0, v[4] != 0, x, y);
+ cx = x; cy = y;
+ break;
+ }
+ case 'M':
+ cx = ox + v[0]; cy = oy + v[1]; sx = cx; sy = cy;
+ move_to(path, svg_point(box, cx, cy));
+ command = relative ? 'l' : 'L'; /* pairs after a moveto are linetos */
+ break;
+ case 'L': cx = ox + v[0]; cy = oy + v[1]; line_to(path, svg_point(box, cx, cy)); break;
+ case 'H': cx = ox + v[0]; line_to(path, svg_point(box, cx, cy)); break;
+ case 'V': cy = (relative ? cy : 0) + v[0]; line_to(path, svg_point(box, cx, cy)); break;
+ case 'Q':
+ lx = ox + v[0]; ly = oy + v[1]; cx = ox + v[2]; cy = oy + v[3];
+ quad_to(path, svg_point(box, lx, ly), svg_point(box, cx, cy));
+ break;
+ case 'T':
+ lx = last == 'Q' || last == 'T' ? 2 * cx - lx : cx; ly = last == 'Q' || last == 'T' ? 2 * cy - ly : cy;
+ cx = ox + v[0]; cy = oy + v[1];
+ quad_to(path, svg_point(box, lx, ly), svg_point(box, cx, cy));
+ break;
+ case 'C': case 'S': {
+ double x1, y1;
+ if (upper == 'C') { x1 = ox + v[0]; y1 = oy + v[1]; }
+ else { x1 = last == 'C' || last == 'S' ? 2 * cx - lx : cx; y1 = last == 'C' || last == 'S' ? 2 * cy - ly : cy; }
+ int at = upper == 'C' ? 2 : 0;
+ lx = ox + v[at]; ly = oy + v[at + 1];
+ double x = ox + v[at + 2], y = oy + v[at + 3];
+ cubic_to(path, svg_point(box, x1, y1), svg_point(box, lx, ly), svg_point(box, x, y));
+ cx = x; cy = y;
+ break;
+ }
+ }
+ last = upper;
+ }
+ close_path(path);
+}
+
+static char *read_file(const char *name, size_t *size) {
+ FILE *f = fopen(name, "rb");
+ if (!f) return NULL;
+ fseek(f, 0, SEEK_END);
+ long length = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ char *bytes = length >= 0 ? malloc((size_t)length + 1) : NULL;
+ if (bytes && fread(bytes, 1, (size_t)length, f) != (size_t)length) { free(bytes); bytes = NULL; }
+ fclose(f);
+ if (bytes) { bytes[length] = 0; *size = (size_t)length; }
+ return bytes;
+}
+
+static const char *attribute(const char *from, const char *end, const char *name) {
+ size_t length = strlen(name);
+ for (const char *p = from; p && p < end; ++p) {
+ p = strstr(p, name);
+ if (!p || p >= end) return NULL;
+ if ((p == from || p[-1] == ' ' || p[-1] == '\n' || p[-1] == '\t') && p[length] == '=' &&
+ (p[length + 1] == '"' || p[length + 1] == '\'')) return p + length + 2;
+ }
+ return NULL;
+}
+
+/* Only solid, nonzero-filled paths: anything else an icon asks for would be
+ * drawn wrong, so it is refused rather than dropped. */
+static const char *const unsupported[] = {"transform", "opacity", "evenodd", "stroke", "fill=\"none\"", "<use",
+ "<rect", "<circle", "<ellipse", "<poly", "<line", "<radial", "<clip",
+ "<mask", "<image", "<text", "<filter", "<pattern"};
+
+static void svg_icon(Path *path, const char *file, uint16_t units, int16_t *advance) {
+ size_t size;
+ char *svg = read_file(file, &size), why[512];
+ if (!svg) fail("cannot read an SVG file");
+ for (size_t i = 0; i < sizeof(unsupported) / sizeof(*unsupported); ++i)
+ if (strstr(svg, unsupported[i])) {
+ snprintf(why, sizeof(why), "%s: icons hold only solid paths, not %s", file, unsupported[i]);
+ fail(why);
+ }
+ const char *tag = strstr(svg, "<svg"), *tag_end = tag ? strchr(tag, '>') : NULL, *view = tag ? attribute(tag, tag_end, "viewBox") : NULL;
+ /* Without a viewBox, the width and height (in px) are the user space. */
+ Box box = {0};
+ const char *width = tag ? attribute(tag, tag_end, "width") : NULL, *height = tag ? attribute(tag, tag_end, "height") : NULL;
+ if (view ? !(view = number(view, &box.x)) || !(view = number(view, &box.y)) || !(view = number(view, &box.width)) ||
+ !number(view, &box.height)
+ : !width || !height || !number(width, &box.width) || !number(height, &box.height) || box.height <= 0) {
+ snprintf(why, sizeof(why), "%s: an icon needs a viewBox, or a width and height", file);
+ fail(why);
+ }
+ box.scale = units / box.height;
+ *advance = (int16_t)lround(box.width * box.scale);
+ for (const char *p = svg; (p = strstr(p, "<path"));) {
+ const char *end = strchr(p, '>'), *d = attribute(p, end, "d");
+ if (d) {
+ char quote = d[-1];
+ const char *close = strchr(d, quote);
+ char *data = strndup(d, (size_t)(close - d));
+ svg_path(path, &box, data);
+ free(data);
+ }
+ p = end ? end : p + 5;
+ }
+ free(svg);
+}
+
+/* ---- Writing. */
+
+static void put16(unsigned char *p, int v) { p[0] = (unsigned char)v; p[1] = (unsigned char)((unsigned)v >> 8); }
+static void put32(unsigned char *p, uint32_t v) { for (int i = 0; i < 4; ++i) p[i] = (unsigned char)(v >> (8 * i)); }
+
+static int16_t unit(double v) {
+ long r = lround(v);
+ if (r < INT16_MIN || r > INT16_MAX) fail("a coordinate does not fit 16 bits");
+ return (int16_t)r;
+}
+
+static int by_code(const void *a, const void *b) {
+ uint32_t x = ((const Built *)a)->code, y = ((const Built *)b)->code;
+ return (x > y) - (x < y);
+}
+
+static void write_symbols(const char *out, uint16_t units, Built *built, size_t count, const Path *path) {
+ qsort(built, count, sizeof(*built), by_code);
+ for (size_t i = 1; i < count; ++i) if (built[i].code == built[i - 1].code) fail("a code is given twice");
+ size_t size = VEGA_SYMBOLS_HEADER + count * VEGA_SYMBOL_BYTES + path->count * VEGA_CURVE_BYTES;
+ unsigned char *bytes = calloc(1, size);
+ if (!bytes) fail("out of memory");
+ memcpy(bytes, "VEGASYM1", 8);
+ put16(bytes + 8, units);
+ put32(bytes + 12, (uint32_t)count);
+ put32(bytes + 16, (uint32_t)path->count);
+ put32(bytes + 20, (uint32_t)size);
+ unsigned char *curves = bytes + VEGA_SYMBOLS_HEADER + count * VEGA_SYMBOL_BYTES;
+ uint32_t next = 0;
+ for (size_t i = 0; i < count; ++i) {
+ int16_t left = 0, bottom = 0, right = 0, top = 0;
+ for (size_t k = 0; k < built[i].count; ++k) {
+ const Quad *q = &path->items[built[i].first + k];
+ unsigned char *c = curves + (size_t)(next + k) * VEGA_CURVE_BYTES;
+ for (int j = 0; j < 3; ++j) {
+ int16_t x = unit(q->p[j].x), y = unit(q->p[j].y);
+ put16(c + 4 * j, x);
+ put16(c + 4 * j + 2, y);
+ if (!k && !j) { left = right = x; bottom = top = y; }
+ if (x < left) left = x;
+ if (x > right) right = x;
+ if (y < bottom) bottom = y;
+ if (y > top) top = y;
+ }
+ }
+ unsigned char *s = bytes + VEGA_SYMBOLS_HEADER + i * VEGA_SYMBOL_BYTES;
+ put32(s, built[i].code);
+ put32(s + 4, next);
+ put32(s + 8, (uint32_t)built[i].count);
+ put16(s + 12, left);
+ put16(s + 14, bottom);
+ put16(s + 16, right);
+ put16(s + 18, top);
+ put16(s + 20, built[i].advance);
+ next += (uint32_t)built[i].count;
+ }
+ FILE *f = fopen(out, "wb");
+ if (!f || fwrite(bytes, 1, size, f) != size || fclose(f)) fail("cannot write the symbol file");
+ free(bytes);
+ char error[128];
+ VegaSymbols check;
+ size_t read_size;
+ char *again = read_file(out, &read_size);
+ if (!again || !vega_symbols_open(&check, (unsigned char *)again, read_size, error, sizeof(error))) fail(error);
+ free(again);
+ printf("{\"symbols\":%zu,\"curves\":%zu,\"bytes\":%zu}\n", count, path->count, size);
+}
+
+/* 0x20-0x7e,0xac00 as a list of codes. */
+static size_t codes_of(const char *text, uint32_t **out) {
+ size_t count = 0, capacity = 0;
+ *out = NULL;
+ for (const char *p = text; *p;) {
+ char *end;
+ unsigned long first = strtoul(p, &end, 0), last = first;
+ if (end == p) fail("--codes takes FIRST[-LAST],...");
+ p = end;
+ if (*p == '-') { last = strtoul(p + 1, &end, 0); p = end; }
+ for (unsigned long c = first; c <= last; ++c) {
+ if (count == capacity && !(*out = realloc(*out, (capacity = capacity ? 2 * capacity : 256) * sizeof(**out))))
+ fail("out of memory");
+ (*out)[count++] = (uint32_t)c;
+ }
+ if (*p == ',') ++p;
+ }
+ return count;
+}
+
+static double seconds(void) {
+ struct timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t);
+ return (double)t.tv_sec + (double)t.tv_nsec / 1e9;
+}
+
+#define BUILD_USAGE "usage: vega-symbols build [--font FONT.ttf --codes 0x20-0x7e,...] [--svg CODE=ICON.svg ...] [--units N] -o OUT.vsym"
+
+static int build(int argc, char **argv) {
+ const char *font = NULL, *out = NULL, *codes = NULL, **icons = calloc((size_t)argc, sizeof(*icons));
+ size_t icon_count = 0;
+ long units = 0;
+ for (int i = 2; i < argc; i += 2) {
+ if (i + 1 == argc) fail(BUILD_USAGE);
+ if (!strcmp(argv[i], "-o")) out = argv[i + 1];
+ else if (!strcmp(argv[i], "--font")) font = argv[i + 1];
+ else if (!strcmp(argv[i], "--codes")) codes = argv[i + 1];
+ else if (!strcmp(argv[i], "--units")) units = strtol(argv[i + 1], NULL, 10);
+ else if (!strcmp(argv[i], "--svg")) icons[icon_count++] = argv[i + 1];
+ else fail(BUILD_USAGE);
+ }
+ if (!out || !font != !codes) fail(BUILD_USAGE);
+ FT_Library library = NULL;
+ FT_Face face = NULL;
+ if (font && (FT_Init_FreeType(&library) || FT_New_Face(library, font, 0, &face))) fail("cannot open the font");
+ /* Twice a font's units by default: TrueType's implied points, halfway
+ * between two control points, then land on whole units. (FreeType finds
+ * them by integer halving, so outlines are doubled before it walks them.) */
+ if (!units) units = face ? 2 * face->units_per_EM : 4096;
+ if (units < 16 || units > 16384) fail("--units: 16 to 16384");
+ uint32_t *list = NULL;
+ size_t count = codes ? codes_of(codes, &list) : 0;
+ Built *built = calloc(count + icon_count + 1, sizeof(*built));
+ Path path = {.font_scale = face ? (double)units / (2.0 * face->units_per_EM) : 1};
+ size_t made = 0;
+ FT_Outline_Funcs walk = {ft_move, ft_line, ft_conic, ft_cubic, 0, 0};
+ for (size_t i = 0; i < count; ++i) {
+ FT_UInt glyph = FT_Get_Char_Index(face, list[i]);
+ if (!glyph || FT_Load_Glyph(face, glyph, FT_LOAD_NO_SCALE)) continue; /* the font lacks it */
+ size_t first = path.count;
+ path.open = false;
+ if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE) {
+ FT_Outline_Transform(&face->glyph->outline, &(FT_Matrix){0x20000, 0, 0, 0x20000});
+ if (FT_Outline_Decompose(&face->glyph->outline, &walk, &path)) fail("cannot walk an outline");
+ }
+ close_path(&path);
+ long advance = lround(face->glyph->metrics.horiAdvance * path.font_scale);
+ built[made++] = (Built){list[i], first, path.count - first, (int16_t)(advance > INT16_MAX ? INT16_MAX : advance)};
+ }
+ for (size_t i = 0; i < icon_count; ++i) {
+ char *equals = strchr(icons[i], '=');
+ if (!equals) fail("--svg takes CODE=FILE");
+ size_t first = path.count;
+ path.open = false;
+ int16_t advance;
+ svg_icon(&path, equals + 1, (uint16_t)units, &advance);
+ built[made++] = (Built){(uint32_t)strtoul(icons[i], NULL, 0), first, path.count - first, advance};
+ }
+ write_symbols(out, (uint16_t)units, built, made, &path);
+ if (face) FT_Done_Face(face);
+ if (library) FT_Done_FreeType(library);
+ free(icons);
+ free(list);
+ free(built);
+ free(path.items);
+ return 0;
+}
+
+static VegaSymbols open_symbols(const char *name, char **bytes) {
+ size_t size;
+ char error[128];
+ VegaSymbols s;
+ if (!(*bytes = read_file(name, &size))) fail("cannot read the symbol file");
+ if (!vega_symbols_open(&s, (unsigned char *)*bytes, size, error, sizeof(error))) fail(error);
+ return s;
+}
+
+static volatile long sink; /* keeps the timed loops' reads */
+static int ignore_point(const FT_Vector *to, void *user) { ++*(long *)user; (void)to; return 0; }
+static int ignore_conic(const FT_Vector *c, const FT_Vector *to, void *user) { (void)c; return ignore_point(to, user); }
+static int ignore_cubic(const FT_Vector *c1, const FT_Vector *c2, const FT_Vector *to, void *user) {
+ (void)c1;
+ (void)c2;
+ return ignore_point(to, user);
+}
+
+/* What reading costs, both ways: opening (verifying the symbol file; parsing
+ * the font's tables) and reaching every symbol's outline. */
+static void measure_open(FT_Library library, const char *font, const VegaSymbols *s) {
+ size_t font_size;
+ char *font_bytes = read_file(font, &font_size), error[128];
+ if (!font_bytes) fail("cannot read the font");
+ int rounds = 200;
+ double started = seconds();
+ for (int i = 0; i < rounds; ++i) {
+ VegaSymbols again;
+ if (!vega_symbols_open(&again, s->bytes, s->size, error, sizeof(error))) fail(error);
+ }
+ double open_us = (seconds() - started) / rounds * 1e6;
+ started = seconds();
+ for (int i = 0; i < rounds; ++i) {
+ FT_Face face;
+ if (FT_New_Memory_Face(library, (const FT_Byte *)font_bytes, (FT_Long)font_size, 0, &face)) fail("cannot open the font");
+ FT_Done_Face(face);
+ }
+ double freetype_open_us = (seconds() - started) / rounds * 1e6;
+ FT_Face face;
+ if (FT_New_Memory_Face(library, (const FT_Byte *)font_bytes, (FT_Long)font_size, 0, &face)) fail("cannot open the font");
+ FT_Outline_Funcs walk = {ignore_point, ignore_point, ignore_conic, ignore_cubic, 0, 0};
+ long touched = 0, walked = 0;
+ started = seconds();
+ for (int r = 0; r < rounds; ++r)
+ for (uint32_t i = 0; i < s->symbols; ++i) {
+ VegaSymbol symbol;
+ if (!vega_symbol(s, vega_symbol_at(s, i).code, &symbol)) fail("lookup");
+ for (uint32_t k = symbol.first; k < symbol.first + symbol.count; ++k) touched += vega_curve(s, k).x2;
+ }
+ double outline_ns = (seconds() - started) / rounds / s->symbols * 1e9;
+ started = seconds();
+ for (int r = 0; r < rounds; ++r)
+ for (uint32_t i = 0; i < s->symbols; ++i) {
+ FT_UInt glyph = FT_Get_Char_Index(face, vega_symbol_at(s, i).code);
+ if (glyph && !FT_Load_Glyph(face, glyph, FT_LOAD_NO_SCALE)) FT_Outline_Decompose(&face->glyph->outline, &walk, &walked);
+ }
+ double freetype_outline_ns = (seconds() - started) / rounds / s->symbols * 1e9;
+ printf("{\"symbol_bytes\":%zu,\"font_bytes\":%zu,\"open_us\":%.1f,\"freetype_open_us\":%.1f,\"outline_ns\":%.0f,"
+ "\"freetype_outline_ns\":%.0f}\n", s->size, font_size, open_us, freetype_open_us, outline_ns, freetype_outline_ns);
+ sink = touched + walked;
+ FT_Done_Face(face);
+ free(font_bytes);
+}
+
+/* Every symbol with a glyph in the font, at each size: our coverage against
+ * FreeType's (no hinting, 8-bit gray) over FreeType's bitmap. */
+static int compare(int argc, char **argv) {
+ if (argc < 6 || strcmp(argv[4], "--sizes")) fail("usage: vega-symbols compare FONT.ttf SYMBOLS.vsym --sizes 12,16,32,64");
+ char *bytes;
+ VegaSymbols s = open_symbols(argv[3], &bytes);
+ FT_Library library;
+ FT_Face face;
+ if (FT_Init_FreeType(&library) || FT_New_Face(library, argv[2], 0, &face)) fail("cannot open the font");
+ measure_open(library, argv[2], &s);
+ for (const char *p = argv[5]; *p;) {
+ char *end;
+ long size = strtol(p, &end, 10);
+ p = *end == ',' ? end + 1 : end;
+ if (size <= 0 || FT_Set_Pixel_Sizes(face, 0, (FT_UInt)size)) fail("--sizes");
+ double scale = (double)size / s.units, total = 0, ours_seconds = 0;
+ uint64_t pixels = 0, close = 0;
+ int worst = 0, glyphs = 0;
+ unsigned char *raster = NULL;
+ size_t raster_size = 0;
+ for (uint32_t i = 0; i < s.symbols; ++i) {
+ VegaSymbol symbol = vega_symbol_at(&s, i);
+ FT_UInt glyph = FT_Get_Char_Index(face, symbol.code);
+ if (!glyph || FT_Load_Glyph(face, glyph, FT_LOAD_NO_HINTING | FT_LOAD_RENDER)) continue;
+ FT_Bitmap *b = &face->glyph->bitmap;
+ size_t need = (size_t)b->width * b->rows;
+ if (!need) continue;
+ if (need > raster_size && !(raster = realloc(raster, raster_size = need))) fail("out of memory");
+ double started = seconds();
+ vega_symbol_raster(&s, &symbol, scale, face->glyph->bitmap_left / scale, face->glyph->bitmap_top / scale,
+ (int)b->width, (int)b->rows, raster);
+ ours_seconds += seconds() - started;
+ for (unsigned j = 0; j < b->rows; ++j)
+ for (unsigned i2 = 0; i2 < b->width; ++i2) {
+ int theirs = b->buffer[j * (unsigned)b->pitch + i2], ours = raster[j * b->width + i2];
+ int difference = abs(theirs - ours);
+ total += difference;
+ close += difference <= 16;
+ if (difference > worst) worst = difference;
+ ++pixels;
+ }
+ ++glyphs;
+ }
+ printf("{\"pixels_per_em\":%ld,\"glyphs\":%d,\"mean_difference\":%.2f,\"worst\":%d,\"within_16\":%.4f,"
+ "\"reference_us_per_glyph\":%.1f}\n", size, glyphs, pixels ? total / (double)pixels : 0, worst,
+ pixels ? (double)close / (double)pixels : 1, glyphs ? ours_seconds / glyphs * 1e6 : 0);
+ free(raster);
+ }
+ FT_Done_Face(face);
+ FT_Done_FreeType(library);
+ free(bytes);
+ return 0;
+}
+
+/* Each symbol's code, curves, bounds, advance and filled area (in square
+ * units: the integral of x dy around its outline, as a check against other
+ * readers of the same fonts and icons). */
+static int info(int argc, char **argv) {
+ if (argc < 3) fail("usage: vega-symbols info SYMBOLS.vsym");
+ char *bytes;
+ VegaSymbols s = open_symbols(argv[2], &bytes);
+ printf("{\"units\":%u,\"symbols\":[", s.units);
+ for (uint32_t i = 0; i < s.symbols; ++i) {
+ VegaSymbol symbol = vega_symbol_at(&s, i);
+ double area = 0;
+ for (uint32_t k = symbol.first; k < symbol.first + symbol.count; ++k) {
+ VegaCurve c = vega_curve(&s, k);
+ /* x(t) = x0 - 2 bx t + ax t^2, y'(t) = -2 by + 2 ay t, over [0, 1]. */
+ double ax = c.x0 - 2 * c.x1 + c.x2, bx = c.x0 - c.x1, ay = c.y0 - 2 * c.y1 + c.y2, by = c.y0 - c.y1;
+ area += c.x0 * -2 * by + (c.x0 * 2 * ay + 4 * bx * by) / 2 + (-4 * bx * ay - 2 * ax * by) / 3 + ax * ay / 2;
+ }
+ printf("%s{\"code\":%u,\"curves\":%u,\"bounds\":[%d,%d,%d,%d],\"advance\":%d,\"area\":%.1f}", i ? "," : "",
+ symbol.code, symbol.count, symbol.left, symbol.bottom, symbol.right, symbol.top, symbol.advance, area);
+ }
+ printf("]}\n");
+ free(bytes);
+ return 0;
+}
+
+static int render(int argc, char **argv) {
+ if (argc < 6) fail("usage: vega-symbols render SYMBOLS.vsym CODE PIXELS OUT.pgm");
+ char *bytes;
+ VegaSymbols s = open_symbols(argv[2], &bytes);
+ VegaSymbol symbol;
+ if (!vega_symbol(&s, (uint32_t)strtoul(argv[3], NULL, 0), &symbol)) fail("no such symbol");
+ double pixels = atof(argv[4]), scale = pixels / s.units;
+ int width = (int)ceil((symbol.advance > symbol.right ? symbol.advance : symbol.right) * scale) + 2;
+ int height = (int)ceil(pixels * 1.25) + 2;
+ unsigned char *raster = malloc((size_t)width * (size_t)height);
+ vega_symbol_raster(&s, &symbol, scale, -1 / scale, (pixels + 1) / scale, width, height, raster);
+ FILE *f = fopen(argv[5], "wb");
+ if (!f) fail("cannot write");
+ fprintf(f, "P5\n%d %d\n255\n", width, height);
+ fwrite(raster, 1, (size_t)width * (size_t)height, f);
+ fclose(f);
+ free(raster);
+ free(bytes);
+ return 0;
+}
+
+int main(int argc, char **argv) {
+ if (argc > 1 && !strcmp(argv[1], "build")) return build(argc, argv);
+ if (argc > 1 && !strcmp(argv[1], "compare")) return compare(argc, argv);
+ if (argc > 1 && !strcmp(argv[1], "info")) return info(argc, argv);
+ if (argc > 1 && !strcmp(argv[1], "render")) return render(argc, argv);
+ fail("vega-symbols build|compare|info|render ...");
+ return 2;
+}
diff --git a/src/symbols.c b/src/symbols.c
new file mode 100644
index 0000000..daa35e5
--- /dev/null
+++ b/src/symbols.c
@@ -0,0 +1,128 @@
+#include "symbols.h"
+#include <math.h>
+#include <stdio.h>
+#include <string.h>
+
+static const unsigned char magic[8] = {'V', 'E', 'G', 'A', 'S', 'Y', 'M', '1'};
+
+static uint16_t u16(const unsigned char *p) { return (uint16_t)(p[0] | p[1] << 8); }
+static int16_t i16(const unsigned char *p) { return (int16_t)u16(p); }
+static uint32_t u32(const unsigned char *p) { return p[0] | p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24; }
+
+VegaSymbol vega_symbol_at(const VegaSymbols *s, uint32_t index) {
+ const unsigned char *p = s->bytes + VEGA_SYMBOLS_HEADER + (size_t)index * VEGA_SYMBOL_BYTES;
+ return (VegaSymbol){u32(p), u32(p + 4), u32(p + 8), i16(p + 12), i16(p + 14), i16(p + 16), i16(p + 18), i16(p + 20)};
+}
+
+VegaCurve vega_curve(const VegaSymbols *s, uint32_t index) {
+ const unsigned char *p = s->bytes + VEGA_SYMBOLS_HEADER + (size_t)s->symbols * VEGA_SYMBOL_BYTES +
+ (size_t)index * VEGA_CURVE_BYTES;
+ return (VegaCurve){i16(p), i16(p + 2), i16(p + 4), i16(p + 6), i16(p + 8), i16(p + 10)};
+}
+
+#define REFUSE(...) do { snprintf(error, error_size, __VA_ARGS__); return false; } while (0)
+
+bool vega_symbols_open(VegaSymbols *s, const unsigned char *bytes, size_t size, char *error, size_t error_size) {
+ if (size < VEGA_SYMBOLS_HEADER || memcmp(bytes, magic, sizeof(magic))) REFUSE("not a symbol file");
+ *s = (VegaSymbols){bytes, size, u16(bytes + 8), u32(bytes + 12), u32(bytes + 16)};
+ uint64_t expected = VEGA_SYMBOLS_HEADER + (uint64_t)s->symbols * VEGA_SYMBOL_BYTES + (uint64_t)s->curves * VEGA_CURVE_BYTES;
+ if (u16(bytes + 10) || u32(bytes + 24) || !s->units) REFUSE("header");
+ if (u32(bytes + 20) != size || expected != size) REFUSE("size: %zu bytes for %u symbols and %u curves", size, s->symbols, s->curves);
+ for (uint32_t i = 0; i < s->symbols; ++i) {
+ VegaSymbol symbol = vega_symbol_at(s, i);
+ const unsigned char *p = bytes + VEGA_SYMBOLS_HEADER + (size_t)i * VEGA_SYMBOL_BYTES;
+ if (u16(p + 22)) REFUSE("symbol %u: flags", i);
+ if (i && symbol.code <= vega_symbol_at(s, i - 1).code) REFUSE("symbol %u: codes out of order", i);
+ if (symbol.first > s->curves || symbol.count > s->curves - symbol.first) REFUSE("symbol %u: curves", i);
+ if (symbol.left > symbol.right || symbol.bottom > symbol.top) REFUSE("symbol %u: bounds", i);
+ }
+ return true;
+}
+
+bool vega_symbol(const VegaSymbols *s, uint32_t code, VegaSymbol *out) {
+ uint32_t low = 0, high = s->symbols;
+ while (low < high) {
+ uint32_t middle = low + (high - low) / 2;
+ VegaSymbol symbol = vega_symbol_at(s, middle);
+ if (symbol.code == code) { *out = symbol; return true; }
+ if (symbol.code < code) low = middle + 1; else high = middle;
+ }
+ return false;
+}
+
+/* Where a t^2 + b t + c = 0 inside (0, 1), appended to ts. */
+static int roots(double a, double b, double c, double *ts) {
+ int n = 0;
+ if (fabs(a) < 1e-12) {
+ if (fabs(b) > 1e-12) ts[n++] = -c / b;
+ } else {
+ double d = b * b - 4 * a * c;
+ if (d >= 0) {
+ /* The root that does not cancel, and the other by the product. */
+ double q = -0.5 * (b + copysign(sqrt(d), b));
+ ts[n++] = q / a;
+ if (q != 0) ts[n++] = c / q;
+ }
+ }
+ int kept = 0;
+ for (int i = 0; i < n; ++i) if (ts[i] > 0 && ts[i] < 1) ts[kept++] = ts[i];
+ return kept;
+}
+
+static double clamp01(double v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
+
+/* The curve's share of the pixel [0, 1]^2 (points relative to its lower left
+ * corner, in pixels): the integral of clamp(1 - x, 0, 1) dy along the part
+ * with y in [0, 1]. Over a closed outline these add up to the covered area,
+ * negative for a counterclockwise one (Green's theorem), so the covered area
+ * is exact, as FreeType's is. The curve splits where it crosses the pixel's
+ * sides; each piece lies left of it (dy counts whole), right of it (nothing),
+ * or across it (a cubic in t, integrated in closed form). */
+static double curve_area(double x0, double y0, double x1, double y1, double x2, double y2) {
+ if (fmax(fmax(y0, y1), y2) <= 0 || fmin(fmin(y0, y1), y2) >= 1 || fmin(fmin(x0, x1), x2) >= 1) return 0;
+ if (fmax(fmax(x0, x1), x2) <= 0) return clamp01(y2) - clamp01(y0);
+ /* x(t) = x0 - 2 bx t + ax t^2, and y alike. */
+ double ax = x0 - 2 * x1 + x2, bx = x0 - x1, ay = y0 - 2 * y1 + y2, by = y0 - y1;
+ double ts[10] = {0};
+ int n = 1;
+ n += roots(ay, -2 * by, y0, ts + n);
+ n += roots(ay, -2 * by, y0 - 1, ts + n);
+ n += roots(ax, -2 * bx, x0, ts + n);
+ n += roots(ax, -2 * bx, x0 - 1, ts + n);
+ ts[n++] = 1;
+ for (int i = 1; i < n; ++i)
+ for (int j = i; j > 0 && ts[j] < ts[j - 1]; --j) { double t = ts[j]; ts[j] = ts[j - 1]; ts[j - 1] = t; }
+ /* (1 - x(t)) y'(t) = (u0 + u1 t + u2 t^2)(v0 + v1 t) */
+ double u0 = 1 - x0, u1 = 2 * bx, u2 = -ax, v0 = -2 * by, v1 = 2 * ay;
+ double k1 = u0 * v0, k2 = (u0 * v1 + u1 * v0) / 2, k3 = (u1 * v1 + u2 * v0) / 3, k4 = u2 * v1 / 4, area = 0;
+ for (int i = 0; i + 1 < n; ++i) {
+ double ta = ts[i], tb = ts[i + 1], t = (ta + tb) / 2;
+ if (tb <= ta) continue;
+ double y = (ay * t - 2 * by) * t + y0, x = (ax * t - 2 * bx) * t + x0;
+ if (y <= 0 || y >= 1 || x >= 1) continue;
+ if (x <= 0) area += ((ay * tb - 2 * by) * tb) - ((ay * ta - 2 * by) * ta);
+ else area += (((k4 * tb + k3) * tb + k2) * tb + k1) * tb - (((k4 * ta + k3) * ta + k2) * ta + k1) * ta;
+ }
+ return area;
+}
+
+double vega_symbol_coverage(const VegaSymbols *s, const VegaSymbol *symbol, double scale, double x, double y) {
+ /* The pixel's lower left corner, in units. */
+ double left = x - 0.5 / scale, bottom = y - 0.5 / scale, area = 0;
+ for (uint32_t k = symbol->first; k < symbol->first + symbol->count; ++k) {
+ VegaCurve c = vega_curve(s, k);
+ area += curve_area((c.x0 - left) * scale, (c.y0 - bottom) * scale, (c.x1 - left) * scale, (c.y1 - bottom) * scale,
+ (c.x2 - left) * scale, (c.y2 - bottom) * scale);
+ }
+ area = fabs(area);
+ return area > 1 ? 1 : area;
+}
+
+void vega_symbol_raster(const VegaSymbols *s, const VegaSymbol *symbol, double scale, double left, double top, int width,
+ int height, unsigned char *out) {
+ for (int j = 0; j < height; ++j)
+ for (int i = 0; i < width; ++i) {
+ double coverage = vega_symbol_coverage(s, symbol, scale, left + (i + 0.5) / scale, top - (j + 0.5) / scale);
+ out[(size_t)j * (size_t)width + (size_t)i] = (unsigned char)lround(coverage * 255);
+ }
+}
diff --git a/src/symbols.h b/src/symbols.h
new file mode 100644
index 0000000..da33e34
--- /dev/null
+++ b/src/symbols.h
@@ -0,0 +1,38 @@
+#ifndef VEGA_SYMBOLS_H
+#define VEGA_SYMBOLS_H
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/* Symbols: glyphs and icons as quadratic curves in one file, verified once
+ * and then read where it lies (docs/design/002-symbol-format.md). */
+enum { VEGA_SYMBOLS_HEADER = 28, VEGA_SYMBOL_BYTES = 24, VEGA_CURVE_BYTES = 12 };
+typedef struct { int16_t x0, y0, x1, y1, x2, y2; } VegaCurve;
+typedef struct {
+ uint32_t code, first, count; /* its curves: [first, first + count) */
+ int16_t left, bottom, right, top; /* bounds, in units, y up */
+ int16_t advance;
+} VegaSymbol;
+typedef struct {
+ const unsigned char *bytes;
+ size_t size;
+ uint16_t units; /* per em */
+ uint32_t symbols, curves;
+} VegaSymbols;
+
+/* Verify bytes as a symbol file (kept, not copied); false with why. What
+ * reading needs is checked, per symbol: after this no read leaves the file.
+ * The curves themselves are not: one outside its symbol's bounds draws wrong. */
+bool vega_symbols_open(VegaSymbols *, const unsigned char *bytes, size_t size, char *error, size_t error_size);
+/* The symbol of code (a binary search), or false. */
+bool vega_symbol(const VegaSymbols *, uint32_t code, VegaSymbol *out);
+VegaSymbol vega_symbol_at(const VegaSymbols *, uint32_t index);
+VegaCurve vega_curve(const VegaSymbols *, uint32_t index);
+/* The coverage (0 to 1) of the pixel whose center is (x, y) in units, at
+ * scale pixels per unit: as a GPU would compute it per pixel. */
+double vega_symbol_coverage(const VegaSymbols *, const VegaSymbol *, double scale, double x, double y);
+/* width x height 8-bit coverage, rows top first; pixel (i, j) has its center
+ * at (left + (i + 0.5) / scale, top - (j + 0.5) / scale) in units. */
+void vega_symbol_raster(const VegaSymbols *, const VegaSymbol *, double scale, double left, double top, int width,
+ int height, unsigned char *out);
+#endif
diff --git a/tests/check.py b/tests/check.py
index f0c495e..c3bcfe2 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -29,9 +29,15 @@
text and row clicks, holds only the rows asked for around the focus, and
every open page hears of each change. In headless Chromium (when node and
chromium are there) the page itself takes keys, a click and typed text.
+10. Symbols: SVG icons (arcs, compact flags, holes, no viewBox) become
+ outlines of the right area, an icon asking for what symbols cannot hold is
+ refused by name, damaged files are refused whole, and (when DejaVu Sans is
+ there) glyphs keep their exact area and draw within a few levels of
+ FreeType's own coverage.
"""
import argparse
import json
+import math
import os
import random
import shutil
@@ -902,11 +908,95 @@ def robustness(vega, aup, work, rounds):
return len(examples), runs
+def symbols(tool, work):
+ """Build, read back and damage symbol files (docs/design/002-symbol-format.md)."""
+ icons = ROOT / 'tests' / 'symbols'
+ out = work / 'icons.vsym'
+ built = run(tool, 'build', '--svg', f'0xE002={icons / "rounded.svg"}', '--svg', f'0xE000={icons / "circle.svg"}',
+ '--svg', f'0xE001={icons / "frame.svg"}', '-o', out)
+ assert built.returncode == 0, built.stderr
+ info = json.loads(run(tool, 'info', out).stdout)
+ units = info['units']
+ square = (units / 16) ** 2 # an icon pixel, in square units
+ expected = {0xE000: math.pi * 49, 0xE001: 196 - 64, 0xE002: 196 - 16 + 4 * math.pi}
+ assert [s['code'] for s in info['symbols']] == sorted(expected), info
+ for symbol in info['symbols']:
+ area = abs(symbol['area']) / square
+ assert abs(area - expected[symbol['code']]) < 2e-3 * expected[symbol['code']], (symbol, expected)
+ assert symbol['bounds'] == [units // 16, units // 16, units * 15 // 16, units * 15 // 16], symbol
+ assert symbol['advance'] == units, symbol
+ refused = run(tool, 'build', '--svg', f'0xE000={icons / "turned.svg"}', '-o', work / 'turned.vsym')
+ assert refused.returncode != 0 and 'turned.svg' in refused.stderr and 'transform' in refused.stderr, refused.stderr
+ twice = run(tool, 'build', '--svg', f'0xE000={icons / "circle.svg"}', '--svg', f'0xE000={icons / "frame.svg"}',
+ '-o', work / 'twice.vsym')
+ assert twice.returncode != 0 and 'twice' in twice.stderr, twice.stderr
+
+ picture = work / 'circle.pgm'
+ assert run(tool, 'render', out, '0xE000', 32, picture).returncode == 0
+ header, pixels = picture.read_bytes().split(b'255\n', 1)
+ w, h = map(int, header.split()[1:3])
+ assert len(pixels) == w * h and abs(sum(pixels) / 255 - math.pi * 49 * 4) < 2, (w, h, sum(pixels) / 255)
+ assert run(tool, 'render', out, '0x41', 32, picture).returncode != 0 # no such symbol
+
+ good = out.read_bytes()
+ count = int.from_bytes(good[12:16], 'little')
+ records = 28
+
+ def damaged(name, data):
+ path = work / f'{name}.vsym'
+ path.write_bytes(bytes(data))
+ refused = run(tool, 'info', path)
+ assert refused.returncode != 0 and refused.stdout == '', (name, refused.stdout)
+ return refused.stderr.strip()
+
+ why = [damaged('short', good[:-1]), damaged('magic', b'VEGASYM2' + good[8:])]
+ swapped = bytearray(good)
+ swapped[records:records + 24], swapped[records + 24:records + 48] = good[records + 24:records + 48], good[records:records + 24]
+ why.append(damaged('order', swapped))
+ beyond = bytearray(good)
+ beyond[records + 8:records + 12] = (1 << 20).to_bytes(4, 'little')
+ why.append(damaged('beyond', beyond))
+ flagged = bytearray(good)
+ flagged[records + 22] = 1
+ why.append(damaged('flags', flagged))
+ counted = bytearray(good)
+ counted[12:16] = (count + 1).to_bytes(4, 'little')
+ why.append(damaged('count', counted))
+ assert len(set(why)) == len(why), why
+
+ report = {'icons': len(info['symbols'])}
+ font = Path('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf')
+ if font.exists():
+ glyphs = work / 'ascii.vsym'
+ assert run(tool, 'build', '--font', font, '--codes', '0x20-0x7e', '-o', glyphs).returncode == 0
+ lines = run(tool, 'compare', font, glyphs, '--sizes', '12,16,32').stdout.splitlines()
+ for line in lines[1:]:
+ size = json.loads(line)
+ assert size['glyphs'] == 94 and size['mean_difference'] < 3 and size['within_16'] > 0.99, size
+ report['glyph_sizes'] = len(lines) - 1
+ try:
+ from fontTools.pens.areaPen import AreaPen
+ from fontTools.ttLib import TTFont
+ except ImportError:
+ return report
+ face = TTFont(font)
+ outlines, cmap = face.getGlyphSet(), face.getBestCmap()
+ shown = json.loads(run(tool, 'info', glyphs).stdout)
+ k = shown['units'] / face['head'].unitsPerEm
+ for symbol in shown['symbols']:
+ pen = AreaPen()
+ outlines[cmap[symbol['code']]].draw(pen)
+ assert abs(abs(pen.value) * k * k - abs(symbol['area'])) < 1, (chr(symbol['code']), pen.value, symbol)
+ report['exact_glyphs'] = len(shown['symbols'])
+ return report
+
+
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--vega', type=Path, required=True)
parser.add_argument('--aup', type=Path, required=True)
parser.add_argument('--serve', type=Path, required=True)
+ parser.add_argument('--symbols', type=Path)
parser.add_argument('--seeds', type=int, default=8)
parser.add_argument('--rounds', type=int, default=3)
args = parser.parse_args()
@@ -937,6 +1027,8 @@ def main():
report['stored_surface_lines'] = stored_surfaces(vega, modules['todo'], work, range(args.seeds))
report['convention_findings'] = conventions(vega, aup, work, modules)
report['examples'], report['robustness_runs'] = robustness(vega, aup, work, args.rounds)
+ if args.symbols:
+ report['symbols'] = symbols(args.symbols.resolve(), work)
print(json.dumps(report))
diff --git a/tests/symbols/circle.svg b/tests/symbols/circle.svg
new file mode 100644
index 0000000..3a6debd
--- /dev/null
+++ b/tests/symbols/circle.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+ <path d="M 8 1 A 7 7 0 1 1 8 15 A 7 7 0 1 1 8 1 Z" fill="#2e3436"/>
+</svg>
diff --git a/tests/symbols/frame.svg b/tests/symbols/frame.svg
new file mode 100644
index 0000000..ce1cfb2
--- /dev/null
+++ b/tests/symbols/frame.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+ <path d="M1 1 H15 V15 H1 Z M4 4 V12 H12 V4 Z"/>
+</svg>
diff --git a/tests/symbols/rounded.svg b/tests/symbols/rounded.svg
new file mode 100644
index 0000000..880765d
--- /dev/null
+++ b/tests/symbols/rounded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M3 1h10a2 2 0 012 2v10a2 2 0 01-2 2H3a2 2 0 01-2-2V3a2 2 0 012-2z"/></svg>
diff --git a/tests/symbols/turned.svg b/tests/symbols/turned.svg
new file mode 100644
index 0000000..9221891
--- /dev/null
+++ b/tests/symbols/turned.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+ <path d="M1 1 H15 V15 H1 Z" transform="rotate(45 8 8)"/>
+</svg>