Files
LithosAnanake/tools/ttftest.c
T
Robert Allan JamesandClaude Sonnet 5 095860251a ttf.c: rasterization -- Bezier flattening + even-odd scanline fill
Punch list §25 item 4.3.7c complete. ttf_rasterize_glyph() flattens
quadratic-Bezier contours (fixed 8-segment subdivision, matching
CIRCLE/ELLIPSE's fixed-segment precedent) and fills them into a
caller-supplied bitmap via even-odd scanline fill, no AA. A local
signed Q48.16 multiply (q48_smul) handles negative outline coordinates,
since the shared q48_mul/q48_div are unsigned-only.

Verified two ways: tools/ttftest.c's ASCII-art dump + structural checks
for 'A'/'.'/'a', all recognizable and passing; and a live amd64
screendump via a throwaway TTF-PROBE word (loaded the font capsule,
rasterized 'A', blit via fb_put_pixel), showing a clearly legible 'A'
on the CANVAS -- probe reverted immediately after capture, only the
permanent ttf.c/ttf.h rasterizer remains. Compile-checked clean on all
three architectures (hal/*.c wildcard); this item's own acceptance is
the amd64 screendump, not a three-arch boot (that's 4.3.7f).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 22:24:47 -04:00

298 lines
10 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
/**
* ttftest.c — Host-side unit test for src/starkernel/hal/ttf.c (FABRIC.md
* item 4.3.7). No QEMU needed, same pattern as tools/README.md's fbtest.c
* entry (that file is itself missing from tools/ — stale doc, reported
* separately, not fixed here).
*
* Expected values below were independently computed by a from-scratch
* Python/struct reference reader (not this codebase's parser), against
* JetBrainsMono-Regular.ttf.
*
* Usage: ./ttftest <path-to-ttf>
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../include/starkernel/ttf.h"
typedef struct {
uint32_t codepoint;
uint32_t expect_glyph;
int16_t expect_contours;
int16_t expect_xmin, expect_ymin, expect_xmax, expect_ymax;
uint32_t expect_glyf_offset;
uint32_t expect_glyf_length;
} expect_t;
static const expect_t EXPECTED[] = {
/* cp glyph nc xmin ymin xmax ymax off len */
{0x0041, 1, 2, 50, 0, 550, 730, 35536, 112}, /* 'A' */
{0x0061, 198, 2, 67, -10, 508, 560, 46968, 244}, /* 'a' */
{0x0030, 737, 3, 80, -10, 520, 740, 92152, 184}, /* '0' */
{0x002E, 908, 1, 218, -10, 382, 152, 110044, 72}, /* '.' */
{0x0021, 913, 2, 225, -5, 375, 730, 110632, 108}, /* '!' */
{0x0040, 1151, 2, 45, -180, 560, 740, 144400, 224}, /* '@' */
{0x0020, 821, 0, 0, 0, 0, 0, 99448, 0}, /* space, empty glyph */
};
static int failures = 0;
static void check_eq_i(const char *what, uint32_t cp, long got, long want) {
if (got != want) {
printf(" FAIL U+%04X %s: got %ld want %ld\n", cp, what, got, want);
failures++;
}
}
/* Outline expectations (4.3.7a), independently computed by a from-scratch
* Python outline decoder (tools/../../tmp .../ttf_outline_ref.py, not
* committed, reproducible from FABRIC.md's 4.3.7a completion note) that
* shares no code with ttf.c. '.' and '0' are simple glyphs; 'A' is simple
* with two contours; 'a' is a translation-only composite (2 components). */
typedef struct {
int32_t x, y;
int on_curve;
} exp_point_t;
typedef struct {
uint32_t codepoint;
uint32_t glyph;
const uint16_t *contour_ends;
uint32_t contour_count;
const exp_point_t *points;
uint32_t point_count;
} outline_expect_t;
static const uint16_t DOT_ENDS[] = {11};
static const exp_point_t DOT_PTS[] = {
{300, -10, 1}, {263, -10, 0}, {218, 34, 0}, {218, 69, 1}, {218, 106, 0}, {263, 152, 0},
{300, 152, 1}, {337, 152, 0}, {382, 106, 0}, {382, 69, 1}, {382, 34, 0}, {337, -10, 0},
};
static const uint16_t A_ENDS[] = {7, 16};
static const exp_point_t A_PTS[] = {
{50, 0, 1}, {240, 730, 1}, {361, 730, 1}, {550, 0, 1}, {459, 0, 1}, {411, 194, 1},
{190, 194, 1}, {142, 0, 1}, {208, 270, 1}, {392, 270, 1}, {336, 495, 1}, {320, 559, 0},
{302, 645, 0}, {300, 658, 1}, {298, 645, 0}, {280, 559, 0}, {264, 496, 1},
};
static const uint16_t ALOWER_ENDS[] = {27, 38, 42};
static const exp_point_t ALOWER_PTS[] = {
{252, -10, 1}, {167, -10, 0}, {67, 85, 0}, {67, 162, 1}, {67, 213, 0}, {113, 289, 0},
{195, 332, 0}, {248, 332, 1}, {418, 332, 1}, {418, 375, 1}, {418, 482, 0}, {301, 482, 1},
{249, 482, 0}, {185, 444, 0}, {183, 410, 1}, {93, 410, 1}, {98, 475, 0}, {209, 560, 0},
{301, 560, 1}, {401, 560, 0}, {508, 464, 0}, {508, 378, 1}, {508, 0, 1}, {419, 0, 1},
{419, 100, 1}, {417, 100, 1}, {409, 49, 0}, {323, -10, 0}, {274, 66, 1}, {340, 66, 0},
{418, 130, 0}, {418, 185, 1}, {418, 262, 1}, {258, 262, 1}, {214, 262, 0}, {159, 209, 0},
{159, 165, 1}, {159, 119, 0}, {220, 66, 0}, {247, 645, 1}, {353, 785, 1}, {450, 785, 1},
{339, 645, 1},
};
static const outline_expect_t OUTLINE_EXPECTED[] = {
{0x002E, 908, DOT_ENDS, 1, DOT_PTS, 12},
{0x0041, 1, A_ENDS, 2, A_PTS, 17},
{0x0061, 199, ALOWER_ENDS, 3, ALOWER_PTS, 43},
};
static int32_t unscale(q48_16_t q) {
return (int32_t) (((int64_t) q) >> 16);
}
static void test_outlines(const ttf_font_t *font) {
ttf_point_t point_buf[600];
uint16_t contour_buf[16];
size_t i;
for (i = 0; i < sizeof(OUTLINE_EXPECTED) / sizeof(OUTLINE_EXPECTED[0]); i++) {
const outline_expect_t *e = &OUTLINE_EXPECTED[i];
ttf_outline_t outline;
int rc;
uint32_t j;
outline.points = point_buf;
outline.max_points = sizeof(point_buf) / sizeof(point_buf[0]);
outline.contour_ends = contour_buf;
outline.max_contours = sizeof(contour_buf) / sizeof(contour_buf[0]);
rc = ttf_glyph_outline(font, e->glyph, &outline);
if (rc != TTF_OK) {
printf(" FAIL U+%04X ttf_glyph_outline rc=%d\n", e->codepoint, rc);
failures++;
continue;
}
printf("U+%04X outline: points=%u (want %u) contours=%u (want %u)\n",
e->codepoint, outline.point_count, e->point_count,
outline.contour_count, e->contour_count);
check_eq_i("point_count", e->codepoint, outline.point_count, e->point_count);
check_eq_i("contour_count", e->codepoint, outline.contour_count, e->contour_count);
for (j = 0; j < e->contour_count && j < outline.contour_count; j++) {
check_eq_i("contour_end", e->codepoint, outline.contour_ends[j], e->contour_ends[j]);
}
for (j = 0; j < e->point_count && j < outline.point_count; j++) {
check_eq_i("point.x", e->codepoint, unscale(outline.points[j].x), e->points[j].x);
check_eq_i("point.y", e->codepoint, unscale(outline.points[j].y), e->points[j].y);
check_eq_i("point.on_curve", e->codepoint, outline.points[j].on_curve, e->points[j].on_curve);
}
}
}
/* 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;
uint8_t *buf;
ttf_font_t font;
int rc;
size_t i;
if (argc < 2) {
fprintf(stderr, "usage: %s <path-to-ttf>\n", argv[0]);
return 2;
}
fp = fopen(argv[1], "rb");
if (!fp) {
perror("fopen");
return 2;
}
fseek(fp, 0, SEEK_END);
size = ftell(fp);
fseek(fp, 0, SEEK_SET);
buf = malloc((size_t) size);
if (!buf || fread(buf, 1, (size_t) size, fp) != (size_t) size) {
fprintf(stderr, "read failed\n");
return 2;
}
fclose(fp);
rc = ttf_parse(buf, (uint32_t) size, &font);
if (rc != TTF_OK) {
printf("ttf_parse failed: rc=%d\n", rc);
return 1;
}
printf("parsed: unitsPerEm=%u numGlyphs=%u indexToLocFormat=%d has_cmap=%d\n",
font.units_per_em, font.num_glyphs, font.index_to_loc_format, font.has_cmap);
for (i = 0; i < sizeof(EXPECTED) / sizeof(EXPECTED[0]); i++) {
const expect_t *e = &EXPECTED[i];
uint32_t gid = ttf_codepoint_to_glyph(&font, e->codepoint);
ttf_glyph_header_t hdr;
int hrc;
printf("U+%04X -> glyph %u (want %u)\n", e->codepoint, gid, e->expect_glyph);
check_eq_i("glyph", e->codepoint, gid, e->expect_glyph);
hrc = ttf_glyph_header(&font, gid, &hdr);
if (hrc != TTF_OK) {
printf(" FAIL U+%04X ttf_glyph_header rc=%d\n", e->codepoint, hrc);
failures++;
continue;
}
check_eq_i("num_contours", e->codepoint, hdr.num_contours, e->expect_contours);
check_eq_i("x_min", e->codepoint, hdr.x_min, e->expect_xmin);
check_eq_i("y_min", e->codepoint, hdr.y_min, e->expect_ymin);
check_eq_i("x_max", e->codepoint, hdr.x_max, e->expect_xmax);
check_eq_i("y_max", e->codepoint, hdr.y_max, e->expect_ymax);
check_eq_i("glyf_offset", e->codepoint, hdr.glyf_offset, e->expect_glyf_offset);
check_eq_i("glyf_length", e->codepoint, hdr.glyf_length, e->expect_glyf_length);
}
test_outlines(&font);
test_rasterize(&font, 0x0041, "'A'");
test_rasterize(&font, 0x002E, "'.'");
test_rasterize(&font, 0x0061, "'a'");
free(buf);
if (failures) {
printf("%d check(s) FAILED\n", failures);
return 1;
}
printf("all checks passed\n");
return 0;
}