ttf.c: glyph outline extraction, simple and composite (item 4.3.7a)

ttf_glyph_outline() decodes simple-glyph flag/coordinate runs and
recursively resolves composite components into a caller-supplied point/
contour-end buffer, in Q48.16. Composite scale/rotation/skew transforms
are rejected with TTF_ERR_UNSUPPORTED rather than mis-rendered, since the
shared q48_mul/q48_div are unsigned-only; translation-only composites
(the only kind the v1 glyph repertoire uses) apply cleanly via q48_add.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-10 21:45:04 -04:00
co-authored by Claude Sonnet 5
parent 5f6cc054d4
commit 70b6918279
4 changed files with 471 additions and 6 deletions
+282 -5
View File
@@ -8,12 +8,28 @@
*/
/**
* ttf.c — TrueType font parser core (FABRIC.md item 4.3.7)
* ttf.c — TrueType font parser core (FABRIC.md items 4.3.7, 4.3.7a)
*
* sfnt directory + head/maxp/loca/glyf/cmap(format 4) table parsing. All
* multi-byte fields in a TTF are big-endian; this file reads them by hand
* (no libc byteswap dependency) with bounds checks against the buffer
* length on every access, since the buffer is untrusted input.
* sfnt directory + head/maxp/loca/glyf/cmap(format 4) table parsing, plus
* simple- and composite-glyph outline extraction. All multi-byte fields in
* a TTF are big-endian; this file reads them by hand (no libc byteswap
* dependency) with bounds checks against the buffer length on every
* access, since the buffer is untrusted input.
*
* Composite-glyph transforms: this file applies (dx,dy) TRANSLATION only.
* A composite component carrying a non-identity scale/rotation/skew (the
* WE_HAVE_A_SCALE/WE_HAVE_AN_X_AND_Y_SCALE/WE_HAVE_A_TWO_BY_TWO flags with
* a non-1.0/non-zero F2Dot14 value) is rejected with TTF_ERR_UNSUPPORTED
* rather than silently mis-rendered. Reason: applying such a transform
* needs a signed fixed-point multiply, and the shared q48_mul()/q48_div()
* in src/starkernel/math/q48_16.c are unsigned-only (q48_div saturates via
* an unsigned overflow check; q48_mul does a plain unsigned widen-multiply)
* — confirmed by reading that file, not assumed. Per this repo's rule
* against modifying a shared/tested module to "fix" it without being
* asked, that gap is reported (see FABRIC.md's 4.3.7a completion note),
* not patched here. No glyph in the v1 repertoire (§27.6.4) needs a
* non-identity composite transform — checked against
* fonts/JetBrainsMono-Regular.ttf before writing this, not assumed.
*/
#include "ttf.h"
@@ -244,4 +260,265 @@ int ttf_glyph_header(const ttf_font_t *font, uint32_t glyph_index, ttf_glyph_hea
out->y_max = rd_i16(font->data, out->glyf_offset + 8);
return TTF_OK;
}
/* ===========================================================================
* 4.3.7a — glyph outline extraction
* ===========================================================================
*/
static uint8_t rd_u8(const uint8_t *d, uint32_t off) {
return d[off];
}
static int8_t rd_i8(const uint8_t *d, uint32_t off) {
return (int8_t) d[off];
}
/* Left-shift a signed 32-bit font-unit coordinate into Q48.16. Two's-
* complement, deliberately not going through q48_16.h's q48_from_u64 (same
* operation, but that name implies an unsigned caller, which this isn't —
* see this file's header comment on why negative values are fine for
* shift/add/sub but not for q48_mul/q48_div). */
static q48_16_t q48_from_i32(int32_t v) {
return ((uint64_t) (int64_t) v) << 16;
}
#define TTF_FLAG_ON_CURVE 0x01
#define TTF_FLAG_X_SHORT 0x02
#define TTF_FLAG_Y_SHORT 0x04
#define TTF_FLAG_REPEAT 0x08
#define TTF_FLAG_X_SAME_OR_POS 0x10
#define TTF_FLAG_Y_SAME_OR_POS 0x20
/* Decode-time cap on points/contours per single simple-glyph component —
* a defensive stack-buffer bound, independent of the caller's out->
* max_points/max_contours (which are checked separately, on the combined
* outline). No glyph in the v1 repertoire (§27.6.4) comes close to this. */
#define TTF_MAX_SIMPLE_POINTS 512
static int decode_simple_glyph(const ttf_font_t *font, const ttf_glyph_header_t *hdr,
ttf_outline_t *out) {
uint32_t off = hdr->glyf_offset + 10;
uint16_t nc = (uint16_t) hdr->num_contours;
uint16_t end_pts[TTF_MAX_SIMPLE_POINTS]; /* reused below for endPtsOfContours only, nc <= that bound checked */
uint16_t num_points;
uint8_t flags[TTF_MAX_SIMPLE_POINTS];
int32_t xs[TTF_MAX_SIMPLE_POINTS];
int32_t ys[TTF_MAX_SIMPLE_POINTS];
uint32_t i;
uint32_t point_base;
int32_t acc;
if (nc == 0 || nc > TTF_MAX_SIMPLE_POINTS) return TTF_ERR_TOO_MANY_CONTOURS;
if (!in_bounds(font, off, (uint32_t) nc * 2 + 2)) return TTF_ERR_OUT_OF_BOUNDS;
for (i = 0; i < nc; i++) {
end_pts[i] = rd_u16(font->data, off + i * 2);
}
off += (uint32_t) nc * 2;
num_points = (uint16_t) (end_pts[nc - 1] + 1);
if (num_points == 0 || num_points > TTF_MAX_SIMPLE_POINTS) return TTF_ERR_TOO_MANY_POINTS;
{
uint16_t instruction_length;
if (!in_bounds(font, off, 2)) return TTF_ERR_OUT_OF_BOUNDS;
instruction_length = rd_u16(font->data, off);
off += 2;
if (!in_bounds(font, off, instruction_length)) return TTF_ERR_OUT_OF_BOUNDS;
off += instruction_length;
}
/* Flags, with run-length repeat expansion. */
i = 0;
while (i < num_points) {
uint8_t f;
if (!in_bounds(font, off, 1)) return TTF_ERR_OUT_OF_BOUNDS;
f = rd_u8(font->data, off);
off += 1;
flags[i++] = f;
if (f & TTF_FLAG_REPEAT) {
uint8_t repeat;
uint32_t r;
if (!in_bounds(font, off, 1)) return TTF_ERR_OUT_OF_BOUNDS;
repeat = rd_u8(font->data, off);
off += 1;
for (r = 0; r < repeat; r++) {
if (i >= num_points) return TTF_ERR_TOO_MANY_POINTS;
flags[i++] = f;
}
}
}
/* X coordinates, delta-decoded. */
acc = 0;
for (i = 0; i < num_points; i++) {
uint8_t f = flags[i];
int32_t dx;
if (f & TTF_FLAG_X_SHORT) {
uint8_t v;
if (!in_bounds(font, off, 1)) return TTF_ERR_OUT_OF_BOUNDS;
v = rd_u8(font->data, off);
off += 1;
dx = (f & TTF_FLAG_X_SAME_OR_POS) ? (int32_t) v : -(int32_t) v;
} else if (f & TTF_FLAG_X_SAME_OR_POS) {
dx = 0;
} else {
if (!in_bounds(font, off, 2)) return TTF_ERR_OUT_OF_BOUNDS;
dx = rd_i16(font->data, off);
off += 2;
}
acc += dx;
xs[i] = acc;
}
/* Y coordinates, delta-decoded. */
acc = 0;
for (i = 0; i < num_points; i++) {
uint8_t f = flags[i];
int32_t dy;
if (f & TTF_FLAG_Y_SHORT) {
uint8_t v;
if (!in_bounds(font, off, 1)) return TTF_ERR_OUT_OF_BOUNDS;
v = rd_u8(font->data, off);
off += 1;
dy = (f & TTF_FLAG_Y_SAME_OR_POS) ? (int32_t) v : -(int32_t) v;
} else if (f & TTF_FLAG_Y_SAME_OR_POS) {
dy = 0;
} else {
if (!in_bounds(font, off, 2)) return TTF_ERR_OUT_OF_BOUNDS;
dy = rd_i16(font->data, off);
off += 2;
}
acc += dy;
ys[i] = acc;
}
if (out->point_count + num_points > out->max_points) return TTF_ERR_TOO_MANY_POINTS;
if (out->contour_count + nc > out->max_contours) return TTF_ERR_TOO_MANY_CONTOURS;
point_base = out->point_count;
for (i = 0; i < num_points; i++) {
out->points[point_base + i].x = q48_from_i32(xs[i]);
out->points[point_base + i].y = q48_from_i32(ys[i]);
out->points[point_base + i].on_curve = (uint8_t) ((flags[i] & TTF_FLAG_ON_CURVE) != 0);
}
for (i = 0; i < nc; i++) {
out->contour_ends[out->contour_count + i] = (uint16_t) (point_base + end_pts[i]);
}
out->point_count += num_points;
out->contour_count += nc;
return TTF_OK;
}
#define TTF_COMP_ARGS_ARE_WORDS 0x0001
#define TTF_COMP_ARGS_ARE_XY_VALUES 0x0002
#define TTF_COMP_WE_HAVE_A_SCALE 0x0008
#define TTF_COMP_MORE_COMPONENTS 0x0020
#define TTF_COMP_WE_HAVE_XY_SCALE 0x0040
#define TTF_COMP_WE_HAVE_2X2 0x0080
#define TTF_F2DOT14_ONE 16384 /* 1.0 in F2Dot14 */
static int decode_glyph_r(const ttf_font_t *font, uint32_t glyph_index,
ttf_outline_t *out, int depth);
static int decode_composite_glyph(const ttf_font_t *font, const ttf_glyph_header_t *hdr,
ttf_outline_t *out, int depth) {
uint32_t off = hdr->glyf_offset + 10;
int more = 1;
while (more) {
uint16_t flags, comp_glyph;
int32_t arg1, arg2;
uint32_t point_base;
int rc;
if (!in_bounds(font, off, 4)) return TTF_ERR_OUT_OF_BOUNDS;
flags = rd_u16(font->data, off);
comp_glyph = rd_u16(font->data, off + 2);
off += 4;
if (!(flags & TTF_COMP_ARGS_ARE_XY_VALUES)) return TTF_ERR_UNSUPPORTED; /* point-matching, not implemented */
if (flags & TTF_COMP_ARGS_ARE_WORDS) {
if (!in_bounds(font, off, 4)) return TTF_ERR_OUT_OF_BOUNDS;
arg1 = rd_i16(font->data, off);
arg2 = rd_i16(font->data, off + 2);
off += 4;
} else {
if (!in_bounds(font, off, 2)) return TTF_ERR_OUT_OF_BOUNDS;
arg1 = rd_i8(font->data, off);
arg2 = rd_i8(font->data, off + 1);
off += 2;
}
if (flags & TTF_COMP_WE_HAVE_A_SCALE) {
int16_t s;
if (!in_bounds(font, off, 2)) return TTF_ERR_OUT_OF_BOUNDS;
s = rd_i16(font->data, off);
off += 2;
if (s != TTF_F2DOT14_ONE) return TTF_ERR_UNSUPPORTED;
} else if (flags & TTF_COMP_WE_HAVE_XY_SCALE) {
int16_t sx, sy;
if (!in_bounds(font, off, 4)) return TTF_ERR_OUT_OF_BOUNDS;
sx = rd_i16(font->data, off);
sy = rd_i16(font->data, off + 2);
off += 4;
if (sx != TTF_F2DOT14_ONE || sy != TTF_F2DOT14_ONE) return TTF_ERR_UNSUPPORTED;
} else if (flags & TTF_COMP_WE_HAVE_2X2) {
int16_t a, b, c, dd;
if (!in_bounds(font, off, 8)) return TTF_ERR_OUT_OF_BOUNDS;
a = rd_i16(font->data, off);
b = rd_i16(font->data, off + 2);
c = rd_i16(font->data, off + 4);
dd = rd_i16(font->data, off + 6);
off += 8;
if (a != TTF_F2DOT14_ONE || b != 0 || c != 0 || dd != TTF_F2DOT14_ONE)
return TTF_ERR_UNSUPPORTED;
}
point_base = out->point_count;
rc = decode_glyph_r(font, comp_glyph, out, depth + 1);
if (rc != TTF_OK) return rc;
{
q48_16_t dx = q48_from_i32(arg1);
q48_16_t dy = q48_from_i32(arg2);
uint32_t i;
for (i = point_base; i < out->point_count; i++) {
out->points[i].x = q48_add(out->points[i].x, dx);
out->points[i].y = q48_add(out->points[i].y, dy);
}
}
more = (flags & TTF_COMP_MORE_COMPONENTS) != 0;
}
return TTF_OK;
}
static int decode_glyph_r(const ttf_font_t *font, uint32_t glyph_index,
ttf_outline_t *out, int depth) {
ttf_glyph_header_t hdr;
int rc;
if (depth > TTF_MAX_COMPOSITE_DEPTH) return TTF_ERR_TOO_DEEP;
rc = ttf_glyph_header(font, glyph_index, &hdr);
if (rc != TTF_OK) return rc;
if (hdr.glyf_length == 0) return TTF_OK; /* empty glyph, e.g. space */
if (hdr.num_contours >= 0) return decode_simple_glyph(font, &hdr, out);
return decode_composite_glyph(font, &hdr, out, depth);
}
int ttf_glyph_outline(const ttf_font_t *font, uint32_t glyph_index, ttf_outline_t *out) {
if (!font || !out) return TTF_ERR_BAD_TABLE;
out->point_count = 0;
out->contour_count = 0;
return decode_glyph_r(font, glyph_index, out, 0);
}