diff options
| author | eunhhu <[email protected]> | 2026-09-27 14:54:53 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-27 14:54:53 +0100 |
| commit | 993871c6dfa19cf4e0bd4a16c9aad26cb0025d70 (patch) | |
| tree | a83e5bc75d859ca5bee96b26700a9419e687cd1a | |
| parent | 2c1e29780da41bba13ac84ad01472d550e6d02e8 (diff) | |
| download | vega-993871c6dfa19cf4e0bd4a16c9aad26cb0025d70.tar.gz vega-993871c6dfa19cf4e0bd4a16c9aad26cb0025d70.zip | |
Draw the TUI's cells as pixels with the symbols' glyphs
vega pixels runs the same scripts as tui --dump and writes each frame as
an image: the view's cells (only those on screen are evaluated) become
glyphs, each drawn once per size with the exact area of its curves, in its
tone's colour on its row's background. No page, no scene in between. A
1920x1350 grid takes 12 ms a frame here, writing the image included.
Symbol files carry the font's line now (format 3: ascent and descent where
format 2 kept zeros), the cell height and baseline; and the build tool takes
fonts in order, later ones filling the codes earlier ones lack: a CJK font
alone drew ambiguous-width arrows two cells wide in a one-cell grid, which
the new check caught, so a monospace Latin font comes first and Hangul after.
| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | docs/design/002-symbol-format.md | 8 | ||||
| -rw-r--r-- | docs/design/003-pixel-surface.md | 41 | ||||
| -rw-r--r-- | surfaces/main.c | 81 | ||||
| -rw-r--r-- | surfaces/pixels.c | 90 | ||||
| -rw-r--r-- | surfaces/vega.h | 14 | ||||
| -rw-r--r-- | symbols/symbols-gl.c | 12 | ||||
| -rw-r--r-- | symbols/symbols-gl.h | 7 | ||||
| -rw-r--r-- | symbols/symbols-tool.c | 104 | ||||
| -rw-r--r-- | symbols/symbols.c | 80 | ||||
| -rw-r--r-- | symbols/symbols.h | 20 | ||||
| -rw-r--r-- | tests/check.py | 66 |
13 files changed, 460 insertions, 75 deletions
@@ -11,7 +11,7 @@ LIBPURE = $(PURE)/build/libpure.a # Parts: lib/ (the aup library: the interface vocabulary), surfaces/ (the vega # host: TUI, CLI, NUI sentences, web), symbols/ (glyphs and icons as one # format, drawn by CPU or GPU). -SURFACES = surfaces/screen.c surfaces/draw.c surfaces/width.c surfaces/web.c surfaces/main.c +SURFACES = surfaces/screen.c surfaces/draw.c surfaces/width.c surfaces/web.c surfaces/pixels.c surfaces/main.c .PHONY: all check FORCE all: $(BUILD)/vega $(BUILD)/vega-symbols @@ -22,8 +22,8 @@ $(BUILD): $(LIBPURE): FORCE $(MAKE) -C $(PURE) build/libpure.a -$(BUILD)/vega: $(SURFACES) surfaces/vega.h $(LIBPURE) | $(BUILD) - $(CC) $(CPPFLAGS) $(CFLAGS) -I$(PURE)/core $(SURFACES) $(LIBPURE) $(LDFLAGS) -lm $(LDLIBS) -o $@ +$(BUILD)/vega: $(SURFACES) surfaces/vega.h symbols/symbols.c symbols/symbols.h $(LIBPURE) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -I$(PURE)/core -Isymbols $(SURFACES) symbols/symbols.c $(LIBPURE) $(LDFLAGS) -lm $(LDLIBS) -o $@ # Building symbol files reads fonts through FreeType; reading them does not. FREETYPE_CFLAGS ?= $(shell pkg-config --cflags freetype2) @@ -15,6 +15,7 @@ printf 'text 6d696c6b\nkey Space\nshow\n' | build/vega cli todo.pmod printf 'text 6d696c6b\nkey Space\nshow\n' | build/vega say todo.pmod build/vega web todo.pmod --watch # 브라우저(GUI). --watch: 모듈이 다시 빌드되면 상태를 이어 바꾼다 # (옆에서 AUP_PATH=lib aup watch examples/todo.aup -o todo.pmod) +build/vega pixels todo.pmod --symbols ui.vsym --grid 48x12 --out frame < script # 픽셀: frame-N.ppm (설계 003) make check # 참조 모델 대조, 프레임 검사, 대화형 pty 검사, 견고성 ``` @@ -36,7 +37,8 @@ curl -s 'localhost:8767/views/cards_list?from=0&count=20' ([설계 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 build --font DejaVuSansMono.ttf --codes 0x20-0x7e,0x2190-0x21ff \ + --font NotoSansCJK-Regular.ttc --face 6 --codes 0xac00-0xd7a3 --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 ``` @@ -46,7 +48,7 @@ build/vega-symbols render ui.vsym 0xE000 48 icon.pgm | 디렉터리 | 무엇 | 만드는 것 | |---|---|---| | `lib/` | aup 라이브러리(`use vega`): 목록·표·포커스·톤 같은 인터페이스 어휘 | aup가 읽는다 | -| `surfaces/` | 모듈을 사람에게 보이는 표면: TUI, CLI, 문장(NUI), 웹. Pure의 `libpure.a`를 링크한다 | `build/vega` | +| `surfaces/` | 모듈을 사람에게 보이는 표면: TUI, CLI, 문장(NUI), 웹, 픽셀. Pure의 `libpure.a`를 링크한다 | `build/vega` | | `symbols/` | 글리프와 아이콘을 한 형식으로, CPU와 GPU로 그리기([설계 002](docs/design/002-symbol-format.md)) | `build/vega-symbols` | Pure와 같은 규칙이다([Pure 009](/srv/git/pure.git): `docs/design/009-layout.md`). 최상위 디렉터리가 역할별 부품이고, diff --git a/docs/design/002-symbol-format.md b/docs/design/002-symbol-format.md index 253b37c..95a2096 100644 --- a/docs/design/002-symbol-format.md +++ b/docs/design/002-symbol-format.md @@ -12,17 +12,19 @@ 재고, 크기와 여는 시간을 잰다. SVG 아이콘 몇 개를 같은 형식으로 바꿔 그려 본다. - 중단 기준: FreeType과의 평균 차이가 256단계 중 8을 넘으면 형식이나 알고리즘을 다시 본다. -## 형식 (`VEGASYM2`, 리틀 엔디언) +## 형식 (`VEGASYM3`, 리틀 엔디언) | 오프셋 | 내용 | |---|---| -| 0 | `VEGASYM2` | +| 0 | `VEGASYM3` | | 8 | u16 단위(em당), u16 0 | -| 12 | u32 심볼 수, u32 점 수, u32 파일 크기, u32 0 | +| 12 | u32 심볼 수, u32 점 수, u32 파일 크기, i16 올림(ascent), i16 내림(descent) | | 28 | 심볼 × 24 B, 코드 순: u32 코드(글리프는 코드 포인트, 아이콘은 빌드 도구가 준 번호), u32 첫 점, u32 점 수, i16 경계 왼쪽·아래·오른쪽·위, i16 advance, u16 플래그(0) | | 28 + 24n | 점 × 4 B: i16 2x + 곡선 밖, i16 2y + 윤곽 시작 | - 좌표는 단위 공간, y가 위로 는다(폰트와 같다). 채우기는 0이 아닌 감김수다. +- 올림과 내림은 글꼴의 줄(기준선에서의 높이, 내림은 음수)이다. 글자를 줄에 놓는 표면이 줄 높이와 기준선을 여기서 얻는다. + 아이콘만 든 파일은 둘 다 0이다. (3판에서 더했다. 2판의 이 자리는 0이어야 했다.) - 윤곽은 점의 사슬이다(TrueType의 점 모델을 압축 없이). 윤곽은 곡선 위의 시작 점으로 열고, 그 점으로 돌아온 점으로 닫는다. 곡선 밖 점은 2차 곡선의 제어점이고, 곡선 밖 점 둘이 이어지면 그 가운데에 곡선 위 점이 있는 것으로 친다(적지 않는다). 곡선 위 점 둘이 이어지면 직선이다(제어점이 없다). diff --git a/docs/design/003-pixel-surface.md b/docs/design/003-pixel-surface.md new file mode 100644 index 0000000..f01406d --- /dev/null +++ b/docs/design/003-pixel-surface.md @@ -0,0 +1,41 @@ +# 003 — 픽셀 표면: 칸에서 픽셀로 곧장 + +2026-09-27. Pure [로드맵](/srv/git/pure.git) 장기 1 "Vega 종합 인터페이스"의 첫 단계. + +착수 기준: + +- 바꿀 결정: 사람에게 보이는 그림(GUI)을 브라우저(웹 표면)에 맡길지, Vega가 픽셀까지 직접 그릴지. 직접 그린다면 뷰에서 + 픽셀까지 무엇을 거칠지(가상 DOM, 씬 그래프를 두지 않고). +- 최소 확인: TUI가 이미 만드는 칸의 격자(`VegaGrid`, 보이는 칸만 평가한다)를 심볼 형식의 글리프로 그려, 같은 스크립트에서 + TUI의 글과 같은 프레임이 나오고 글자가 옆 칸으로 번지지 않는지 본다. 화면이 없는 Pi에서 이미지 파일로 확인한다. + +## 한 것 + +`vega pixels MODULE --symbols FONT.vsym [--size PX] --grid WxH --out PREFIX < script`: TUI의 dump와 같은 스크립트를 받아 `show`마다 +그 글을 쓰고 `PREFIX-N.ppm`에 그림을 쓴다. 경로는 짧다. 앱의 뷰가 칸(`vega_draw`, 화면에 든 칸만 평가)이 되고, 칸이 글리프가 +되어 픽셀에 합성된다(`surfaces/pixels.c`). 중간에 문서나 장면 트리가 없다. + +- 글리프는 크기마다 처음 쓸 때 한 번 정확 넓이로 굽는다(`vega_glyphs`, 심볼 파일의 곡선에서, [002](002-symbol-format.md)). +- 칸은 숫자 하나만큼 넓고 글꼴의 줄만큼 높다. 줄은 심볼 파일의 올림과 내림(3판에서 더한 헤더의 두 값)이다. 넓은 글자(한글, + CJK)는 두 칸을 쓴다. +- 톤마다 색이 있고, 포커스 행은 배경이 다르다. 심볼에 없는 글자는 칸에 상자를 그린다. +- 글꼴은 차례로 채운다(`vega-symbols build --font A --codes … --font B --codes …`). 앞 글꼴에 없는 코드만 뒤 글꼴에서 가져온다. + 처음에는 한 CJK 글꼴(Noto Sans Mono CJK KR)만 썼는데, 동아시아 폭이 모호한 글자(화살표 등)를 그 글꼴은 전각으로 그려 한 칸인 + 격자에서 옆 칸으로 번졌다. 검사가 그것을 잡았다. 라틴 고정폭(DejaVu Sans Mono) 다음에 한글을 채우면 번지지 않는다. + +## 장치가 말한 것 + +Raspberry Pi 5. todo 예제에 한글 항목 넷, 48×12칸, 20px: 실행 전체(3.6 MB 심볼 파일 열기, 글리프 약 40개 굽기, 그리기, +PPM 쓰기)가 7 ms다. 192×45칸(1920×1350)은 프레임마다 12 ms다(그리기, 합성, 7.8 MB PPM을 `/dev/shm`에 쓰기). + +## 확인 + +`tests/check.py` 11번. 같은 스크립트를 `tui --dump`와 `pixels`로 돌려 글이 같고, 그림에서 빈 칸은 그 행의 배경(포커스 행은 제 +색)뿐이며 글자가 있는 칸에는 잉크가 있다(프레임 둘, 칸 1,024개). + +## 다음 + +- 화면에 내기: KMS 덤 버퍼(XRGB8888)에 바로 합성하고 페이지를 넘긴다. 이 Pi는 HDMI가 비어 있어도 writeback 커넥터가 있어 화면 + 없이 확인할 수 있다. 바뀐 칸만 다시 그린다(TUI가 이미 칸마다 보인 것을 기억한다). +- 입력: evdev(키보드, 마우스, 터치). 칸의 격자가 있으니 클릭이 곧 행이다(`vega_hit`). +- 비례 글꼴의 줄: 칸이 아닌 글자 폭으로 놓는 표와 문단. 합성은 GPU로(아틀라스, [002](002-symbol-format.md)). diff --git a/surfaces/main.c b/surfaces/main.c index 82131ab..d00bca6 100644 --- a/surfaces/main.c +++ b/surfaces/main.c @@ -20,6 +20,9 @@ * say sentences (the first natural-language surface), driven by a script * web [--listen ADDRESS] [--port N] * a web page (a GUI), on 127.0.0.1 unless another address is given + * pixels --symbols FONT.vsym [--size PX] --grid WxH --out PREFIX + * the TUI's frames drawn with a font's glyphs, driven by a + * script: each show writes PREFIX-N.ppm after the text * With --store the app's states live in that tree store: the app continues * where it was, whichever surface runs it. With --watch (tui, web) a module * written again replaces the running one and states keep their values by @@ -456,15 +459,45 @@ static int interactive(void) { /* ---- scripts -------------------------------------------------------- */ -typedef enum { SURFACE_CLI, SURFACE_SAY, SURFACE_TUI, SURFACE_WEB } Surface; +typedef enum { SURFACE_CLI, SURFACE_SAY, SURFACE_TUI, SURFACE_WEB, SURFACE_PIXELS } Surface; + +/* The pixel surface's font and where its frames go. */ +static VegaFace face; +static const char *frames_prefix; +static unsigned frames; + +/* A frame as a binary PPM (P6): prefix-N.ppm. */ +static bool write_frame(const VegaGrid *grid, char path[512]) { + int width = (int)grid->width * face.cell_width, height = (int)grid->height * face.cell_height; + uint32_t *pixels = malloc((size_t)width * (size_t)height * sizeof(*pixels)); + unsigned char *bytes = malloc((size_t)width * (size_t)height * 3); + bool ok = pixels && bytes; + if (ok) { + vega_pixels(grid, &face, pixels, (size_t)width); + for (size_t i = 0; i < (size_t)width * (size_t)height; ++i) { + bytes[3 * i] = (unsigned char)(pixels[i] >> 16); + bytes[3 * i + 1] = (unsigned char)(pixels[i] >> 8); + bytes[3 * i + 2] = (unsigned char)pixels[i]; + } + snprintf(path, 512, "%s-%u.ppm", frames_prefix, ++frames); + FILE *f = fopen(path, "wb"); + ok = f && fprintf(f, "P6\n%d %d\n255\n", width, height) > 0 && + fwrite(bytes, 3, (size_t)width * (size_t)height, f) == (size_t)width * (size_t)height; + if (f && fclose(f)) ok = false; + } + free(pixels); + free(bytes); + return ok; +} /* painting: show writes the terminal bytes of the frame, not the grid. */ static int script(Surface surface, unsigned width, unsigned height, bool painting) { static char line[1 << 16]; VegaGrid grid = {0}; - if (surface == SURFACE_TUI && !vega_grid(&grid, width, height)) return 2; + bool gridded = surface == SURFACE_TUI || surface == SURFACE_PIXELS; + if (gridded && !vega_grid(&grid, width, height)) return 2; bool shown = false; - drawn = surface == SURFACE_TUI ? &grid : NULL; + drawn = gridded ? &grid : NULL; bool color = surface == SURFACE_CLI && isatty(STDOUT_FILENO) && !getenv("NO_COLOR"); unsigned long counted = vega_evaluations(); while (fgets(line, sizeof(line), stdin)) { @@ -498,7 +531,7 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin } else if (!strcmp(command, "click")) { unsigned at_line = 0, at_column = 0; verb = "point"; - if (surface != SURFACE_TUI) { fputs("vega: click needs the tui\n", stderr); continue; } + if (!gridded) { fputs("vega: click needs the tui or pixels\n", stderr); continue; } acted = sscanf(line, "%*s %u %u", &at_line, &at_column) == 2 && at_line && shown; ok = acted && mouse(0, at_line - 1); } else if (!strcmp(command, "time")) { @@ -538,6 +571,11 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin vega_draw(&grid, &app, &screen, entry, entry_length, typing); dump(&grid); printf("cells %lu\n", vega_evaluations() - counted); + char path[512]; + if (surface == SURFACE_PIXELS) { + if (!write_frame(&grid, path)) { fprintf(stderr, "vega: cannot write %s\n", path); return 1; } + printf("frame %s %dx%d\n", path, (int)grid.width * face.cell_width, (int)grid.height * face.cell_height); + } } counted = vega_evaluations(); } else { @@ -556,8 +594,28 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin return 0; } +/* The symbols a pixel surface draws with, read whole and kept. */ +static bool open_face(const char *path, float size) { + static VegaSymbols symbols; + FILE *f = fopen(path, "rb"); + long length = f && !fseek(f, 0, SEEK_END) ? ftell(f) : -1; + unsigned char *bytes = length > 0 && !fseek(f, 0, SEEK_SET) ? malloc((size_t)length) : NULL; + bool ok = bytes && fread(bytes, 1, (size_t)length, f) == (size_t)length; + if (f) fclose(f); + char error[128] = "cannot read it"; + ok = ok && vega_symbols_open(&symbols, bytes, (size_t)length, error, sizeof(error)); + if (ok && !vega_face_open(&face, &symbols, size)) { + ok = false; + snprintf(error, sizeof(error), "no line and digit to lay a grid out with"); + } + if (!ok) fprintf(stderr, "vega: %s: %s\n", path, error); + return ok; /* the bytes stay: the face reads them */ +} + int main(int argc, char **argv) { unsigned width = 0, height = 0, port = 8080; + const char *symbols_path = NULL; + float size = 16; bool dumped = false, painting = false, watch = false, usage = argc < 3; const char *store = NULL, *listen_on = "127.0.0.1"; for (int i = 3; !usage && i < argc; i += 2) { @@ -571,24 +629,33 @@ int main(int argc, char **argv) { else if (!strcmp(argv[i], "--listen") && !strcmp(argv[1], "web")) listen_on = argv[i + 1]; else if (!strcmp(argv[i], "--port") && !strcmp(argv[1], "web") && sscanf(argv[i + 1], "%u", &port) == 1 && port < 65536) {} - else if ((!strcmp(argv[i], "--dump") || !strcmp(argv[i], "--paint")) && + else if ((!strcmp(argv[i], "--dump") || !strcmp(argv[i], "--paint") || + (!strcmp(argv[i], "--grid") && !strcmp(argv[1], "pixels"))) && sscanf(argv[i + 1], "%ux%u", &width, &height) == 2 && width && height && width <= 1000 && height <= 1000) dumped = true, painting = !strcmp(argv[i], "--paint"); + else if (!strcmp(argv[i], "--symbols") && !strcmp(argv[1], "pixels")) symbols_path = argv[i + 1]; + else if (!strcmp(argv[i], "--out") && !strcmp(argv[1], "pixels")) frames_prefix = argv[i + 1]; + else if (!strcmp(argv[i], "--size") && !strcmp(argv[1], "pixels") && sscanf(argv[i + 1], "%f", &size) == 1) {} else usage = true; } - if (usage || (dumped && strcmp(argv[1], "tui")) || (!strcmp(argv[1], "check") && argc != 3)) { + bool pixels = argc > 1 && !strcmp(argv[1], "pixels"); + if (usage || (dumped && strcmp(argv[1], "tui") && !pixels) || (!strcmp(argv[1], "check") && argc != 3) || + (pixels && (!dumped || !symbols_path || !frames_prefix))) { fputs("vega tui|cli|say MODULE.pmod [--store PATH]\n" "vega tui MODULE.pmod [--store PATH] [--watch] | --dump|--paint WIDTHxHEIGHT < script\n" "vega web MODULE.pmod [--store PATH] [--listen ADDRESS] [--port N] [--watch]\n" + "vega pixels MODULE.pmod [--store PATH] --symbols FONT.vsym [--size PX] --grid WIDTHxHEIGHT --out PREFIX < script\n" "vega check MODULE.pmod\n", stderr); return 2; } + if (pixels && !open_face(symbols_path, size)) return 2; if (!strcmp(argv[1], "check")) { if (!load_module(argv[2], &modules[0])) return 2; return vega_check(&modules[0], print_finding, argv[2]) ? 1 : 0; } Surface surface = !strcmp(argv[1], "cli") ? SURFACE_CLI : !strcmp(argv[1], "say") ? SURFACE_SAY : - !strcmp(argv[1], "tui") ? SURFACE_TUI : !strcmp(argv[1], "web") ? SURFACE_WEB : (Surface)-1; + !strcmp(argv[1], "tui") ? SURFACE_TUI : !strcmp(argv[1], "web") ? SURFACE_WEB : + pixels ? SURFACE_PIXELS : (Surface)-1; if ((int)surface < 0) { fprintf(stderr, "vega: unknown surface %s\n", argv[1]); return 2; } if (!load(argv[2], store)) return 2; if (watch && !dumped) { diff --git a/surfaces/pixels.c b/surfaces/pixels.c new file mode 100644 index 0000000..67e7bc5 --- /dev/null +++ b/surfaces/pixels.c @@ -0,0 +1,90 @@ +#include "vega.h" +#include <math.h> +#include <stdlib.h> +#include <string.h> + +/* The pixel surface: the TUI's grid of cells, each drawn with its glyph + * (drawn once per size, the exact area of its curves) in its tone's colour. + * The view's cells go straight into pixels: no page, no scene. */ + +/* Colours, 0xRRGGBB: the background, a focused row's, and one per tone. */ +static const uint32_t background = 0x1e1e2e, focused = 0x313244, strong = 0xffffff; +static const uint32_t tones[VEGA_TONES] = {0xcdd6f4, 0x7f849c, 0x89b4fa, 0xa6e3a1, 0xf9e2af, 0xf38ba8}; + +bool vega_face_open(VegaFace *face, const VegaSymbols *symbols, float pixels) { + *face = (VegaFace){0}; + if (!symbols->units || symbols->ascent <= symbols->descent || pixels < 4 || pixels > 256) return false; + float scale = pixels / symbols->units; + /* A cell is as wide as a digit (the grid is monospace: wide characters + * take two) and as tall as the font's line. */ + VegaSymbol zero; + if (!vega_symbol(symbols, '0', &zero) || zero.advance <= 0) return false; + face->cell_width = (int)lroundf(zero.advance * scale); + face->baseline = (int)ceilf(symbols->ascent * scale); + face->cell_height = face->baseline + (int)ceilf(-symbols->descent * scale); + face->glyphs = vega_glyphs_open(symbols, scale); + return face->glyphs && face->cell_width > 0; +} + +void vega_face_close(VegaFace *face) { + vega_glyphs_close(face->glyphs); + *face = (VegaFace){0}; +} + +static void fill(uint32_t *out, size_t stride, int x, int y, int width, int height, uint32_t color) { + for (int j = 0; j < height; ++j) + for (int i = 0; i < width; ++i) out[(size_t)(y + j) * stride + (size_t)(x + i)] = color; +} + +/* Coverage a (0-255) of color over the pixel: straight alpha, per channel. */ +static uint32_t blend(uint32_t under, uint32_t color, unsigned a) { + uint32_t out = 0; + for (int shift = 0; shift < 24; shift += 8) { + int u = (int)(under >> shift & 0xff), c = (int)(color >> shift & 0xff); + out |= (uint32_t)(u + ((c - u) * (int)a + 127) / 255) << shift; + } + return out; +} + +static void draw(uint32_t *out, size_t stride, int width, int height, const VegaBitmap *glyph, int pen_x, int pen_y, + uint32_t color) { + const VegaPlace *p = &glyph->place; + for (int j = 0; j < p->height; ++j) { + int y = pen_y - p->top + j; + if (y < 0 || y >= height) continue; + for (int i = 0; i < p->width; ++i) { + int x = pen_x + p->left + i; + unsigned a = glyph->pixels[(size_t)j * (size_t)p->width + (size_t)i]; + if (a && x >= 0 && x < width) out[(size_t)y * stride + (size_t)x] = blend(out[(size_t)y * stride + (size_t)x], color, a); + } + } +} + +/* A character the symbols lack: a box in its cell. */ +static void missing(uint32_t *out, size_t stride, int x, int y, int width, int height, int limit, uint32_t color) { + int left = x + 1, right = (x + width < limit ? x + width : limit) - 2, top = y + height / 5, bottom = y + height - height / 5; + for (int i = left; i <= right; ++i) out[(size_t)top * stride + (size_t)i] = out[(size_t)bottom * stride + (size_t)i] = color; + for (int j = top; j <= bottom; ++j) out[(size_t)j * stride + (size_t)left] = out[(size_t)j * stride + (size_t)right] = color; +} + +void vega_pixels(const VegaGrid *grid, VegaFace *face, uint32_t *out, size_t stride) { + int width = (int)grid->width * face->cell_width, height = (int)grid->height * face->cell_height; + /* Backgrounds first: a wide glyph spans its neighbour's cell. */ + for (unsigned r = 0; r < grid->height; ++r) + for (unsigned c = 0; c < grid->width; ++c) + fill(out, stride, (int)c * face->cell_width, (int)r * face->cell_height, face->cell_width, face->cell_height, + grid->cells[(size_t)r * grid->width + c].focus ? focused : background); + for (unsigned r = 0; r < grid->height; ++r) + for (unsigned c = 0; c < grid->width; ++c) { + const VegaCell *cell = &grid->cells[(size_t)r * grid->width + c]; + if (!cell->ch || cell->ch == ' ') continue; + uint32_t color = cell->strong ? strong : tones[cell->tone < VEGA_TONES ? cell->tone : 0]; + int x = (int)c * face->cell_width, y = (int)r * face->cell_height; + const VegaBitmap *glyph = vega_glyph(face->glyphs, cell->ch); + unsigned cells = vega_width(cell->ch) == 2 ? 2 : 1; + if (!glyph) missing(out, stride, x, y, face->cell_width * (int)cells, face->cell_height, width, color); + else draw(out, stride, width, height, glyph, x, y + face->baseline, color); + if (cell->mark && (glyph = vega_glyph(face->glyphs, cell->mark))) + draw(out, stride, width, height, glyph, x, y + face->baseline, color); + } +} diff --git a/surfaces/vega.h b/surfaces/vega.h index f01667b..b8e0044 100644 --- a/surfaces/vega.h +++ b/surfaces/vega.h @@ -1,6 +1,7 @@ #ifndef VEGA_H #define VEGA_H #include "module.h" +#include "symbols.h" #include <stdio.h> /* Vega reads a verified Pure module by convention (docs/design/001-first-component.md): @@ -121,4 +122,17 @@ void vega_grid_free(VegaGrid *); void vega_draw(VegaGrid *, PureApp *, const VegaScreen *, const char *prompt, size_t prompt_length, bool typing); /* The part and item a line of the last drawn grid shows. */ bool vega_hit(const VegaGrid *, unsigned line, unsigned *part, size_t *item); + +/* Pixels: the grid drawn with a font's glyphs (a symbol file, pixels per em). + * A cell is as wide as a digit and as tall as the font's line; each glyph is + * drawn once, the first time a cell shows it. */ +typedef struct { + VegaGlyphs *glyphs; + int cell_width, cell_height, baseline; +} VegaFace; +bool vega_face_open(VegaFace *, const VegaSymbols *, float pixels); +void vega_face_close(VegaFace *); +/* The grid into out: 0xRRGGBB pixels, grid->width * cell_width across and + * grid->height * cell_height down, stride pixels apart. */ +void vega_pixels(const VegaGrid *, VegaFace *, uint32_t *out, size_t stride); #endif diff --git a/symbols/symbols-gl.c b/symbols/symbols-gl.c index c1ce64e..d91a5dc 100644 --- a/symbols/symbols-gl.c +++ b/symbols/symbols-gl.c @@ -87,14 +87,6 @@ bool vega_gl_open(VegaGl *gl, char *error, size_t error_size) { return true; } -/* The pixels a symbol reaches at scale, one more on each side. */ -static VegaPlace place_of(const VegaSymbol *symbol, float scale) { - if (!symbol->count) return (VegaPlace){0}; - int left = (int)floorf(symbol->left * scale) - 1, right = (int)ceilf(symbol->right * scale) + 1; - int top = (int)ceilf(symbol->top * scale) + 1, bottom = (int)floorf(symbol->bottom * scale) - 1; - return (VegaPlace){0, 0, right - left, top - bottom, left, top}; -} - bool vega_gl_atlas(VegaGl *gl, const VegaSymbols *s, float scale, const uint32_t *wanted, uint32_t count, VegaAtlas *out, char *error, size_t error_size) { (void)gl; @@ -112,7 +104,7 @@ bool vega_gl_atlas(VegaGl *gl, const VegaSymbols *s, float scale, const uint32_t uint32_t index = wanted ? wanted[i] : i; if (index >= s->symbols) continue; VegaSymbol symbol = vega_symbol_at(s, index); - VegaPlace p = place_of(&symbol, scale); + VegaPlace p = vega_symbol_place(&symbol, scale); area += (uint64_t)p.width * (uint64_t)p.height; } int width = 64; @@ -122,7 +114,7 @@ bool vega_gl_atlas(VegaGl *gl, const VegaSymbols *s, float scale, const uint32_t uint32_t index = wanted ? wanted[i] : i; if (index >= s->symbols) continue; VegaSymbol symbol = vega_symbol_at(s, index); - VegaPlace p = place_of(&symbol, scale); + VegaPlace p = vega_symbol_place(&symbol, scale); if (!p.width) continue; if (p.width > width) width = p.width; if (x + p.width > width) { x = 0; y += shelf; shelf = 0; } diff --git a/symbols/symbols-gl.h b/symbols/symbols-gl.h index 919a114..cc316a4 100644 --- a/symbols/symbols-gl.h +++ b/symbols/symbols-gl.h @@ -14,17 +14,12 @@ typedef struct { GLint target; } VegaGl; -/* Where each symbol is in an atlas: its rectangle there (x, y, width, height) - * and the pixel offset of that rectangle's top left from the symbol's origin - * (right, up). A symbol with nothing to draw has an empty rectangle. */ -typedef struct { int32_t x, y, width, height, left, top; } VegaPlace; - typedef struct { GLuint texture; /* 8-bit coverage */ int width, height; float scale; /* pixels per unit */ uint32_t symbols; - VegaPlace *places; /* per symbol index */ + VegaPlace *places; /* per symbol index: its box and where the atlas keeps it */ } VegaAtlas; /* A symbol at a pen position (whole pixels from the target's top left, on the diff --git a/symbols/symbols-tool.c b/symbols/symbols-tool.c index b003978..b705d84 100644 --- a/symbols/symbols-tool.c +++ b/symbols/symbols-tool.c @@ -1,7 +1,7 @@ #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 build [--font FONT.ttf [--face N] --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 @@ -407,7 +407,9 @@ static int by_code(const void *a, const void *b) { return (x > y) - (x < y); } -static void write_symbols(const char *out, uint16_t units, Built *built, size_t count, const Path *path) { +/* line: a font's ascent and descent in the build's units (0 and 0 without a font). */ +static void write_symbols(const char *out, uint16_t units, const long line[2], Built *built, size_t count, const Path *path) { + long ascent = line[0], descent = line[1]; 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"); Points points = {0}; @@ -422,16 +424,21 @@ static void write_symbols(const char *out, uint16_t units, Built *built, size_t bool even = units % 2 == 0 && units > 16; for (size_t i = 0; even && i < points.count; ++i) even = points.items[i].x % 2 == 0 && points.items[i].y % 2 == 0; for (size_t i = 0; even && i < count; ++i) even = built[i].advance % 2 == 0; + even = even && ascent % 2 == 0 && descent % 2 == 0; if (!even) break; for (size_t i = 0; i < points.count; ++i) { points.items[i].x /= 2; points.items[i].y /= 2; } for (size_t i = 0; i < count; ++i) built[i].advance = (int16_t)(built[i].advance / 2); + ascent /= 2, descent /= 2; units /= 2; } + if (ascent > INT16_MAX || descent < INT16_MIN) fail("the font's line does not fit 16 bits"); size_t size = VEGA_SYMBOLS_HEADER + count * VEGA_SYMBOL_BYTES + points.count * VEGA_POINT_BYTES; unsigned char *bytes = calloc(1, size); if (!bytes) fail("out of memory"); - memcpy(bytes, "VEGASYM2", 8); + memcpy(bytes, "VEGASYM3", 8); put16(bytes + 8, units); + put16(bytes + 24, (int)ascent); + put16(bytes + 26, (int)descent); put32(bytes + 12, (uint32_t)count); put32(bytes + 16, (uint32_t)points.count); put32(bytes + 20, (uint32_t)size); @@ -506,48 +513,68 @@ static double seconds(void) { 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" +#define BUILD_USAGE \ + "usage: vega-symbols build [--font FONT.ttf [--face N] --codes 0x20-0x7e,...]... [--svg CODE=ICON.svg ...] [--units N] -o OUT.vsym" + +/* Fonts in order, each with the codes to take from it: a code an earlier + * font gave is not taken again, so later fonts fill what earlier ones lack + * (a monospace Latin font, then a CJK one for Hangul). */ +typedef struct { const char *path, *codes; long face; } Font; 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; + const char *out = NULL, **icons = calloc((size_t)argc, sizeof(*icons)); + Font *fonts = calloc((size_t)argc, sizeof(*fonts)); + size_t icon_count = 0, font_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], "--font")) fonts[font_count++] = (Font){argv[i + 1], NULL, 0}; + else if (!strcmp(argv[i], "--face") && font_count) fonts[font_count - 1].face = strtol(argv[i + 1], NULL, 10); /* in a .ttc */ + else if (!strcmp(argv[i], "--codes") && font_count) fonts[font_count - 1].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); + if (!out) fail(BUILD_USAGE); + for (size_t f = 0; f < font_count; ++f) if (!fonts[f].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; + FT_Face *faces = calloc(font_count + 1, sizeof(*faces)); + if (font_count && FT_Init_FreeType(&library)) fail("cannot start FreeType"); + for (size_t f = 0; f < font_count; ++f) + if (FT_New_Face(library, fonts[f].path, fonts[f].face, &faces[f])) fail("cannot open a font"); + /* Twice the first 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.) Other fonts are scaled into the same units. */ + if (!units) units = font_count ? 2 * faces[0]->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 capacity = icon_count + 1; + uint32_t **lists = calloc(font_count + 1, sizeof(*lists)); + size_t *counts = calloc(font_count + 1, sizeof(*counts)); + for (size_t f = 0; f < font_count; ++f) capacity += counts[f] = codes_of(fonts[f].codes, &lists[f]); + Built *built = calloc(capacity, sizeof(*built)); + Path path = {.font_scale = 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"); + for (size_t f = 0; f < font_count; ++f) { + FT_Face face = faces[f]; + path.font_scale = (double)units / (2.0 * face->units_per_EM); + for (size_t i = 0; i < counts[f]; ++i) { + bool given = false; + for (size_t k = 0; k < made && !given; ++k) given = built[k].code == lists[f][i]; + FT_UInt glyph = given ? 0 : FT_Get_Char_Index(face, lists[f][i]); + if (!glyph || FT_Load_Glyph(face, glyph, FT_LOAD_NO_SCALE)) continue; /* given, or 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((double)face->glyph->metrics.horiAdvance * units / face->units_per_EM); + built[made++] = (Built){lists[f][i], first, path.count - first, (int16_t)(advance > INT16_MAX ? INT16_MAX : advance)}; } - close_path(&path); - long advance = lround((double)face->glyph->metrics.horiAdvance * units / face->units_per_EM); - 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], '='); @@ -558,11 +585,20 @@ static int build(int argc, char **argv) { 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); + /* The first font's line as FreeType reports it (hhea or typo metrics). */ + long line[2] = {font_count ? lround((double)faces[0]->ascender * units / faces[0]->units_per_EM) : 0, + font_count ? lround((double)faces[0]->descender * units / faces[0]->units_per_EM) : 0}; + write_symbols(out, (uint16_t)units, line, built, made, &path); + for (size_t f = 0; f < font_count; ++f) { + FT_Done_Face(faces[f]); + free(lists[f]); + } if (library) FT_Done_FreeType(library); + free(faces); + free(lists); + free(counts); + free(fonts); free(icons); - free(list); free(built); free(path.items); return 0; @@ -698,7 +734,7 @@ 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); + printf("{\"units\":%u,\"ascent\":%d,\"descent\":%d,\"symbols\":[", s.units, s.ascent, s.descent); for (uint32_t i = 0; i < s.symbols; ++i) { VegaSymbol symbol = vega_symbol_at(&s, i); double area = 0; diff --git a/symbols/symbols.c b/symbols/symbols.c index 6f355f1..f00639a 100644 --- a/symbols/symbols.c +++ b/symbols/symbols.c @@ -4,7 +4,7 @@ #include <stdlib.h> #include <string.h> -static const unsigned char magic[8] = {'V', 'E', 'G', 'A', 'S', 'Y', 'M', '2'}; +static const unsigned char magic[8] = {'V', 'E', 'G', 'A', 'S', 'Y', 'M', '3'}; 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); } @@ -17,7 +17,7 @@ VegaSymbol vega_symbol_at(const VegaSymbols *s, uint32_t index) { /* A point is two i16s, each its coordinate times two plus a flag: off the * curve in x, starting a contour in y. */ -VegaPoint vega_point(const VegaSymbols *s, uint32_t index) { +VegaPoint vega_symbols_point(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_POINT_BYTES; int x = i16(p), y = i16(p + 2); @@ -29,9 +29,9 @@ VegaPoint vega_point(const VegaSymbols *s, uint32_t index) { 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)}; + *s = (VegaSymbols){bytes, size, u16(bytes + 8), i16(bytes + 24), i16(bytes + 26), u32(bytes + 12), u32(bytes + 16)}; uint64_t expected = VEGA_SYMBOLS_HEADER + (uint64_t)s->symbols * VEGA_SYMBOL_BYTES + (uint64_t)s->points * VEGA_POINT_BYTES; - if (u16(bytes + 10) || u32(bytes + 24) || !s->units) REFUSE("header"); + if (u16(bytes + 10) || !s->units || s->ascent < s->descent) REFUSE("header"); if (u32(bytes + 20) != size || expected != size) REFUSE("size: %zu bytes for %u symbols and %u points", size, s->symbols, s->points); for (uint32_t i = 0; i < s->symbols; ++i) { VegaSymbol symbol = vega_symbol_at(s, i); @@ -42,7 +42,7 @@ bool vega_symbols_open(VegaSymbols *s, const unsigned char *bytes, size_t size, if (symbol.left > symbol.right || symbol.bottom > symbol.top) REFUSE("symbol %u: bounds", i); /* An off point then always has both neighbours within the symbol. */ if (symbol.count) { - VegaPoint first = vega_point(s, symbol.first), last = vega_point(s, symbol.first + symbol.count - 1); + VegaPoint first = vega_symbols_point(s, symbol.first), last = vega_symbols_point(s, symbol.first + symbol.count - 1); if (first.off || !first.move) REFUSE("symbol %u: its first point does not start a contour on the curve", i); if (last.off) REFUSE("symbol %u: its last point is off the curve", i); } @@ -62,15 +62,15 @@ bool vega_symbol(const VegaSymbols *s, uint32_t code, VegaSymbol *out) { } bool vega_symbol_curve(const VegaSymbols *s, const VegaSymbol *symbol, uint32_t index, VegaCurve *out) { - VegaPoint p = vega_point(s, index); + VegaPoint p = vega_symbols_point(s, index); if (p.off) { - VegaPoint a = vega_point(s, index - 1), b = vega_point(s, index + 1); + VegaPoint a = vega_symbols_point(s, index - 1), b = vega_symbols_point(s, index + 1); *out = (VegaCurve){a.off ? (a.x + p.x) * 0.5f : a.x, a.off ? (a.y + p.y) * 0.5f : a.y, p.x, p.y, b.off ? (p.x + b.x) * 0.5f : b.x, b.off ? (p.y + b.y) * 0.5f : b.y}; return true; } if (index + 1 >= symbol->first + symbol->count) return false; - VegaPoint b = vega_point(s, index + 1); + VegaPoint b = vega_symbols_point(s, index + 1); if (b.off || b.move) return false; *out = (VegaCurve){p.x, p.y, (p.x + b.x) * 0.5f, (p.y + b.y) * 0.5f, b.x, b.y}; return true; @@ -183,3 +183,67 @@ void vega_symbol_raster(const VegaSymbols *s, const VegaSymbol *symbol, double s } free(curves); } + +VegaPlace vega_symbol_place(const VegaSymbol *symbol, float scale) { + if (!symbol->count) return (VegaPlace){0}; + int left = (int)floorf(symbol->left * scale) - 1, right = (int)ceilf(symbol->right * scale) + 1; + int top = (int)ceilf(symbol->top * scale) + 1, bottom = (int)floorf(symbol->bottom * scale) - 1; + return (VegaPlace){0, 0, right - left, top - bottom, left, top}; +} + +/* An open table from code to bitmap, twice as large as what it holds. */ +typedef struct { uint32_t code; bool used, missing; VegaBitmap bitmap; } Slot; +struct VegaGlyphs { + const VegaSymbols *symbols; + float scale; + Slot *slots; + size_t capacity, count; +}; + +VegaGlyphs *vega_glyphs_open(const VegaSymbols *s, float scale) { + VegaGlyphs *glyphs = calloc(1, sizeof(*glyphs)); + if (!glyphs) return NULL; + *glyphs = (VegaGlyphs){s, scale, calloc(256, sizeof(Slot)), 256, 0}; + if (!glyphs->slots) { free(glyphs); return NULL; } + return glyphs; +} + +static Slot *slot_of(Slot *slots, size_t capacity, uint32_t code) { + size_t at = (code * 2654435761u) & (capacity - 1); + while (slots[at].used && slots[at].code != code) at = (at + 1) & (capacity - 1); + return &slots[at]; +} + +const VegaBitmap *vega_glyph(VegaGlyphs *glyphs, uint32_t code) { + Slot *slot = slot_of(glyphs->slots, glyphs->capacity, code); + if (slot->used) return slot->missing ? NULL : &slot->bitmap; + if (2 * (glyphs->count + 1) > glyphs->capacity) { + Slot *larger = calloc(2 * glyphs->capacity, sizeof(Slot)); + if (!larger) return NULL; + for (size_t i = 0; i < glyphs->capacity; ++i) + if (glyphs->slots[i].used) *slot_of(larger, 2 * glyphs->capacity, glyphs->slots[i].code) = glyphs->slots[i]; + free(glyphs->slots); + glyphs->slots = larger; + glyphs->capacity *= 2; + slot = slot_of(glyphs->slots, glyphs->capacity, code); + } + VegaSymbol symbol; + *slot = (Slot){.code = code, .used = true}; + ++glyphs->count; + if (!vega_symbol(glyphs->symbols, code, &symbol)) { slot->missing = true; return NULL; } + VegaPlace place = vega_symbol_place(&symbol, glyphs->scale); + unsigned char *pixels = place.width ? malloc((size_t)place.width * (size_t)place.height) : NULL; + if (place.width && !pixels) { slot->missing = true; return NULL; } + if (pixels) + vega_symbol_raster(glyphs->symbols, &symbol, glyphs->scale, place.left / glyphs->scale, place.top / glyphs->scale, + place.width, place.height, pixels, (size_t)place.width); + slot->bitmap = (VegaBitmap){place, pixels}; + return &slot->bitmap; +} + +void vega_glyphs_close(VegaGlyphs *glyphs) { + if (!glyphs) return; + for (size_t i = 0; i < glyphs->capacity; ++i) free((void *)glyphs->slots[i].bitmap.pixels); + free(glyphs->slots); + free(glyphs); +} diff --git a/symbols/symbols.h b/symbols/symbols.h index 20fbd22..0af3412 100644 --- a/symbols/symbols.h +++ b/symbols/symbols.h @@ -23,7 +23,8 @@ typedef struct { typedef struct { const unsigned char *bytes; size_t size; - uint16_t units; /* per em */ + uint16_t units; /* per em */ + int16_t ascent, descent; /* a font's line, in units from the baseline, y up (0 and 0 for icons) */ uint32_t symbols, points; } VegaSymbols; @@ -35,7 +36,7 @@ bool vega_symbols_open(VegaSymbols *, const unsigned char *bytes, size_t size, c /* 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); -VegaPoint vega_point(const VegaSymbols *, uint32_t index); +VegaPoint vega_symbols_point(const VegaSymbols *, uint32_t index); /* The curve that point index of symbol begins, if any: an off point is the * control of one, and an on point followed by another on point in the same * contour starts a line. Every curve of a symbol is had once this way, from @@ -47,9 +48,24 @@ uint32_t vega_symbol_curves(const VegaSymbols *, const VegaSymbol *, VegaCurve * /* The coverage (0 to 1) of the pixel whose center is (x, y) in units, at * scale pixels per unit: the exact area the curves' outline covers. */ double vega_coverage(const VegaCurve *, uint32_t count, double scale, double x, double y); +/* The pixels a symbol reaches at scale (pixels per unit), one more on each + * side: width and height, and the offset of the box's top left from the + * symbol's origin (right, up). x and y are where a set of bitmaps (an atlas) + * keeps it. A symbol with nothing to draw has an empty box. */ +typedef struct { int32_t x, y, width, height, left, top; } VegaPlace; +VegaPlace vega_symbol_place(const VegaSymbol *, float scale); /* width x height 8-bit coverage, rows top first, stride bytes apart; 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, size_t stride); + +/* Symbols drawn once at one scale and kept, for surfaces that compose them + * on the CPU: each the first time it is asked for. */ +typedef struct { VegaPlace place; const unsigned char *pixels; } VegaBitmap; /* pixels: place.width per row */ +typedef struct VegaGlyphs VegaGlyphs; +VegaGlyphs *vega_glyphs_open(const VegaSymbols *, float scale); +/* The bitmap of code, or NULL when the file has no such symbol. */ +const VegaBitmap *vega_glyph(VegaGlyphs *, uint32_t code); +void vega_glyphs_close(VegaGlyphs *); #endif diff --git a/tests/check.py b/tests/check.py index 3669fdb..e21cfc8 100644 --- a/tests/check.py +++ b/tests/check.py @@ -29,6 +29,10 @@ 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. +11. Pixels: the pixel surface draws the TUI's frames with a font's glyphs + (a Latin monospace font, then Korean from a CJK one): the same text, blank + cells only background (a focused row's own colour), every other cell + inked, so no glyph spills into its neighbour. 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 @@ -1013,6 +1017,67 @@ def symbols(tool, work): return report +def pixels(vega, tool, module, work): + """The pixel surface draws the TUI's grid: the same frames as text, and in + the image every blank cell is its row's background (the focused row's + own colour) and every other cell has ink.""" + latin = Path('/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf') + korean = Path('/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc') + if not latin.exists() or not korean.exists(): + return None + # A monospace Latin font first (its arrows are one cell wide), then Hangul + # from Noto Sans Mono CJK KR (face 6 of the collection) where it lacks them. + face = work / 'ui.vsym' + built = run(tool, 'build', '--font', latin, '--codes', '0x20-0x7e,0x2190-0x21ff', '--font', korean, '--face', 6, + '--codes', '0x3131-0x318e,0xac00-0xd7a3', '-o', face) + assert built.returncode == 0, built.stderr + items = ['우유 사기', 'Pure 런타임 문서 읽기', 'aup로 앱 만들기', 'draw pixels'] + script = ''.join(f'text {item.encode().hex()}\n' for item in items) + 'key Space\nshow\nkey ArrowUp\nkey ArrowUp\nshow\n' + columns, rows = 44, 12 + text = run(vega, 'tui', module, '--dump', f'{columns}x{rows}', stdin=script) + drawn = run(vega, 'pixels', module, '--symbols', face, '--size', 18, '--grid', f'{columns}x{rows}', + '--out', work / 'frame', stdin=script) + assert text.returncode == 0 and drawn.returncode == 0, drawn.stderr + frames = [line.split() for line in drawn.stdout.splitlines() if line.startswith('frame ')] + assert [line for line in drawn.stdout.splitlines() if not line.startswith('frame ')] == text.stdout.splitlines() + assert len(frames) == 2 + lines = text.stdout.splitlines() + background, focused = (0x1e, 0x1e, 0x2e), (0x31, 0x32, 0x44) + checked = 0 + for number, (_, path, size) in enumerate(frames): + data = Path(path).read_bytes() + header, image = data.split(b'255\n', 1) + width, height = map(int, header.split()[1:3]) + assert f'{width}x{height}' == size and width % columns == 0 and height % rows == 0 and len(image) == width * height * 3 + cell_width, cell_height = width // columns, height // rows + frame_lines = lines[number * (rows + 1):number * (rows + 1) + rows] + for row, line in enumerate(frame_lines): + shown = line[1:].split('\t')[0] + color = focused if line[0] == '>' else background + # each character's first cell, as the grid lays them out + cells, column = {}, 0 + for character in shown: + cells[column] = character + column += width_of(character) + for column in range(columns): + x0, y0 = column * cell_width, row * cell_height + inked = any(tuple(image[3 * ((y0 + j) * width + x0 + i):3 * ((y0 + j) * width + x0 + i) + 3]) != color + for j in range(cell_height) for i in range(cell_width)) + character = cells.get(column) + if character is None and any(start < column < start + width_of(c) for start, c in cells.items()): + continue # the second half of a wide character + if character in (None, ' '): + assert not inked, (number, row, column, line) + else: + assert inked, (number, row, column, character, line) + checked += 1 + return {'frames': len(frames), 'cells': checked} + + +def width_of(character): + return width(character) or 0 + + def main(): parser = argparse.ArgumentParser() parser.add_argument('--vega', type=Path, required=True) @@ -1051,6 +1116,7 @@ def main(): report['examples'], report['robustness_runs'] = robustness(vega, aup, work, args.rounds) if args.symbols: report['symbols'] = symbols(args.symbols.resolve(), work) + report['pixels'] = pixels(vega, args.symbols.resolve(), modules['todo'], work) print(json.dumps(report)) |
