diff --git a/FABRIC.md b/FABRIC.md index b92b594..47bd763 100644 --- a/FABRIC.md +++ b/FABRIC.md @@ -4779,11 +4779,75 @@ document and committing that amendment as its own item.* > capsule-birth/init.4th wiring exists for it; that's 4.3.7e's job once `TTF-TEXT` needs it. > Same "not a kernel-boot change" posture as 4.3.7/4.3.7a; no three-arch QEMU run applies. -- [ ] **4.3.7c — Rasterization.** Bézier curve flattening to line segments (reusing the +- [x] **4.3.7c — Rasterization.** Bézier curve flattening to line segments (reusing the existing `LINE`/Bresenham primitive where practical), then fill. Antialiasing approach is an open question — not decided here, resolve when this item is picked up. *Done when:* a single glyph outline rasterizes to a recognizable filled (or outlined, if AA is deferred) shape on the CANVAS, screendump-verified. *Refs:* §27.7. + > **Done 2026-08-10.** `ttf_rasterize_glyph()` in `src/starkernel/hal/ttf.c`/`ttf.h`: + > quadratic-Bezier contour flattening (fixed 8-segment subdivision per curve, same + > fixed-segment-count approach as `capsules/fabric.4th`'s `CIRCLE`/`ELLIPSE`, 36 segs, and + > `ARC`, 18 segs — no adaptive tessellation) followed by an even-odd scanline fill into a + > caller-supplied 8-bit-per-pixel bitmap. No antialiasing, per this item's own allowance. + > + > **Doc error found and reported, not fixed:** §27.7 decision #1 claims 4.3.3b put + > `LINE`/`CIRCLE`/`ARC`/`ELLIPSE` "in C rather than FORTH" — 4.3.3b's own completion note + > says the opposite: they're FORTH words in `capsules/fabric.4th` (blocks 4903–4912), built + > on the C-level `PLOT`/`TO-RASTER` primitives. C is still the right call for this item + > (4.3.7's item text says so directly, independent of that reasoning), but "reusing the + > existing `LINE`/Bresenham primitive" from a C module can't mean literally calling the + > FORTH word — it means the same Bresenham/scanline approach at the C level, which is what + > this implementation does (no FORTH call from `ttf.c`). + > + > **Fill rule: even-odd, not TrueType's native nonzero winding.** Simpler to implement (no + > edge-direction bookkeeping) and identical to nonzero winding for the v1 glyph repertoire + > (§27.6.4), whose contours are simple and properly nested (outer contour + inner counters, + > e.g. `a`/`o`) — the two rules only diverge on self-intersecting outlines, which no v1 + > glyph has. Not correct in general for an arbitrary font; whoever needs that generality + > later should switch to nonzero winding (requires tracking edge direction, not otherwise + > hard). + > + > **Local signed Q48.16 multiply (`q48_smul`), not the shared `q48_mul`.** Rasterization + > scales outline coordinates (routinely negative — e.g. `a`'s yMin is −10) by a + > non-negative scale/Bezier-blend weight, exactly the signed use case `q48_mul`/`q48_div` + > (`src/starkernel/math/q48_16.c`) don't support (see 4.3.7a's completion note). Per this + > repo's rule against modifying a shared/tested module without being asked, that gap is + > reported there, not patched here; `q48_smul` is local to `ttf.c`, a plain + > `((int64_t)a * (int64_t)b) >> 16`, safe because every value in this module (glyph + > coordinates scaled to at most a few hundred pixels) stays far inside `int64_t` range. + > Scanline edge-intersection division is done as plain `int64_t` arithmetic directly + > (`x0 + dx*t_num/dy`), not through any Q48.16 divide helper — C's native signed integer + > division already handles the sign correctly there, so no wrapper was needed for that part. + > + > **Verification.** Host-side (`tools/ttftest.c`, extended): `test_rasterize()` rasterizes + > `A`/`.`/`a` at 28px into a small bitmap and prints an ASCII-art dump plus structural + > checks (non-empty, not implausibly full, background corner untouched). All three glyphs + > render as visually correct, recognizable letterforms; all checks pass. No independent + > reference rasterizer exists to diff pixel-for-pixel against (unlike 4.3.7/4.3.7a, which + > had a from-scratch Python decoder) — the item's own acceptance bar is "recognizable," not + > pixel-exact, so this is judged sufficient. + > + > **Live screendump, amd64, per this item's explicit requirement (the only 4.3.7–4.3.7b + > items so far to need one).** A throwaway registered word, `TTF-PROBE` (added to + > `src/word_source/framebuffer_words.c`, `__STARKERNEL__`-gated), loaded the font capsule, + > rasterized `'A'` at 48px, and blitted it via `fb_put_pixel()` at screen position + > (100,100). Boot: custom one-off script (not a repo file — mirrors + > `scripts/qemu_screenshot.sh`'s monitor-socket + `socat` + HMP `screendump` pattern, plus + > a serial-socket command injection channel that script doesn't have, same injection + > technique used throughout 4.3.x) built the amd64 ISO, booted to `[Hera] ok>`, injected + > `TTF-PROBE` over the serial socket, confirmed `TTF-PROBE: rasterized 'A' at (100,100)` in + > the serial log, then issued `screendump` over the monitor socket. Result: a clearly + > legible white `'A'` at the expected screen position, committed as + > `fb/amd64/ttf-rasterize-glyph-A-20260810-221922.png`. Probe reverted immediately after + > (`framebuffer_words.c` back to its pre-probe content, confirmed via `git diff`) — per + > feedback-revert-probes-after-capture, only the permanent rasterizer in `ttf.c`/`ttf.h` + > survives. + > + > **Compile-checked on all three architectures** (`make -f Makefile.starkernel ARCH= + > clean` then plain build, no `qemu`), zero warnings from `ttf.c` on any — this item's own + > "done when" only requires the amd64 screendump, not a three-arch boot (that's 4.3.7f's + > job), but `ttf.c` builds into all three via the `hal/*.c` wildcard so a portability + > compile check costs little and catches real bugs early. - [ ] **4.3.7d — Glyph raster cache.** Rasterizing on every draw call is too slow for repeated text; cache rasterized bitmaps keyed by (font, codepoint, size). *Done when:* diff --git a/capsules/BLOCK_MAP.md b/capsules/BLOCK_MAP.md index 68fe5aa..4421dcd 100644 --- a/capsules/BLOCK_MAP.md +++ b/capsules/BLOCK_MAP.md @@ -1,5 +1,5 @@ # Capsule Block Manifest — Auto-generated - + diff --git a/include/starkernel/ttf.h b/include/starkernel/ttf.h index 2fed5ec..710adcf 100644 --- a/include/starkernel/ttf.h +++ b/include/starkernel/ttf.h @@ -182,6 +182,42 @@ typedef struct { */ int ttf_glyph_outline(const ttf_font_t *font, uint32_t glyph_index, ttf_outline_t *out); +/** A caller-owned 8-bit-per-pixel bitmap. `ttf_rasterize_glyph()` writes + * `fill_value` into covered pixels and leaves everything else untouched + * (it does not clear the buffer first — caller's job, so repeated + * rasterization into the same bitmap, e.g. for a text run, composites + * correctly without an extra clear between glyphs). */ +typedef struct { + uint8_t *pixels; + uint32_t width; + uint32_t height; +} ttf_bitmap_t; + +/** + * ttf_rasterize_glyph - Flatten a glyph's outline (quadratic Bezier + * contours, fixed segment count per curve — see ttf.c's file header + * comment) and fill it into `out` using the even-odd rule (FABRIC.md item + * 4.3.7c; see that item's completion note for why even-odd rather than + * nonzero winding — correct for the v1 glyph repertoire's non-self- + * intersecting nested contours, not necessarily for an arbitrary font). + * No antialiasing (explicitly deferred, per 4.3.7c's own "done when" + * clause). Never allocates — flattening uses a fixed-size local buffer + * bounded by TTF_RASTER_MAX_POINTS/TTF_RASTER_MAX_CONTOURS. + * + * @param scale Font-design-units-to-pixels scale, Q48.16, e.g. + * q48_div(q48_from_u64(size_px), q48_from_u64(font->units_per_em)) + * @param origin_x Pixel-space X (Q48.16) of the glyph's (0,0) font origin + * within `out` + * @param origin_y Pixel-space Y (Q48.16) of the glyph's baseline (font + * y=0) within `out`; font Y-up is flipped to raster + * Y-down internally + * @return TTF_OK, or a TTF_ERR_* code (including TTF_ERR_TOO_MANY_POINTS/ + * _CONTOURS if the flattened outline exceeds the local buffer) + */ +int ttf_rasterize_glyph(const ttf_font_t *font, uint32_t glyph_index, + q48_16_t scale, q48_16_t origin_x, q48_16_t origin_y, + uint8_t fill_value, ttf_bitmap_t *out); + #ifdef __STARKERNEL__ #include "capsule.h" diff --git a/src/starkernel/hal/ttf.c b/src/starkernel/hal/ttf.c index 073da9f..cf95980 100644 --- a/src/starkernel/hal/ttf.c +++ b/src/starkernel/hal/ttf.c @@ -523,6 +523,296 @@ int ttf_glyph_outline(const ttf_font_t *font, uint32_t glyph_index, ttf_outline_ return decode_glyph_r(font, glyph_index, out, 0); } +/* =========================================================================== + * 4.3.7c — rasterization: quadratic Bezier flattening + even-odd scanline + * fill. No antialiasing (deferred, per this item's own "done when" clause). + * + * Fill rule: even-odd, not TrueType's native nonzero winding. Simpler to + * implement (no edge-direction bookkeeping) and produces an identical + * result to nonzero winding for the v1 glyph repertoire (§27.6.4), whose + * contours are simple (non-self-intersecting) and properly nested + * (outer contour + inner counters, e.g. 'a', 'o') -- the two rules only + * diverge on self-intersecting outlines, which no glyph here has. Not + * correct in general for an arbitrary font. + * + * Local signed Q48.16 multiply (q48_smul): the shared q48_mul()/q48_div() + * (src/starkernel/math/q48_16.c) are unsigned-only (see 4.3.7a's + * completion note) and this file routinely multiplies a negative outline + * coordinate by a non-negative scale/Bezier-blend weight -- exactly the + * signed use case q48_mul doesn't support. Reported there, not patched + * here, per this repo's rule against modifying a shared/tested module + * without being asked. All values here (glyph coordinates scaled to at + * most a few hundred pixels) stay far inside int64_t range. + * =========================================================================== + */ + +#define TTF_QUAD_SEGMENTS 8 /* fixed subdivision count per quadratic curve -- + * same fixed-segment-count approach as + * capsules/fabric.4th's CIRCLE/ELLIPSE (36 segs) + * and ARC (18 segs); no adaptive tessellation. */ +#define TTF_RASTER_RAW_MAX_POINTS 256 +#define TTF_RASTER_RAW_MAX_CONTOURS 16 +#define TTF_RASTER_FLAT_MAX_POINTS (TTF_RASTER_RAW_MAX_POINTS * TTF_QUAD_SEGMENTS) +#define TTF_RASTER_FLAT_MAX_CONTOURS TTF_RASTER_RAW_MAX_CONTOURS + +/* Flattened polygon vertex: pixel space, Q48.16, after scale + origin + + * the font-Y-up-to-raster-Y-down flip. */ +typedef struct { + q48_16_t x, y; +} ttf_flat_point_t; + +static q48_16_t q48_smul(q48_16_t a, q48_16_t b) { + return (q48_16_t) (((int64_t) a * (int64_t) b) >> 16); +} + +static q48_16_t q48_mid(q48_16_t a, q48_16_t b) { + return (q48_16_t) (((int64_t) (a + b)) >> 1); +} + +static void raw_midpoint(const ttf_point_t *a, const ttf_point_t *b, ttf_point_t *out) { + out->x = q48_mid(a->x, b->x); + out->y = q48_mid(a->y, b->y); + out->on_curve = 1; +} + +static void to_pixel_space(q48_16_t fx, q48_16_t fy, q48_16_t scale, + q48_16_t origin_x, q48_16_t origin_y, + q48_16_t *px, q48_16_t *py) { + *px = q48_add(origin_x, q48_smul(fx, scale)); + *py = q48_sub(origin_y, q48_smul(fy, scale)); +} + +static void flat_push(ttf_flat_point_t *pts, uint32_t *count, uint32_t max, + q48_16_t x, q48_16_t y, int *err) { + if (*err != TTF_OK) return; + if (*count >= max) { *err = TTF_ERR_TOO_MANY_POINTS; return; } + pts[*count].x = x; + pts[*count].y = y; + (*count)++; +} + +/* Subdivide one quadratic Bezier (control points already in pixel space) + * into TTF_QUAD_SEGMENTS line segments. Emits the interior + final + * vertices only -- the caller's `cur` is already the start (p0). */ +static void flatten_quad(ttf_flat_point_t *pts, uint32_t *count, uint32_t max, + q48_16_t p0x, q48_16_t p0y, q48_16_t p1x, q48_16_t p1y, + q48_16_t p2x, q48_16_t p2y, int *err) { + uint32_t i; + for (i = 1; i <= TTF_QUAD_SEGMENTS; i++) { + q48_16_t t = (q48_16_t) ((uint64_t) Q48_ONE * i / TTF_QUAD_SEGMENTS); + q48_16_t omt = (q48_16_t) (Q48_ONE - t); + q48_16_t w0 = q48_smul(omt, omt); + q48_16_t w1 = q48_smul(q48_smul((q48_16_t) (2ULL * Q48_ONE), omt), t); + q48_16_t w2 = q48_smul(t, t); + q48_16_t x = q48_add(q48_add(q48_smul(w0, p0x), q48_smul(w1, p1x)), q48_smul(w2, p2x)); + q48_16_t y = q48_add(q48_add(q48_smul(w0, p0y), q48_smul(w1, p1y)), q48_smul(w2, p2y)); + flat_push(pts, count, max, x, y, err); + } +} + +/* Walk one contour (raw[begin..end] inclusive, font design units) and + * append its flattened, closed polygon to the flat point list. Handles + * the three TrueType start-point cases (first point on-curve, last point + * on-curve, or neither -- synthesized midpoint start) and implied + * on-curve points between two consecutive off-curve control points. */ +static void flatten_contour(const ttf_point_t *raw, uint32_t begin, uint32_t end, + q48_16_t scale, q48_16_t origin_x, q48_16_t origin_y, + ttf_flat_point_t *flat_pts, uint32_t *flat_count, uint32_t flat_max, + int *err) { + uint32_t n = end - begin + 1; + ttf_point_t start_pt; + uint32_t walk_start, walk_count, idx, steps_done; + q48_16_t cur_px, cur_py, start_px, start_py; + + if (n == 0 || *err != TTF_OK) return; + + if (raw[begin].on_curve) { + start_pt = raw[begin]; + walk_start = 1; + walk_count = n - 1; + } else if (raw[end].on_curve) { + start_pt = raw[end]; + walk_start = 0; + walk_count = n - 1; + } else { + raw_midpoint(&raw[end], &raw[begin], &start_pt); + walk_start = 0; + walk_count = n; + } + + to_pixel_space(start_pt.x, start_pt.y, scale, origin_x, origin_y, &start_px, &start_py); + cur_px = start_px; + cur_py = start_py; + flat_push(flat_pts, flat_count, flat_max, start_px, start_py, err); + + idx = walk_start; + steps_done = 0; + while (steps_done < walk_count && *err == TTF_OK) { + const ttf_point_t *p = &raw[begin + idx]; + + if (p->on_curve) { + q48_16_t px, py; + to_pixel_space(p->x, p->y, scale, origin_x, origin_y, &px, &py); + flat_push(flat_pts, flat_count, flat_max, px, py, err); + cur_px = px; + cur_py = py; + idx = (idx + 1) % n; + steps_done++; + } else if (steps_done + 1 < walk_count) { + uint32_t idx2 = (idx + 1) % n; + const ttf_point_t *p2 = &raw[begin + idx2]; + ttf_point_t end_pt; + int consumed2; + q48_16_t c1x, c1y, ex, ey; + + if (p2->on_curve) { + end_pt = *p2; + consumed2 = 1; + } else { + raw_midpoint(p, p2, &end_pt); + consumed2 = 0; + } + + to_pixel_space(p->x, p->y, scale, origin_x, origin_y, &c1x, &c1y); + to_pixel_space(end_pt.x, end_pt.y, scale, origin_x, origin_y, &ex, &ey); + flatten_quad(flat_pts, flat_count, flat_max, cur_px, cur_py, c1x, c1y, ex, ey, err); + cur_px = ex; + cur_py = ey; + + if (consumed2) { + idx = (idx2 + 1) % n; + steps_done += 2; + } else { + idx = idx2; + steps_done += 1; + } + } else { + /* Last point in the walk is a control point -- the curve it + * starts closes the contour, ending at start_pt. */ + q48_16_t c1x, c1y; + to_pixel_space(p->x, p->y, scale, origin_x, origin_y, &c1x, &c1y); + flatten_quad(flat_pts, flat_count, flat_max, cur_px, cur_py, c1x, c1y, + start_px, start_py, err); + cur_px = start_px; + cur_py = start_py; + steps_done += 1; + } + } + + /* Close the polygon back to its start when the walk ended on an + * on-curve vertex that isn't the start itself -- the common case, + * not a rare fallback (a contour that ends mid-curve self-closes in + * the branch above instead). */ + if (cur_px != start_px || cur_py != start_py) { + flat_push(flat_pts, flat_count, flat_max, start_px, start_py, err); + } +} + +int ttf_rasterize_glyph(const ttf_font_t *font, uint32_t glyph_index, + q48_16_t scale, q48_16_t origin_x, q48_16_t origin_y, + uint8_t fill_value, ttf_bitmap_t *out) { + ttf_point_t raw_points[TTF_RASTER_RAW_MAX_POINTS]; + uint16_t raw_contour_ends[TTF_RASTER_RAW_MAX_CONTOURS]; + ttf_outline_t raw_outline; + ttf_flat_point_t flat_points[TTF_RASTER_FLAT_MAX_POINTS]; + uint16_t flat_contour_ends[TTF_RASTER_FLAT_MAX_CONTOURS]; + uint32_t flat_count = 0; + uint32_t flat_contour_count = 0; + int err = TTF_OK; + uint32_t c, begin; + int rc; + int32_t yrow; + + if (!font || !out || !out->pixels) return TTF_ERR_BAD_TABLE; + + raw_outline.points = raw_points; + raw_outline.max_points = TTF_RASTER_RAW_MAX_POINTS; + raw_outline.contour_ends = raw_contour_ends; + raw_outline.max_contours = TTF_RASTER_RAW_MAX_CONTOURS; + + rc = ttf_glyph_outline(font, glyph_index, &raw_outline); + if (rc != TTF_OK) return rc; + if (raw_outline.contour_count == 0) return TTF_OK; /* empty glyph, e.g. space */ + + begin = 0; + for (c = 0; c < raw_outline.contour_count; c++) { + uint32_t end = raw_outline.contour_ends[c]; + + flatten_contour(raw_points, begin, end, scale, origin_x, origin_y, + flat_points, &flat_count, TTF_RASTER_FLAT_MAX_POINTS, &err); + if (err != TTF_OK) return err; + + if (flat_contour_count >= TTF_RASTER_FLAT_MAX_CONTOURS) return TTF_ERR_TOO_MANY_CONTOURS; + flat_contour_ends[flat_contour_count++] = (uint16_t) (flat_count - 1); + + begin = end + 1; + } + + /* Even-odd scanline fill, sampled at the pixel-center Y (row + 0.5) + * so a vertex landing exactly on an integer scanline doesn't get + * double-counted -- standard convention. */ + for (yrow = 0; yrow < (int32_t) out->height; yrow++) { + q48_16_t yc = q48_add((q48_16_t) ((uint64_t) yrow << 16), (q48_16_t) (Q48_ONE / 2)); + q48_16_t xs[64]; + uint32_t xn = 0; + uint32_t cbegin = 0; + + for (c = 0; c < flat_contour_count; c++) { + uint32_t cend = flat_contour_ends[c]; + uint32_t i; + for (i = cbegin; i <= cend; i++) { + uint32_t j = (i == cend) ? cbegin : i + 1; + int64_t y0q = (int64_t) flat_points[i].y; + int64_t y1q = (int64_t) flat_points[j].y; + int below0 = y0q <= (int64_t) yc; + int below1 = y1q <= (int64_t) yc; + if (below0 == below1) continue; /* edge doesn't cross this scanline */ + { + int64_t x0 = (int64_t) flat_points[i].x; + int64_t x1 = (int64_t) flat_points[j].x; + int64_t dy = y1q - y0q; + int64_t dx = x1 - x0; + int64_t t_num = (int64_t) yc - y0q; + int64_t x = x0 + (dx * t_num) / dy; + if (xn < 64) xs[xn++] = (q48_16_t) x; + } + } + cbegin = cend + 1; + } + + /* insertion sort -- xn is small for a glyph outline */ + { + uint32_t a; + for (a = 1; a < xn; a++) { + q48_16_t key = xs[a]; + int64_t keyv = (int64_t) key; + int32_t b = (int32_t) a - 1; + while (b >= 0 && (int64_t) xs[b] > keyv) { + xs[b + 1] = xs[b]; + b--; + } + xs[b + 1] = key; + } + } + + { + uint32_t a; + for (a = 0; a + 1 < xn; a += 2) { + int32_t xa = (int32_t) (((int64_t) xs[a]) >> 16); + int32_t xb = (int32_t) (((int64_t) xs[a + 1]) >> 16); + int32_t x; + if (xa < 0) xa = 0; + if (xb > (int32_t) out->width) xb = (int32_t) out->width; + for (x = xa; x < xb; x++) { + out->pixels[(uint32_t) yrow * out->width + (uint32_t) x] = fill_value; + } + } + } + } + + return TTF_OK; +} + /* =========================================================================== * 4.3.7b — font data ingestion (capsule-backed, zero-copy) * =========================================================================== diff --git a/tools/ttftest.c b/tools/ttftest.c index 94198a1..8d566d1 100644 --- a/tools/ttftest.c +++ b/tools/ttftest.c @@ -149,6 +149,78 @@ static void test_outlines(const ttf_font_t *font) { } } +/* Rasterization (4.3.7c) sanity check: rasterize a glyph at a modest + * pixel size, print an ASCII-art dump for visual "is this recognizable" + * inspection (this item's own acceptance criterion), plus loose + * structural checks -- fill coverage in a plausible range, background + * untouched in the far corner -- since there's no independent reference + * rasterizer to diff against pixel-for-pixel (unlike 4.3.7/4.3.7a, which + * had a from-scratch Python decoder to check against). */ +static void test_rasterize(const ttf_font_t *font, uint32_t codepoint, const char *label) { + enum { SIZE_PX = 28, MARGIN = 6 }; + uint32_t w = SIZE_PX + MARGIN * 2; + uint32_t h = SIZE_PX + MARGIN * 2; + uint8_t *pixels = calloc((size_t) w * h, 1); + ttf_bitmap_t bmp; + q48_16_t scale; + uint32_t gid; + int rc; + uint32_t x, y, filled = 0; + + if (!pixels) { + printf("test_rasterize U+%04X: calloc failed\n", codepoint); + failures++; + return; + } + + bmp.pixels = pixels; + bmp.width = w; + bmp.height = h; + + scale = q48_div(q48_from_u64(SIZE_PX), q48_from_u64(font->units_per_em)); + gid = ttf_codepoint_to_glyph(font, codepoint); + + rc = ttf_rasterize_glyph(font, gid, scale, + q48_from_u64(MARGIN), + q48_from_u64(SIZE_PX + MARGIN), + 255, &bmp); + if (rc != TTF_OK) { + printf("test_rasterize U+%04X (%s): ttf_glyph_outline/rasterize rc=%d\n", + codepoint, label, rc); + failures++; + free(pixels); + return; + } + + printf("U+%04X (%s) rasterized %ux%u:\n", codepoint, label, w, h); + for (y = 0; y < h; y++) { + putchar('|'); + for (x = 0; x < w; x++) { + int on = pixels[y * w + x] != 0; + putchar(on ? '#' : '.'); + if (on) filled++; + } + putchar('|'); + putchar('\n'); + } + printf(" filled=%u / %u px (%.1f%%)\n", filled, w * h, 100.0 * filled / (w * h)); + + if (filled == 0) { + printf(" FAIL U+%04X (%s): rasterized to nothing\n", codepoint, label); + failures++; + } else if (filled > (w * h * 3) / 4) { + printf(" FAIL U+%04X (%s): implausibly full (%u/%u px) -- looks like a fill-rule bug\n", + codepoint, label, filled, w * h); + failures++; + } + if (pixels[0] != 0) { + printf(" FAIL U+%04X (%s): top-left corner (background) is filled\n", codepoint, label); + failures++; + } + + free(pixels); +} + int main(int argc, char **argv) { FILE *fp; long size; @@ -211,6 +283,10 @@ int main(int argc, char **argv) { test_outlines(&font); + test_rasterize(&font, 0x0041, "'A'"); + test_rasterize(&font, 0x002E, "'.'"); + test_rasterize(&font, 0x0061, "'a'"); + free(buf); if (failures) {