diff options
| author | eunhhu <[email protected]> | 2026-09-27 13:45:31 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-27 13:45:31 +0100 |
| commit | 2920e389f624263efe515d9a5f33f6b822efff5e (patch) | |
| tree | 098011662955822230b3baa3141b997092d96153 /symbols | |
| parent | e650c341704e1b7f4c9d5a05bfa745c54002a599 (diff) | |
| download | vega-2920e389f624263efe515d9a5f33f6b822efff5e.tar.gz vega-2920e389f624263efe515d9a5f33f6b822efff5e.zip | |
Name Vega's parts: lib/, surfaces/ and symbols/
src/ held two parts, the surfaces that show modules to people and the
symbol format with its CPU and GPU drawing. As in Pure (design 009), top
level directories are now the parts, and the README says what each builds.
Diffstat (limited to 'symbols')
| -rw-r--r-- | symbols/symbols-gl.c | 260 | ||||
| -rw-r--r-- | symbols/symbols-gl.h | 24 | ||||
| -rw-r--r-- | symbols/symbols-tool.c | 881 | ||||
| -rw-r--r-- | symbols/symbols.c | 185 | ||||
| -rw-r--r-- | symbols/symbols.h | 54 |
5 files changed, 1404 insertions, 0 deletions
diff --git a/symbols/symbols-gl.c b/symbols/symbols-gl.c new file mode 100644 index 0000000..0e78efe --- /dev/null +++ b/symbols/symbols-gl.c @@ -0,0 +1,260 @@ +#include "symbols-gl.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +enum { BANDS = 16 }; + +/* Coordinates travel doubled, so implied points stay whole: 2x of a point + * is its raw i16 without the flag, and an implied point's is the sum of its + * two neighbours' coordinates. */ +#define SHARED \ + "#version 310 es\n" \ + "precision highp float;\n" \ + "precision highp int;\n" + +static const char vertex_source[] = SHARED + "layout(location = 0) in vec4 place; /* origin x, y in pixels; pixels per unit; unused */\n" + "layout(location = 1) in vec4 bounds; /* left, bottom, right, top in units */\n" + "layout(location = 2) in uint bands; /* its first band */\n" + "uniform vec2 target;\n" + "out vec2 units;\n" + "flat out float scale;\n" + "flat out ivec2 rows; /* bottom and top in units */\n" + "flat out uint base;\n" + "void main() {\n" + " vec2 corner = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1));\n" + " vec2 pad = vec2(1.0 / place.z);\n" + " units = mix(bounds.xy - pad, bounds.zw + pad, corner);\n" + " vec2 pixel = place.xy + vec2(units.x, -units.y) * place.z;\n" + " gl_Position = vec4(pixel.x / target.x * 2.0 - 1.0, 1.0 - pixel.y / target.y * 2.0, 0.0, 1.0);\n" + " scale = place.z;\n" + " rows = ivec2(bounds.yw);\n" + " base = bands;\n" + "}\n"; + +static const char fragment_source[] = SHARED + "layout(std430, binding = 0) readonly buffer Points { uint points[]; };\n" + "layout(std430, binding = 1) readonly buffer Bands { uint bands[]; };\n" + "uniform uint listed; /* where the lists of curves start in bands */\n" + "in vec2 units;\n" + "flat in float scale;\n" + "flat in ivec2 rows;\n" + "flat in uint base;\n" + "out vec4 color;\n" + "\n" + "ivec3 point(uint i) { /* doubled x, doubled y, off */\n" + " uint w = points[i];\n" + " int x = int(w << 16u) >> 16, y = int(w) >> 16;\n" + " return ivec3(x & ~1, y & ~1, x & 1);\n" + "}\n" + "int band(int doubled) { /* which of 16 bands a doubled y lies in */\n" + " return clamp((doubled - 2 * rows.x) * 16 / (2 * (rows.y - rows.x) + 2), 0, 15);\n" + "}\n" + "int roots(float a, float b, float c, inout float ts[10], int n) {\n" + " float r0 = -1.0, r1 = -1.0;\n" + " if (a == 0.0) { if (b != 0.0) r0 = -c / b; }\n" + " else {\n" + " float d = b * b - 4.0 * a * c;\n" + " if (d >= 0.0) {\n" + " float q = -0.5 * (b + (b >= 0.0 ? sqrt(d) : -sqrt(d)));\n" + " r0 = q / a;\n" + " if (q != 0.0) r1 = c / q;\n" + " }\n" + " }\n" + " if (r0 > 0.0 && r0 < 1.0) ts[n++] = r0;\n" + " if (r1 > 0.0 && r1 < 1.0) ts[n++] = r1;\n" + " return n;\n" + "}\n" + "/* As curve_area in symbols.c: the curve's share of the pixel [0, 1]^2. */\n" + "float area(vec2 p0, vec2 p1, vec2 p2) {\n" + " if (max(max(p0.y, p1.y), p2.y) <= 0.0 || min(min(p0.y, p1.y), p2.y) >= 1.0 || min(min(p0.x, p1.x), p2.x) >= 1.0)\n" + " return 0.0;\n" + " if (max(max(p0.x, p1.x), p2.x) <= 0.0) return clamp(p2.y, 0.0, 1.0) - clamp(p0.y, 0.0, 1.0);\n" + " vec2 a = p0 - 2.0 * p1 + p2, b = p0 - p1;\n" + " float ts[10];\n" + " ts[0] = 0.0;\n" + " int n = 1;\n" + " n = roots(a.y, -2.0 * b.y, p0.y, ts, n);\n" + " n = roots(a.y, -2.0 * b.y, p0.y - 1.0, ts, n);\n" + " n = roots(a.x, -2.0 * b.x, p0.x, ts, n);\n" + " n = roots(a.x, -2.0 * b.x, p0.x - 1.0, ts, n);\n" + " ts[n++] = 1.0;\n" + " for (int i = 1; i < n; ++i)\n" + " for (int j = i; j > 0 && ts[j] < ts[j - 1]; --j) { float t = ts[j]; ts[j] = ts[j - 1]; ts[j - 1] = t; }\n" + " float u0 = 1.0 - p0.x, u1 = 2.0 * b.x, u2 = -a.x, v0 = -2.0 * b.y, v1 = 2.0 * a.y;\n" + " float k1 = u0 * v0, k2 = (u0 * v1 + u1 * v0) / 2.0, k3 = (u1 * v1 + u2 * v0) / 3.0, k4 = u2 * v1 / 4.0, sum = 0.0;\n" + " for (int i = 0; i + 1 < n; ++i) {\n" + " float ta = ts[i], tb = ts[i + 1], t = (ta + tb) / 2.0;\n" + " if (tb <= ta) continue;\n" + " float y = (a.y * t - 2.0 * b.y) * t + p0.y, x = (a.x * t - 2.0 * b.x) * t + p0.x;\n" + " if (y <= 0.0 || y >= 1.0 || x >= 1.0) continue;\n" + " if (x <= 0.0) sum += (a.y * tb - 2.0 * b.y) * tb - (a.y * ta - 2.0 * b.y) * ta;\n" + " else sum += (((k4 * tb + k3) * tb + k2) * tb + k1) * tb - (((k4 * ta + k3) * ta + k2) * ta + k1) * ta;\n" + " }\n" + " return sum;\n" + "}\n" + "void main() {\n" + " vec2 corner = units - vec2(0.5 / scale); /* the pixel's lower left, in units */\n" + " int b0 = band(2 * int(floor(corner.y))), b1 = band(2 * int(ceil(corner.y + 1.0 / scale)));\n" + " float sum = 0.0;\n" + " for (int b = b0; b <= b1; ++b) {\n" + " uint at = bands[base + 2u * uint(b)], end = at + bands[base + 2u * uint(b) + 1u];\n" + " for (; at < end; ++at) {\n" + " uint k = bands[listed + at];\n" + " ivec3 p = point(k), n = point(k + 1u);\n" + " ivec2 s, c, e;\n" + " if (p.z == 1) {\n" + " ivec3 q = point(k - 1u);\n" + " s = q.z == 1 ? (q.xy + p.xy) / 2 : q.xy;\n" + " c = p.xy;\n" + " e = n.z == 1 ? (p.xy + n.xy) / 2 : n.xy;\n" + " } else {\n" + " s = p.xy;\n" + " c = (p.xy + n.xy) / 2;\n" + " e = n.xy;\n" + " }\n" + " /* a curve over several of this pixel's bands counts in the first */\n" + " if (max(band(min(min(s.y, c.y), e.y)), b0) != b) continue;\n" + " sum += area((vec2(s) * 0.5 - corner) * scale, (vec2(c) * 0.5 - corner) * scale, (vec2(e) * 0.5 - corner) * scale);\n" + " }\n" + " }\n" + " color = vec4(min(abs(sum), 1.0), 0.0, 0.0, 1.0);\n" + "}\n"; + +static GLuint compile(GLenum type, const char *source, char *error, size_t error_size) { + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &source, NULL); + glCompileShader(shader); + GLint ok; + glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); + if (!ok) { + glGetShaderInfoLog(shader, (GLsizei)error_size, NULL, error); + glDeleteShader(shader); + return 0; + } + return shader; +} + +/* Doubled coordinates make implied points whole; bands as the shader cuts them. */ +static int band(int doubled, int bottom, int top) { + int b = (doubled - 2 * bottom) * BANDS / (2 * (top - bottom) + 2); + return b < 0 ? 0 : b >= BANDS ? BANDS - 1 : b; +} + +bool vega_gl_open(VegaGl *gl, const VegaSymbols *s, char *error, size_t error_size) { + *gl = (VegaGl){0}; + GLuint vertex = compile(GL_VERTEX_SHADER, vertex_source, error, error_size); + GLuint fragment = vertex ? compile(GL_FRAGMENT_SHADER, fragment_source, error, error_size) : 0; + if (!fragment) return false; + gl->program = glCreateProgram(); + glAttachShader(gl->program, vertex); + glAttachShader(gl->program, fragment); + glLinkProgram(gl->program); + glDeleteShader(vertex); + glDeleteShader(fragment); + GLint ok; + glGetProgramiv(gl->program, GL_LINK_STATUS, &ok); + if (!ok) { + glGetProgramInfoLog(gl->program, (GLsizei)error_size, NULL, error); + return false; + } + /* The point table, as the file has it. */ + glGenBuffers(1, &gl->points); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, gl->points); + glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)(s->points ? s->points : 1) * VEGA_POINT_BYTES, + s->bytes + VEGA_SYMBOLS_HEADER + (size_t)s->symbols * VEGA_SYMBOL_BYTES, GL_STATIC_DRAW); + /* Bands: per symbol, 16 (start, count) pairs into one list of curves, + * each curve (by the point that begins it) in every band it reaches. */ + size_t table = (size_t)s->symbols * BANDS * 2, listed = 0; + uint32_t *counts = calloc(table + 1, sizeof(*counts)); + for (int pass = 0; pass < 2; ++pass) { + uint32_t *bands = pass ? malloc((table + listed + 1) * sizeof(*bands)) : NULL; + size_t next = 0; + for (uint32_t i = 0; i < s->symbols; ++i) { + VegaSymbol symbol = vega_symbol_at(s, i); + uint32_t *mine = pass ? bands + (size_t)i * BANDS * 2 : counts + (size_t)i * BANDS * 2; + if (pass) + for (int b = 0; b < BANDS; ++b) { + mine[2 * b] = (uint32_t)next; + next += counts[(size_t)i * BANDS * 2 + 2 * b + 1]; + mine[2 * b + 1] = 0; + } + VegaCurve c; + for (uint32_t k = symbol.first; k < symbol.first + symbol.count; ++k) { + if (!vega_symbol_curve(s, &symbol, k, &c)) continue; + float low = c.y0 < c.y1 ? c.y0 : c.y1, high = c.y0 > c.y1 ? c.y0 : c.y1; + low = c.y2 < low ? c.y2 : low; + high = c.y2 > high ? c.y2 : high; + int from = band((int)(2 * low), symbol.bottom, symbol.top), to = band((int)(2 * high), symbol.bottom, symbol.top); + for (int b = from; b <= to; ++b) { + if (pass) bands[table + mine[2 * b] + mine[2 * b + 1]] = k; + ++mine[2 * b + 1]; + listed += !pass; + } + } + } + if (pass) { + glGenBuffers(1, &gl->bands); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, gl->bands); + glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)((table + listed + 1) * sizeof(*bands)), bands, GL_STATIC_DRAW); + free(bands); + } + } + free(counts); + glGenVertexArrays(1, &gl->array); + glGenBuffers(1, &gl->instances); + glBindVertexArray(gl->array); + glBindBuffer(GL_ARRAY_BUFFER, gl->instances); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + glEnableVertexAttribArray(2); + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 36, (void *)0); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 36, (void *)16); + glVertexAttribIPointer(2, 1, GL_UNSIGNED_INT, 36, (void *)32); + glVertexAttribDivisor(0, 1); + glVertexAttribDivisor(1, 1); + glVertexAttribDivisor(2, 1); + glBindVertexArray(0); + glUseProgram(gl->program); + glUniform1ui(glGetUniformLocation(gl->program, "listed"), (GLuint)table); + gl->viewport = glGetUniformLocation(gl->program, "target"); + if (glGetError() != GL_NO_ERROR) { + snprintf(error, error_size, "GL error while uploading"); + return false; + } + return true; +} + +void vega_gl_draw(VegaGl *gl, const VegaSymbols *s, const VegaGlyph *glyphs, uint32_t count, int width, int height) { + float *instances = malloc((size_t)(count ? count : 1) * 36); + for (uint32_t i = 0; i < count; ++i) { + VegaSymbol symbol = vega_symbol_at(s, glyphs[i].symbol); + float *f = instances + (size_t)i * 9; + f[0] = glyphs[i].x, f[1] = glyphs[i].y, f[2] = glyphs[i].scale, f[3] = 0; + f[4] = symbol.left, f[5] = symbol.bottom, f[6] = symbol.right, f[7] = symbol.top; + uint32_t base = glyphs[i].symbol * BANDS * 2; + memcpy(f + 8, &base, 4); + } + glUseProgram(gl->program); + glUniform2f(gl->viewport, (float)width, (float)height); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, gl->points); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, gl->bands); + glBindVertexArray(gl->array); + glBindBuffer(GL_ARRAY_BUFFER, gl->instances); + glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)count * 36, instances, GL_STREAM_DRAW); + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE); + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)count); + glBindVertexArray(0); + free(instances); +} + +void vega_gl_close(VegaGl *gl) { + glDeleteBuffers(1, &gl->points); + glDeleteBuffers(1, &gl->bands); + glDeleteBuffers(1, &gl->instances); + glDeleteVertexArrays(1, &gl->array); + glDeleteProgram(gl->program); + *gl = (VegaGl){0}; +} diff --git a/symbols/symbols-gl.h b/symbols/symbols-gl.h new file mode 100644 index 0000000..4c6db67 --- /dev/null +++ b/symbols/symbols-gl.h @@ -0,0 +1,24 @@ +#ifndef VEGA_SYMBOLS_GL_H +#define VEGA_SYMBOLS_GL_H +#include "symbols.h" +#include <GLES3/gl31.h> + +/* Symbols drawn by the GPU (OpenGL ES 3.1): the file's point table goes up + * as it is, one storage buffer, and each pixel of a symbol's box sums the + * exact area of the curves crossing its row, as vega_coverage does. Rows + * are cut into bands at upload, each listing its curves, so a pixel reads + * only those. Needs a current context. */ +typedef struct { + GLuint program, points, bands, instances, array; + GLint viewport; +} VegaGl; + +/* A symbol placed: its origin in pixels from the target's top left, and + * pixels per unit. */ +typedef struct { float x, y, scale; uint32_t symbol; } VegaGlyph; + +bool vega_gl_open(VegaGl *, const VegaSymbols *, char *error, size_t error_size); +/* Coverage added into the red channel of the bound framebuffer. */ +void vega_gl_draw(VegaGl *, const VegaSymbols *, const VegaGlyph *, uint32_t count, int width, int height); +void vega_gl_close(VegaGl *); +#endif diff --git a/symbols/symbols-tool.c b/symbols/symbols-tool.c new file mode 100644 index 0000000..a94feb3 --- /dev/null +++ b/symbols/symbols-tool.c @@ -0,0 +1,881 @@ +#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 + * vega-symbols gpu OUT.vsym --sizes 12,16,32,64 [--frames N] */ +#include "symbols-gl.h" +#include "symbols.h" +#include <EGL/egl.h> +#include <EGL/eglext.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; +/* An outline as the pen drew it. smooth: the end lies halfway between this + * control and the next, by construction, so it may go unwritten. */ +typedef enum { MOVE, LINE, QUAD } Kind; +typedef struct { Kind kind; bool smooth; Point control, end; } Segment; +typedef struct { Segment *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; /* segments [first, first + count) */ + +static void fail(const char *why) { + fprintf(stderr, "vega-symbols: %s\n", why); + exit(1); +} + +static void add(Path *path, Segment segment) { + 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++] = segment; + path->at = segment.end; +} + +static bool same(Point a, Point b) { return a.x == b.x && a.y == b.y; } + +static void line_to(Path *path, Point to) { + if (!same(to, path->at)) add(path, (Segment){LINE, false, to, to}); +} + +static void quad_to(Path *path, Point control, Point to) { add(path, (Segment){QUAD, false, control, to}); } + +static Point lerp(Point a, Point b, double t) { return (Point){a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t}; } +static double length(double x, double y) { return sqrt(x * x + y * y); } + +/* Whether the cubic of these differences stays within tolerance of zero + * (split in halves until its control points do). */ +static bool fits(Point p0, Point p1, Point p2, Point p3, double tolerance) { + if (length(p1.x, p1.y) <= tolerance && length(p2.x, p2.y) <= tolerance) return true; + Point mid = {(p0.x + 3 * (p1.x + p2.x) + p3.x) / 8, (p0.y + 3 * (p1.y + p2.y) + p3.y) / 8}; + if (length(mid.x, mid.y) > tolerance) return false; + Point d = {(p3.x + p2.x - p1.x - p0.x) / 8, (p3.y + p2.y - p1.y - p0.y) / 8}; + return fits(p0, lerp(p0, p1, 0.5), (Point){mid.x - d.x, mid.y - d.y}, mid, tolerance) && + fits(mid, (Point){mid.x + d.x, mid.y + d.y}, lerp(p2, p3, 0.5), p3, tolerance); +} + +/* The piece of cubic c over [a, b], as a cubic. */ +static void piece(const Point c[4], double a, double b, Point out[4]) { + Point q[4]; + for (int k = 0; k < 2; ++k) { + double t = k ? b : a, u = 1 - t; + Point e = {u * u * u * c[0].x + 3 * u * u * t * c[1].x + 3 * u * t * t * c[2].x + t * t * t * c[3].x, + u * u * u * c[0].y + 3 * u * u * t * c[1].y + 3 * u * t * t * c[2].y + t * t * t * c[3].y}; + Point d = {3 * (u * u * (c[1].x - c[0].x) + 2 * u * t * (c[2].x - c[1].x) + t * t * (c[3].x - c[2].x)), + 3 * (u * u * (c[1].y - c[0].y) + 2 * u * t * (c[2].y - c[1].y) + t * t * (c[3].y - c[2].y))}; + double h = (b - a) / 3 * (k ? -1 : 1); + q[3 * k] = e; + q[1 + k] = (Point){e.x + d.x * h, e.y + d.y * h}; + } + memcpy(out, q, sizeof(q)); +} + +/* A cubic as a quadratic spline whose joins lie halfway between neighbouring + * controls (so they go unwritten), within tolerance: the approach of cu2qu + * (fontTools). With n pieces, piece i takes its control on the line between + * the points where the cubic's end tangents reach 1.5 times their control + * arms, at i / (n - 1); the fewest pieces that fit win. */ +static bool spline(const Point c[4], int n, double tolerance, Point *controls) { + if (n == 1) { + /* One quadratic: its control where the end tangents meet. */ + Point a = {c[1].x - c[0].x, c[1].y - c[0].y}, b = {c[2].x - c[3].x, c[2].y - c[3].y}; + double cross = a.x * b.y - a.y * b.x; + if (fabs(cross) < 1e-12) return false; + double t = ((c[3].x - c[0].x) * b.y - (c[3].y - c[0].y) * b.x) / cross; + controls[0] = (Point){c[0].x + a.x * t, c[0].y + a.y * t}; + Point q1 = lerp(c[0], controls[0], 2.0 / 3), q2 = lerp(c[3], controls[0], 2.0 / 3); + return fits((Point){0, 0}, (Point){q1.x - c[1].x, q1.y - c[1].y}, (Point){q2.x - c[2].x, q2.y - c[2].y}, + (Point){0, 0}, tolerance); + } + Point from = c[0], before = {0, 0}; + for (int i = 0; i < n; ++i) { + Point p[4]; + piece(c, (double)i / n, (double)(i + 1) / n, p); + Point near = lerp(p[0], p[1], 1.5), far = lerp(p[3], p[2], 1.5); + controls[i] = lerp(near, far, (double)i / (n - 1)); + } + for (int i = 0; i < n; ++i) { + Point p[4]; + piece(c, (double)i / n, (double)(i + 1) / n, p); + Point to = i + 1 < n ? lerp(controls[i], controls[i + 1], 0.5) : c[3]; + Point after = {to.x - p[3].x, to.y - p[3].y}; + Point q1 = lerp(from, controls[i], 2.0 / 3), q2 = lerp(to, controls[i], 2.0 / 3); + if (length(after.x, after.y) > tolerance || + !fits(before, (Point){q1.x - p[1].x, q1.y - p[1].y}, (Point){q2.x - p[2].x, q2.y - p[2].y}, after, tolerance)) + return false; + from = to; + before = after; + } + return true; +} + +static void cubic_to(Path *path, Point c1, Point c2, Point to) { + Point c[4] = {path->at, c1, c2, to}, controls[64]; + if (same(c[0], c1) && same(c1, c2) && same(c2, to)) return; + for (int n = 1; n <= 64; ++n) + if (spline(c, n, 1, controls)) { + for (int i = 0; i < n; ++i) + add(path, (Segment){QUAD, i + 1 < n, controls[i], i + 1 < n ? lerp(controls[i], controls[i + 1], 0.5) : to}); + return; + } + fail("a cubic that 64 quadratics do not fit"); +} + +static void close_path(Path *path) { + if (path->open) { + line_to(path, path->start); + /* A contour of a move alone draws nothing: drop it. */ + if (path->count && path->items[path->count - 1].kind == MOVE) --path->count; + } + path->open = false; +} + +static void move_to(Path *path, Point to) { + close_path(path); + add(path, (Segment){MOVE, false, to, to}); + 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. */ + +typedef struct { long x, y; bool off, move; } Emitted; +typedef struct { Emitted *items; size_t count, capacity; } Points; + +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 void emit(Points *points, Point at, bool off, bool move) { + if (points->count == points->capacity) { + points->capacity = points->capacity ? 2 * points->capacity : 256; + if (!(points->items = realloc(points->items, points->capacity * sizeof(*points->items)))) fail("out of memory"); + } + points->items[points->count++] = (Emitted){lround(at.x), lround(at.y), off, move}; +} + +/* Whether s ends where its control and the next curve's would imply. */ +static bool implied(const Segment *s, const Segment *next) { + if (s->kind != QUAD || !next || next->kind != QUAD) return false; + return s->smooth || (2 * lround(s->end.x) == lround(s->control.x) + lround(next->control.x) && + 2 * lround(s->end.y) == lround(s->control.y) + lround(next->control.y)); +} + +/* One symbol's segments as points: each contour an on point that starts it, + * then per segment its control (if a curve) and its end, unless the next + * curve's control implies it; the last end, back at the start, is written. + * A contour that starts at an implied point (as FreeType starts TrueType's + * when its first point is off) starts instead at its first written one. */ +static void emit_symbol(Points *points, const Segment *segments, size_t count) { + for (size_t i = 0; i < count;) { + size_t end = i + 1; + while (end < count && segments[end].kind != MOVE) ++end; + const Segment *body = segments + i + 1; + size_t n = end - i - 1, from = 0; + if (n > 1 && implied(&body[n - 1], &body[0])) + for (size_t k = 0; k + 1 < n; ++k) + if (!implied(&body[k], &body[k + 1])) { from = k + 1; break; } + emit(points, from ? body[from - 1].end : segments[i].end, false, true); + for (size_t m = 0; m < n; ++m) { + const Segment *s = &body[(from + m) % n], *next = m + 1 < n ? &body[(from + m + 1) % n] : NULL; + const Emitted *last = &points->items[points->count - 1]; + if (s->kind == LINE) { + if (last->off || last->x != lround(s->end.x) || last->y != lround(s->end.y)) emit(points, s->end, false, false); + continue; + } + emit(points, s->control, true, false); + if (!implied(s, next)) emit(points, s->end, false, false); + } + i = end; + } +} + +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"); + Points points = {0}; + size_t *first = calloc(count + 1, sizeof(*first)); + for (size_t i = 0; i < count; ++i) { + first[i] = points.count; + emit_symbol(&points, path->items + built[i].first, built[i].count); + } + first[count] = points.count; + /* Halve units while every coordinate is even: a TrueType font's own. */ + for (;;) { + 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; + 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); + units /= 2; + } + 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); + put16(bytes + 8, units); + put32(bytes + 12, (uint32_t)count); + put32(bytes + 16, (uint32_t)points.count); + put32(bytes + 20, (uint32_t)size); + unsigned char *at = bytes + VEGA_SYMBOLS_HEADER + count * VEGA_SYMBOL_BYTES; + for (size_t i = 0; i < points.count; ++i) { + const Emitted *e = &points.items[i]; + if (e->x < -16384 || e->x > 16383 || e->y < -16384 || e->y > 16383) fail("a coordinate does not fit 15 bits"); + put16(at + 4 * i, (int)(2 * e->x + e->off)); + put16(at + 4 * i + 2, (int)(2 * e->y + e->move)); + } + for (size_t i = 0; i < count; ++i) { + long left = 0, bottom = 0, right = 0, top = 0; + for (size_t k = first[i]; k < first[i + 1]; ++k) { + const Emitted *e = &points.items[k]; + if (k == first[i]) { left = right = e->x; bottom = top = e->y; } + left = e->x < left ? e->x : left; + right = e->x > right ? e->x : right; + bottom = e->y < bottom ? e->y : bottom; + top = e->y > top ? e->y : top; + } + unsigned char *p = bytes + VEGA_SYMBOLS_HEADER + i * VEGA_SYMBOL_BYTES; + put32(p, built[i].code); + put32(p + 4, (uint32_t)first[i]); + put32(p + 8, (uint32_t)(first[i + 1] - first[i])); + put16(p + 12, (int)left); + put16(p + 14, (int)bottom); + put16(p + 16, (int)right); + put16(p + 18, (int)top); + put16(p + 20, built[i].advance); + } + FILE *f = fopen(out, "wb"); + if (!f || fwrite(bytes, 1, size, f) != size || fclose(f)) fail("cannot write the symbol file"); + char error[128]; + VegaSymbols check; + if (!vega_symbols_open(&check, bytes, size, error, sizeof(error))) fail(error); + size_t curves = 0; + VegaCurve curve; + for (uint32_t i = 0; i < check.symbols; ++i) { + VegaSymbol symbol = vega_symbol_at(&check, i); + for (uint32_t k = symbol.first; k < symbol.first + symbol.count; ++k) curves += vega_symbol_curve(&check, &symbol, k, &curve); + } + printf("{\"symbols\":%zu,\"points\":%zu,\"curves\":%zu,\"units\":%u,\"bytes\":%zu}\n", count, points.count, curves, + units, size); + free(bytes); + free(first); + free(points.items); +} + +/* 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((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], '='); + 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; + uint32_t most = 0; + for (uint32_t i = 0; i < s->symbols; ++i) most = vega_symbol_at(s, i).count > most ? vega_symbol_at(s, i).count : most; + VegaCurve *curves = malloc((most + 1) * sizeof(*curves)); + 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"); + uint32_t count = vega_symbol_curves(s, &symbol, curves); + touched += count ? (long)curves[count - 1].x2 : 0; + } + 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); + free(curves); +} + +/* 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, points, 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; + uint32_t curves = 0; + VegaCurve c; + for (uint32_t k = symbol.first; k < symbol.first + symbol.count; ++k) { + if (!vega_symbol_curve(&s, &symbol, k, &c)) continue; + ++curves; + /* 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,\"points\":%u,\"curves\":%u,\"bounds\":[%d,%d,%d,%d],\"advance\":%d,\"area\":%.1f}", + i ? "," : "", symbol.code, symbol.count, curves, 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; +} + +/* ---- The GPU: a context of its own, no window (EGL, Mesa's surfaceless platform). */ + +static void gl_context(void) { + PFNEGLGETPLATFORMDISPLAYEXTPROC platform = (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); + EGLDisplay display = platform ? platform(EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY, NULL) : EGL_NO_DISPLAY; + EGLint major, minor, found; + EGLConfig config; + EGLint wanted[] = {EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE}; + EGLint version[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE}; + if (display == EGL_NO_DISPLAY || !eglInitialize(display, &major, &minor) || !eglBindAPI(EGL_OPENGL_ES_API) || + !eglChooseConfig(display, wanted, &config, 1, &found)) + fail("no EGL display for OpenGL ES 3.1"); + /* With no surface, a context needs no config (EGL_KHR_no_config_context). */ + EGLContext context = eglCreateContext(display, found ? config : EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, version); + if (context == EGL_NO_CONTEXT || !eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context)) + fail("no OpenGL ES 3.1 context"); +} + +/* A width x height target of 8-bit red, bound. */ +static GLuint gl_target(int width, int height) { + GLuint texture, framebuffer; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width, height); + glGenFramebuffers(1, &framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) fail("no framebuffer"); + glViewport(0, 0, width, height); + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); + return texture; +} + +/* Every symbol in a grid of cells, drawn by the GPU and by vega_coverage: + * how far apart; then pages of text (glyphs cycling through the file) + * drawn again and again: how long a page takes. */ +static int gpu(int argc, char **argv) { + if (argc < 5 || strcmp(argv[3], "--sizes")) fail("usage: vega-symbols gpu SYMBOLS.vsym --sizes 12,16,32,64 [--frames N]"); + int frames = argc > 6 && !strcmp(argv[5], "--frames") ? atoi(argv[6]) : 20; + if (frames < 1) fail("--frames"); + char *bytes, error[4096]; + VegaSymbols s = open_symbols(argv[2], &bytes); + gl_context(); + printf("{\"renderer\":\"%s\"}\n", (const char *)glGetString(GL_RENDERER)); + VegaGl gl; + double started = seconds(); + if (!vega_gl_open(&gl, &s, error, sizeof(error))) fail(error); + glFinish(); + double open_ms = (seconds() - started) * 1e3; + for (const char *p = argv[4]; *p;) { + char *end; + long size = strtol(p, &end, 10); + p = *end == ',' ? end + 1 : end; + if (size <= 0 || size > 512) fail("--sizes"); + double scale = (double)size / s.units; + /* Cells that hold any symbol, so none reaches into another's. */ + int left = 0, bottom = 0, right = 0, top = 0; + for (uint32_t i = 0; i < s.symbols; ++i) { + VegaSymbol symbol = vega_symbol_at(&s, i); + left = symbol.left < left ? symbol.left : left; + bottom = symbol.bottom < bottom ? symbol.bottom : bottom; + right = symbol.right > right ? symbol.right : right; + top = symbol.top > top ? symbol.top : top; + } + int cell = (int)ceil((right - left > top - bottom ? right - left : top - bottom) * scale) + 4; + int across = 4096 / cell, rows = (int)((s.symbols + (uint32_t)across - 1) / (uint32_t)across); + if (rows * cell > 4096) rows = 4096 / cell; + int width = across * cell, height = rows * cell; + uint32_t shown = s.symbols < (uint32_t)(across * rows) ? s.symbols : (uint32_t)(across * rows); + VegaGlyph *glyphs = malloc(shown * sizeof(*glyphs)); + for (uint32_t i = 0; i < shown; ++i) + glyphs[i] = (VegaGlyph){(float)(i % (uint32_t)across * (uint32_t)cell) + 2 - (float)(left * scale), + (float)(i / (uint32_t)across * (uint32_t)cell) + 2 + (float)(top * scale), (float)scale, i}; + GLuint texture = gl_target(width, height); + vega_gl_draw(&gl, &s, glyphs, shown, width, height); + unsigned char *drawn = malloc((size_t)width * (size_t)height), *reference = malloc((size_t)cell * (size_t)cell); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(0, 0, width, height, GL_RED, GL_UNSIGNED_BYTE, drawn); + double total = 0; + uint64_t pixels = 0; + int worst = 0; + for (uint32_t i = 0; i < shown; ++i) { + VegaSymbol symbol = vega_symbol_at(&s, i); + int x0 = (int)(i % (uint32_t)across) * cell, y0 = (int)(i / (uint32_t)across) * cell; + vega_symbol_raster(&s, &symbol, scale, (x0 - glyphs[i].x) / scale, (glyphs[i].y - y0) / scale, cell, cell, reference); + for (int j = 0; j < cell; ++j) + for (int k = 0; k < cell; ++k) { + /* GL's rows run bottom up */ + int gpu_value = drawn[(size_t)(height - 1 - (y0 + j)) * (size_t)width + (size_t)(x0 + k)]; + int difference = abs(gpu_value - reference[j * cell + k]); + total += difference; + worst = difference > worst ? difference : worst; + ++pixels; + } + } + /* Pages: glyphs at their advances in lines, a full target's worth. */ + int page_width = 1920, page_height = 1080; + uint32_t page = 0, capacity = 1 << 16; + VegaGlyph *text = malloc(capacity * sizeof(*text)); + float x = 0, line = (float)size * 1.2f; + for (float y = line; y + (float)size * 0.3f < (float)page_height && page < capacity; ) { + VegaSymbol symbol = vega_symbol_at(&s, page % s.symbols); + float advance = (float)(symbol.advance * scale); + if (x + advance > (float)page_width) { x = 0; y += line; continue; } + text[page++] = (VegaGlyph){x, y, (float)scale, page % s.symbols}; + x += advance; + } + glDeleteTextures(1, &texture); + texture = gl_target(page_width, page_height); + vega_gl_draw(&gl, &s, text, page, page_width, page_height); + glFinish(); + started = seconds(); + for (int f = 0; f < frames; ++f) vega_gl_draw(&gl, &s, text, page, page_width, page_height); + glFinish(); + double page_ms = (seconds() - started) / frames * 1e3; + printf("{\"pixels_per_em\":%ld,\"symbols\":%u,\"mean_difference\":%.3f,\"worst\":%d,\"page_glyphs\":%u," + "\"page_ms\":%.2f,\"upload_ms\":%.1f}\n", size, shown, pixels ? total / (double)pixels : 0, worst, page, page_ms, open_ms); + glDeleteTextures(1, &texture); + free(text); + free(glyphs); + free(drawn); + free(reference); + } + vega_gl_close(&gl); + 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); + if (argc > 1 && !strcmp(argv[1], "gpu")) return gpu(argc, argv); + fail("vega-symbols build|compare|info|render|gpu ..."); + return 2; +} diff --git a/symbols/symbols.c b/symbols/symbols.c new file mode 100644 index 0000000..cedd68f --- /dev/null +++ b/symbols/symbols.c @@ -0,0 +1,185 @@ +#include "symbols.h" +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static const unsigned char magic[8] = {'V', 'E', 'G', 'A', 'S', 'Y', 'M', '2'}; + +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)}; +} + +/* 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) { + 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); + /* x >> 1 without relying on how a negative number shifts */ + return (VegaPoint){(int16_t)((x - (x & 1)) / 2), (int16_t)((y - (y & 1)) / 2), x & 1, y & 1}; +} + +#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->points * VEGA_POINT_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 points", size, s->symbols, s->points); + 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->points || symbol.count > s->points - symbol.first) REFUSE("symbol %u: points", i); + 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); + 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); + } + } + 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; +} + +bool vega_symbol_curve(const VegaSymbols *s, const VegaSymbol *symbol, uint32_t index, VegaCurve *out) { + VegaPoint p = vega_point(s, index); + if (p.off) { + VegaPoint a = vega_point(s, index - 1), b = vega_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); + 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; +} + +/* 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; +} + +/* Without branches on the flags, which no predictor guesses: implied points + * and whether a point begins a curve come out of arithmetic on them. */ +uint32_t vega_symbol_curves(const VegaSymbols *s, const VegaSymbol *symbol, VegaCurve *out) { + if (!symbol->count) return 0; + const unsigned char *p = s->bytes + VEGA_SYMBOLS_HEADER + (size_t)s->symbols * VEGA_SYMBOL_BYTES + + (size_t)symbol->first * VEGA_POINT_BYTES; + uint32_t count = 0; + int x = i16(p), y = i16(p + 2), off = x & 1; + float bx = 0, by = 0, bo = 0, hx = (float)(x - off) * 0.5f, hy = (float)(y - (y & 1)) * 0.5f, ho = (float)off; + for (uint32_t k = 1; k < symbol->count; ++k) { + p += VEGA_POINT_BYTES; + x = i16(p); + y = i16(p + 2); + float nx = (float)(x - (x & 1)) * 0.5f, ny = (float)(y - (y & 1)) * 0.5f, no = (float)(x & 1); + /* before, or halfway to here if before is off; here if here is on */ + float sx = bx + (hx - bx) * 0.5f * bo, sy = by + (hy - by) * 0.5f * bo; + out[count] = (VegaCurve){hx + (sx - hx) * ho, hy + (sy - hy) * ho, hx + (nx - hx) * 0.5f * (1 - ho), + hy + (ny - hy) * 0.5f * (1 - ho), nx + (hx - nx) * 0.5f * no, ny + (hy - ny) * 0.5f * no}; + /* an off point begins a curve, and so does an on point before an on + * point of the same contour (a line) */ + count += (uint32_t)(off | (((x & 1) ^ 1) & ((y & 1) ^ 1))); + off = x & 1; + bx = hx, by = hy, bo = ho; + hx = nx, hy = ny, ho = no; + } + return count; +} + +double vega_coverage(const VegaCurve *curves, uint32_t count, 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 = 0; k < count; ++k) { + const VegaCurve *c = &curves[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) { + VegaCurve *curves = malloc((symbol->count ? symbol->count : 1) * sizeof(*curves)); + uint32_t count = curves ? vega_symbol_curves(s, symbol, curves) : 0; + for (int j = 0; j < height; ++j) + for (int i = 0; i < width; ++i) { + double coverage = vega_coverage(curves, count, 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); + } + free(curves); +} diff --git a/symbols/symbols.h b/symbols/symbols.h new file mode 100644 index 0000000..998ff3f --- /dev/null +++ b/symbols/symbols.h @@ -0,0 +1,54 @@ +#ifndef VEGA_SYMBOLS_H +#define VEGA_SYMBOLS_H +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +/* Symbols: glyphs and icons as outlines of quadratic curves in one file, + * verified once and then read where it lies (docs/design/002-symbol-format.md). + * An outline is a chain of points, on or off the curve, as in TrueType: two + * off points in a row imply the on point halfway between them. */ +enum { VEGA_SYMBOLS_HEADER = 28, VEGA_SYMBOL_BYTES = 24, VEGA_POINT_BYTES = 4 }; +typedef struct { + int16_t x, y; + bool off; /* a control point */ + bool move; /* starts a contour */ +} VegaPoint; +typedef struct { float x0, y0, x1, y1, x2, y2; } VegaCurve; /* a line has its middle as control */ +typedef struct { + uint32_t code, first, count; /* its points: [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, points; +} 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 + * or its symbol. The points 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); +VegaPoint vega_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 + * the point and its two neighbours alone (as a GPU would fetch it). */ +bool vega_symbol_curve(const VegaSymbols *, const VegaSymbol *, uint32_t index, VegaCurve *out); +/* All of a symbol's curves, in one pass over its points; out has room for + * symbol->count (a symbol has no more curves than points). */ +uint32_t vega_symbol_curves(const VegaSymbols *, const VegaSymbol *, VegaCurve *out); +/* 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); +/* 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 |
