ttf.c/ttf.h/ttftest.c: TrueType parser core -- sfnt/head/maxp/loca/glyf/cmap (item 4.3.7)
Freestanding C module resolving a Unicode codepoint (cmap format 4) to a glyph index and its outline header (contour count, bounding box), verified against an independent from-scratch Python reference reader via tools/ttftest.c. Not yet wired into the boot path or capsule system. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b9b7cb2c6a
commit
5f6cc054d4
@@ -4661,7 +4661,7 @@ document and committing that amendment as its own item.*
|
||||
> before scoping REPL wiring (M8) — 4.3.7 (TrueType, adjunct) is the scoped-but-not-started
|
||||
> next work, not M8.
|
||||
|
||||
- [ ] **4.3.7 — TTF parser core.** A C module (not FORTH — parsing and rasterization are
|
||||
- [x] **4.3.7 — TTF parser core.** A C module (not FORTH — parsing and rasterization are
|
||||
impractical to interpret) reading a `.ttf`'s `sfnt` directory plus `head`/`maxp`/`loca`/
|
||||
`glyf`/`cmap` tables, resolving a codepoint to a glyph index and its outline data. All
|
||||
scaled/derived values in Q48.16, not float — see §27.7. *Done when:* given an embedded test
|
||||
@@ -4669,6 +4669,48 @@ document and committing that amendment as its own item.*
|
||||
`glyf` table offset and reads its outline header (contour count, bounding box), verified
|
||||
against values independently read from the same font with a reference tool (e.g.
|
||||
`fonttools`/`ttx`), not just "doesn't crash." *Refs:* §27.7.
|
||||
> **Done 2026-08-10.** `include/starkernel/ttf.h` + `src/starkernel/hal/ttf.c`: sfnt
|
||||
> directory walk, `head` (unitsPerEm, indexToLocFormat)/`maxp` (numGlyphs)/`loca`/`glyf`
|
||||
> table location, and a format-4 `cmap` subtable selector + lookup (format 12 explicitly
|
||||
> deferred — not needed for the BMP-only v1 glyph repertoire). Every multi-byte read is
|
||||
> manually big-endian-decoded with a bounds check against the buffer length first — no
|
||||
> libc byteswap dependency, matches the freestanding/no-libc build (`-ffreestanding
|
||||
> -nostdlib -fno-builtin`, confirmed by direct single-file compile against the real
|
||||
> `Makefile.starkernel` amd64 `COMMON_CFLAGS`/`ARCH_CFLAGS`, zero warnings under
|
||||
> `-Wall -Werror -Wextra`). No Q48.16 conversion needed here — every field this item reads
|
||||
> is a raw on-disk integer, not a scaled/derived value; Q48.16 becomes relevant starting
|
||||
> 4.3.7a (outline point extraction).
|
||||
>
|
||||
> Verification tool substitution, recorded plainly: `fonttools`/`ttx` is not installed in
|
||||
> this environment (no network install attempted). Verified instead against a from-scratch
|
||||
> second implementation — a plain `struct`-module Python script
|
||||
> (`/tmp/.../scratchpad/ttf_ref.py`, not committed, reproducible from this note) that
|
||||
> shares no code with `ttf.c` — arguably a stronger independence guarantee than a
|
||||
> shared-library-backed tool would have given, though not what the item text named. A host
|
||||
> test harness, `tools/ttftest.c` (new, follows `tools/README.md`'s already-established
|
||||
> "host test, no QEMU needed" pattern — note `fbtest.c`, that entry's other example, turns
|
||||
> out not to actually exist in `tools/`; stale-doc discrepancy, reported not fixed),
|
||||
> compiles `ttf.c` directly and checks 7 codepoints from `fonts/JetBrainsMono-Regular.ttf`
|
||||
> (`A a 0 . ! @` plus space, the empty-glyph case) against the Python reference's output —
|
||||
> glyph index, contour count, bounding box (including two negative-`yMin` cases, `a` and
|
||||
> `@`, which exercise signed-field decoding), `glyf` offset, and `glyf` length all match.
|
||||
> `gcc -std=c99 -Wall -Wextra -Werror`, zero warnings.
|
||||
>
|
||||
> `.ttf` bytes reach the parser via a plain host `fopen`/`fread` into a `malloc` buffer in
|
||||
> `ttftest.c`, not yet via a capsule (that's 4.3.7b, still open — see §27.7's 2026-08-10
|
||||
> note on the raw-blob-vs-hex/base64 design question). `ttf_parse()` itself is agnostic to
|
||||
> how its buffer arrived, so this doesn't gate 4.3.7's own "done when" clause.
|
||||
>
|
||||
> `fonts/JetBrainsMono-Regular.ttf` added (SIL OFL 1.1, confirmed by reading the font's own
|
||||
> embedded `name`-table license string directly, not assumed from the filename) as the v1
|
||||
> test/default font, resolving §27.7's licensing-check blocker — see `fonts/README.md`.
|
||||
>
|
||||
> Not a kernel-boot change (no FORTH words, no capsule/init.4th wiring, `ttf.c` isn't
|
||||
> called from anywhere in the boot path yet — it's picked up by the existing
|
||||
> `hal/*.c` wildcard in `Makefile.starkernel` but dead code until something calls it) — no
|
||||
> three-arch QEMU acceptance boot applies to this item; the amd64 single-file freestanding
|
||||
> compile check above is what stands in for it, per this item's own "done when" clause
|
||||
> (which never asked for a kernel boot).
|
||||
|
||||
- [ ] **4.3.7a — Glyph outline extraction.** Simple and composite glyph outlines from `glyf`
|
||||
— on-curve/off-curve point lists, quadratic Bézier control points, composite glyph
|
||||
@@ -5480,4 +5522,8 @@ reverted or deprecated by this work.
|
||||
(4.3.7c), hex vs. base64 encoding (4.3.7b), which `.ttf` file serves as the v1 test/default
|
||||
font (not chosen yet — needs a licensing check before any specific font is embedded).
|
||||
|
||||
**Update 2026-08-10 — v1 test/default font chosen:** `JetBrainsMono-Regular.ttf`, licensed
|
||||
SIL OFL 1.1 (permissive, embedding/redistribution allowed) — resolves the licensing-check
|
||||
blocker above.
|
||||
|
||||
*Refs:* §25.5 (punch list), GAP-B2.
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
This file is part of the StarForth project.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
You may obtain a copy of the License at:
|
||||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||||
|
||||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
express or implied, including but not limited to the warranties of
|
||||
merchantability, fitness for a particular purpose, and noninfringement.
|
||||
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* ttf.h - TrueType font parser core (Freestanding)
|
||||
*
|
||||
* FABRIC.md item 4.3.7. Reads a TTF's sfnt directory plus head/maxp/loca/
|
||||
* glyf/cmap tables, resolving a Unicode codepoint to a glyph index and its
|
||||
* outline header (contour count, bounding box). Does NOT extract outline
|
||||
* points or rasterize — that is 4.3.7a/4.3.7c. No floating point; all
|
||||
* fields read here are raw integers straight from the font's own
|
||||
* big-endian on-disk format (see FABRIC.md §27.7 decision #2 for why the
|
||||
* Q48.16-vs-float call was made, and why it doesn't bind this file, which
|
||||
* never scales anything).
|
||||
*
|
||||
* cmap: only format 4 (Windows/Unicode BMP) subtables are resolved. This
|
||||
* covers ASCII and all of the BMP, which is what the v1 glyph repertoire
|
||||
* (§27.6.4) needs. Format 12 (supplementary planes) is deferred — no
|
||||
* v1 glyph requires it.
|
||||
*/
|
||||
|
||||
#ifndef STARKERNEL_TTF_H
|
||||
#define STARKERNEL_TTF_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "q48_16.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define TTF_OK 0
|
||||
#define TTF_ERR_BAD_SFNT -1
|
||||
#define TTF_ERR_TABLE_MISSING -2
|
||||
#define TTF_ERR_BAD_TABLE -3
|
||||
#define TTF_ERR_BAD_GLYPH_INDEX -4
|
||||
#define TTF_ERR_OUT_OF_BOUNDS -5
|
||||
|
||||
/** Glyph index returned for "no mapping" by ttf_codepoint_to_glyph(). */
|
||||
#define TTF_GLYPH_MISSING 0
|
||||
|
||||
/**
|
||||
* Parsed font handle. Borrows the caller's buffer (does not copy or own
|
||||
* it) — the buffer must outlive the ttf_font_t.
|
||||
*/
|
||||
typedef struct {
|
||||
const uint8_t *data;
|
||||
uint32_t size;
|
||||
|
||||
uint32_t head_off;
|
||||
uint32_t maxp_off;
|
||||
uint32_t loca_off;
|
||||
uint32_t loca_len;
|
||||
uint32_t glyf_off;
|
||||
uint32_t glyf_len;
|
||||
|
||||
uint16_t units_per_em;
|
||||
int16_t index_to_loc_format; /* 0 = Offset16 (x2), 1 = Offset32 */
|
||||
uint16_t num_glyphs;
|
||||
|
||||
/* Selected cmap subtable (format 4 only, see file header comment). */
|
||||
uint32_t cmap_subtable_off;
|
||||
int has_cmap;
|
||||
} ttf_font_t;
|
||||
|
||||
/** Raw glyf record header, per §27.6/4.3.7's "done when" clause. */
|
||||
typedef struct {
|
||||
int16_t num_contours; /* >= 0 simple glyph, < 0 composite glyph */
|
||||
int16_t x_min;
|
||||
int16_t y_min;
|
||||
int16_t x_max;
|
||||
int16_t y_max;
|
||||
uint32_t glyf_offset; /* absolute file offset of this glyph's record */
|
||||
uint32_t glyf_length; /* bytes; 0 for an empty glyph (e.g. space) */
|
||||
} ttf_glyph_header_t;
|
||||
|
||||
/**
|
||||
* ttf_parse - Locate and validate the sfnt directory and the head/maxp/
|
||||
* loca/glyf tables (cmap is optional; ttf_codepoint_to_glyph() fails
|
||||
* cleanly if absent). Does not copy `data` — `out` borrows it.
|
||||
*
|
||||
* @param data Whole .ttf file contents
|
||||
* @param size Length of data in bytes
|
||||
* @param out Parsed handle to populate
|
||||
* @return TTF_OK, or a TTF_ERR_* code
|
||||
*/
|
||||
int ttf_parse(const uint8_t *data, uint32_t size, ttf_font_t *out);
|
||||
|
||||
/**
|
||||
* ttf_codepoint_to_glyph - Resolve a Unicode codepoint via the font's
|
||||
* format-4 cmap subtable.
|
||||
*
|
||||
* @return glyph index, or TTF_GLYPH_MISSING if unmapped or no cmap
|
||||
*/
|
||||
uint32_t ttf_codepoint_to_glyph(const ttf_font_t *font, uint32_t codepoint);
|
||||
|
||||
/**
|
||||
* ttf_glyph_header - Read a glyph's outline header (contour count,
|
||||
* bounding box) via loca + glyf. Does not extract contour points.
|
||||
*
|
||||
* @param glyph_index As returned by ttf_codepoint_to_glyph()
|
||||
* @param out Header to populate
|
||||
* @return TTF_OK, or a TTF_ERR_* code
|
||||
*/
|
||||
int ttf_glyph_header(const ttf_font_t *font, uint32_t glyph_index,
|
||||
ttf_glyph_header_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* STARKERNEL_TTF_H */
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* ttf.c — TrueType font parser core (FABRIC.md item 4.3.7)
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "ttf.h"
|
||||
|
||||
#define TAG(a, b, c, d) \
|
||||
(((uint32_t)(a) << 24) | ((uint32_t)(b) << 16) | ((uint32_t)(c) << 8) | (uint32_t)(d))
|
||||
|
||||
static int fits(uint32_t size, uint32_t off, uint32_t len) {
|
||||
if (off > size) return 0;
|
||||
if (len > size - off) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int in_bounds(const ttf_font_t *f, uint32_t off, uint32_t len) {
|
||||
return fits(f->size, off, len);
|
||||
}
|
||||
|
||||
static uint16_t rd_u16(const uint8_t *d, uint32_t off) {
|
||||
return (uint16_t)(((uint16_t) d[off] << 8) | (uint16_t) d[off + 1]);
|
||||
}
|
||||
|
||||
static int16_t rd_i16(const uint8_t *d, uint32_t off) {
|
||||
return (int16_t) rd_u16(d, off);
|
||||
}
|
||||
|
||||
static uint32_t rd_u32(const uint8_t *d, uint32_t off) {
|
||||
return ((uint32_t) d[off] << 24) | ((uint32_t) d[off + 1] << 16) |
|
||||
((uint32_t) d[off + 2] << 8) | (uint32_t) d[off + 3];
|
||||
}
|
||||
|
||||
/* Locate a table record in the sfnt directory. Returns TTF_OK and fills
|
||||
* tbl_off/tbl_len, or TTF_ERR_TABLE_MISSING. */
|
||||
static int find_table(const uint8_t *data, uint32_t size, uint16_t num_tables,
|
||||
uint32_t tag, uint32_t *tbl_off, uint32_t *tbl_len) {
|
||||
uint32_t i;
|
||||
for (i = 0; i < num_tables; i++) {
|
||||
uint32_t rec = 12 + i * 16;
|
||||
if (!fits(size, rec, 16))
|
||||
return TTF_ERR_OUT_OF_BOUNDS;
|
||||
{
|
||||
uint32_t rec_tag = rd_u32(data, rec);
|
||||
if (rec_tag == tag) {
|
||||
*tbl_off = rd_u32(data, rec + 8);
|
||||
*tbl_len = rd_u32(data, rec + 12);
|
||||
return TTF_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
return TTF_ERR_TABLE_MISSING;
|
||||
}
|
||||
|
||||
int ttf_parse(const uint8_t *data, uint32_t size, ttf_font_t *out) {
|
||||
uint32_t sfnt_version;
|
||||
uint16_t num_tables;
|
||||
uint32_t glyf_len_unused;
|
||||
int rc;
|
||||
|
||||
if (!data || !out || size < 12) return TTF_ERR_BAD_SFNT;
|
||||
|
||||
sfnt_version = rd_u32(data, 0);
|
||||
if (sfnt_version != 0x00010000UL && sfnt_version != TAG('t', 'r', 'u', 'e'))
|
||||
return TTF_ERR_BAD_SFNT; /* not TrueType outlines (e.g. 'OTTO' = CFF) */
|
||||
|
||||
num_tables = rd_u16(data, 4);
|
||||
if (!fits(size, 12, (uint32_t) num_tables * 16))
|
||||
return TTF_ERR_OUT_OF_BOUNDS;
|
||||
|
||||
out->data = data;
|
||||
out->size = size;
|
||||
out->has_cmap = 0;
|
||||
out->cmap_subtable_off = 0;
|
||||
|
||||
rc = find_table(data, size, num_tables, TAG('h', 'e', 'a', 'd'), &out->head_off, &glyf_len_unused);
|
||||
if (rc != TTF_OK) return rc;
|
||||
if (!in_bounds(out, out->head_off, 54)) return TTF_ERR_BAD_TABLE;
|
||||
|
||||
out->units_per_em = rd_u16(data, out->head_off + 18);
|
||||
out->index_to_loc_format = rd_i16(data, out->head_off + 50);
|
||||
if (out->index_to_loc_format != 0 && out->index_to_loc_format != 1)
|
||||
return TTF_ERR_BAD_TABLE;
|
||||
|
||||
rc = find_table(data, size, num_tables, TAG('m', 'a', 'x', 'p'), &out->maxp_off, &glyf_len_unused);
|
||||
if (rc != TTF_OK) return rc;
|
||||
if (!in_bounds(out, out->maxp_off, 6)) return TTF_ERR_BAD_TABLE;
|
||||
out->num_glyphs = rd_u16(data, out->maxp_off + 4);
|
||||
|
||||
rc = find_table(data, size, num_tables, TAG('l', 'o', 'c', 'a'), &out->loca_off, &out->loca_len);
|
||||
if (rc != TTF_OK) return rc;
|
||||
{
|
||||
uint32_t expect_entries = (uint32_t) out->num_glyphs + 1;
|
||||
uint32_t expect_bytes = out->index_to_loc_format == 0
|
||||
? expect_entries * 2
|
||||
: expect_entries * 4;
|
||||
if (out->loca_len < expect_bytes) return TTF_ERR_BAD_TABLE;
|
||||
if (!in_bounds(out, out->loca_off, expect_bytes)) return TTF_ERR_OUT_OF_BOUNDS;
|
||||
}
|
||||
|
||||
rc = find_table(data, size, num_tables, TAG('g', 'l', 'y', 'f'), &out->glyf_off, &out->glyf_len);
|
||||
if (rc != TTF_OK) return rc;
|
||||
if (!in_bounds(out, out->glyf_off, out->glyf_len)) return TTF_ERR_OUT_OF_BOUNDS;
|
||||
|
||||
/* cmap is optional to overall parse success; codepoint lookup just
|
||||
* fails cleanly (TTF_GLYPH_MISSING) if it's absent or unrecognized. */
|
||||
{
|
||||
uint32_t cmap_off, cmap_len_unused;
|
||||
rc = find_table(data, size, num_tables, TAG('c', 'm', 'a', 'p'), &cmap_off, &cmap_len_unused);
|
||||
if (rc == TTF_OK && in_bounds(out, cmap_off, 4)) {
|
||||
uint16_t cmap_num_tables = rd_u16(data, cmap_off + 2);
|
||||
uint32_t i;
|
||||
if (in_bounds(out, cmap_off + 4, (uint32_t) cmap_num_tables * 8)) {
|
||||
uint32_t best_off = 0;
|
||||
int best_score = -1;
|
||||
for (i = 0; i < cmap_num_tables; i++) {
|
||||
uint32_t rec = cmap_off + 4 + i * 8;
|
||||
uint16_t platform_id = rd_u16(data, rec);
|
||||
uint16_t encoding_id = rd_u16(data, rec + 2);
|
||||
uint32_t sub_off = cmap_off + rd_u32(data, rec + 4);
|
||||
int score = -1;
|
||||
|
||||
if (!in_bounds(out, sub_off, 2)) continue;
|
||||
if (rd_u16(data, sub_off) != 4) continue; /* format 4 only, see header */
|
||||
|
||||
if (platform_id == 3 && encoding_id == 1) score = 3; /* Windows BMP */
|
||||
else if (platform_id == 0) score = 2; /* Unicode */
|
||||
else if (platform_id == 3 && encoding_id == 0) score = 1; /* Windows symbol */
|
||||
|
||||
if (score > best_score) {
|
||||
best_score = score;
|
||||
best_off = sub_off;
|
||||
}
|
||||
}
|
||||
if (best_score >= 0) {
|
||||
out->cmap_subtable_off = best_off;
|
||||
out->has_cmap = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TTF_OK;
|
||||
}
|
||||
|
||||
uint32_t ttf_codepoint_to_glyph(const ttf_font_t *font, uint32_t codepoint) {
|
||||
uint32_t sub;
|
||||
uint16_t seg_count_x2, seg_count;
|
||||
uint32_t end_code_off, start_code_off, id_delta_off, id_range_off;
|
||||
uint32_t i;
|
||||
|
||||
if (!font || !font->has_cmap) return TTF_GLYPH_MISSING;
|
||||
if (codepoint > 0xFFFFUL) return TTF_GLYPH_MISSING; /* format 4 = BMP only */
|
||||
|
||||
sub = font->cmap_subtable_off;
|
||||
if (!in_bounds(font, sub, 14)) return TTF_GLYPH_MISSING;
|
||||
|
||||
seg_count_x2 = rd_u16(font->data, sub + 6);
|
||||
seg_count = (uint16_t) (seg_count_x2 / 2);
|
||||
end_code_off = sub + 14;
|
||||
start_code_off = end_code_off + seg_count_x2 + 2; /* +2 skips reservedPad */
|
||||
id_delta_off = start_code_off + seg_count_x2;
|
||||
id_range_off = id_delta_off + seg_count_x2;
|
||||
|
||||
if (!in_bounds(font, end_code_off, (uint32_t) seg_count_x2 * 4 + 2))
|
||||
return TTF_GLYPH_MISSING;
|
||||
|
||||
for (i = 0; i < seg_count; i++) {
|
||||
uint16_t end_code = rd_u16(font->data, end_code_off + i * 2);
|
||||
if (codepoint > end_code) continue;
|
||||
|
||||
{
|
||||
uint16_t start_code = rd_u16(font->data, start_code_off + i * 2);
|
||||
int16_t id_delta = rd_i16(font->data, id_delta_off + i * 2);
|
||||
uint16_t id_range_offset = rd_u16(font->data, id_range_off + i * 2);
|
||||
|
||||
if (codepoint < start_code) return TTF_GLYPH_MISSING;
|
||||
|
||||
if (id_range_offset == 0) {
|
||||
return (uint32_t) ((uint16_t) (codepoint + (uint32_t) (int32_t) id_delta));
|
||||
} else {
|
||||
uint32_t glyph_addr = id_range_off + i * 2 + id_range_offset +
|
||||
(uint32_t) (codepoint - start_code) * 2;
|
||||
uint16_t glyph_id;
|
||||
if (!in_bounds(font, glyph_addr, 2)) return TTF_GLYPH_MISSING;
|
||||
glyph_id = rd_u16(font->data, glyph_addr);
|
||||
if (glyph_id == 0) return TTF_GLYPH_MISSING;
|
||||
return (uint32_t) ((uint16_t) (glyph_id + (uint32_t) (int32_t) id_delta));
|
||||
}
|
||||
}
|
||||
}
|
||||
return TTF_GLYPH_MISSING;
|
||||
}
|
||||
|
||||
int ttf_glyph_header(const ttf_font_t *font, uint32_t glyph_index, ttf_glyph_header_t *out) {
|
||||
uint32_t off_a, off_b;
|
||||
|
||||
if (!font || !out) return TTF_ERR_BAD_TABLE;
|
||||
if (glyph_index >= font->num_glyphs) return TTF_ERR_BAD_GLYPH_INDEX;
|
||||
|
||||
if (font->index_to_loc_format == 0) {
|
||||
uint32_t e = font->loca_off + glyph_index * 2;
|
||||
if (!in_bounds(font, e, 4)) return TTF_ERR_OUT_OF_BOUNDS;
|
||||
off_a = (uint32_t) rd_u16(font->data, e) * 2;
|
||||
off_b = (uint32_t) rd_u16(font->data, e + 2) * 2;
|
||||
} else {
|
||||
uint32_t e = font->loca_off + glyph_index * 4;
|
||||
if (!in_bounds(font, e, 8)) return TTF_ERR_OUT_OF_BOUNDS;
|
||||
off_a = rd_u32(font->data, e);
|
||||
off_b = rd_u32(font->data, e + 4);
|
||||
}
|
||||
|
||||
if (off_b < off_a) return TTF_ERR_BAD_TABLE;
|
||||
|
||||
out->glyf_offset = font->glyf_off + off_a;
|
||||
out->glyf_length = off_b - off_a;
|
||||
|
||||
if (out->glyf_length == 0) {
|
||||
/* Empty glyph (e.g. space) — no glyf record at all. */
|
||||
out->num_contours = 0;
|
||||
out->x_min = out->y_min = out->x_max = out->y_max = 0;
|
||||
return TTF_OK;
|
||||
}
|
||||
|
||||
if (!in_bounds(font, out->glyf_offset, 10)) return TTF_ERR_OUT_OF_BOUNDS;
|
||||
|
||||
out->num_contours = rd_i16(font->data, out->glyf_offset);
|
||||
out->x_min = rd_i16(font->data, out->glyf_offset + 2);
|
||||
out->y_min = rd_i16(font->data, out->glyf_offset + 4);
|
||||
out->x_max = rd_i16(font->data, out->glyf_offset + 6);
|
||||
out->y_max = rd_i16(font->data, out->glyf_offset + 8);
|
||||
|
||||
return TTF_OK;
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 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++;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
free(buf);
|
||||
|
||||
if (failures) {
|
||||
printf("%d check(s) FAILED\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user