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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2ef8d26c6f
commit
095860251a
@@ -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)
|
||||
* ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user