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
|
> before scoping REPL wiring (M8) — 4.3.7 (TrueType, adjunct) is the scoped-but-not-started
|
||||||
> next work, not M8.
|
> 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`/
|
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
|
`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
|
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
|
`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.
|
against values independently read from the same font with a reference tool (e.g.
|
||||||
`fonttools`/`ttx`), not just "doesn't crash." *Refs:* §27.7.
|
`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`
|
- [ ] **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
|
— 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
|
(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).
|
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.
|
*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