Files
LithosAnanake/include/starkernel/ttf.h
T
Robert Allan JamesandClaude Sonnet 5 715ebcc57e ttf_words.c: TTF-TEXT entry point, hmtx advance widths (item 4.3.7e)
Punch list §25 item 4.3.7e complete. New src/word_source/ttf_words.c
registers TTF-TEXT ( c-addr u x y size color -- ): lazily loads the v1
default font capsule + raster cache once, decodes UTF-8 (C
reimplementation mirroring capsules/fabric.4th's DECODE-UTF8 exactly),
looks up each glyph's cached bitmap, blits via fb_put_pixel, advances
the pen by the glyph's real hmtx advance width scaled to pixels.

Necessary plumbing: ttf_parse() now also locates hhea/hmtx, and
ttf_glyph_advance_width() reads a glyph's advance width -- required for
this item's own "proportional spacing correct" acceptance clause, no
advance-width data existed anywhere else in the parser. Verified in
tools/ttftest.c: A/a/0/space all read advance_width=600, correctly
uniform since JetBrainsMono-Regular.ttf is monospace.

(x,y) is raster pixel space (top-left origin, Y-down), deliberately not
the stroke font TEXT's Cartesian Y-up convention -- recorded explicitly
in ttf_words.h, not conflated.

Verified live, amd64, screendump: injected
S" Hi 4.3.7e!" 200 200 28 16777215 TTF-TEXT over a serial socket after
boot, no error, captured a screendump showing the string rendered
legibly with correct mixed-case/digit/punctuation glyphs and even
spacing. TTF-TEXT is this item's permanent deliverable, not a
throwaway probe. Compile-checked clean on all three architectures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 23:11:40 -04:00

322 lines
13 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.
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
#define TTF_ERR_TOO_MANY_POINTS -6
#define TTF_ERR_TOO_MANY_CONTOURS -7
#define TTF_ERR_TOO_DEEP -8 /* composite glyph nesting exceeded TTF_MAX_COMPOSITE_DEPTH */
#define TTF_ERR_UNSUPPORTED -9 /* e.g. a non-identity composite transform or point-matched
* component args — see ttf.c's file header comment; not a
* malformed font, just a code path this parser doesn't
* implement yet */
/* Recursion guard for nested composite glyphs (a component referencing a
* component). The TTF spec doesn't hard-cap this; this is a defensive
* limit for freestanding/kernel-stack safety. */
#define TTF_MAX_COMPOSITE_DEPTH 8
/** 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;
/* hhea/hmtx, for ttf_glyph_advance_width() -- proportional spacing
* (FABRIC.md item 4.3.7e). Mandatory tables per the TrueType spec,
* so their absence fails ttf_parse() same as head/maxp/loca/glyf. */
uint32_t hmtx_off;
uint16_t num_h_metrics;
} 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);
/**
* ttf_glyph_advance_width - Horizontal advance width via hmtx, in raw
* font design units (NOT scaled by unitsPerEm -- caller's job, e.g.
* `q48_mul(q48_from_u64(width), scale)`; always non-negative, so the
* shared q48_mul is fine here unlike the signed cases documented in
* ttf.c's rasterizer). Glyphs past hmtx's numberOfHMetrics entries share
* the last entry's width, per spec.
*
* @param glyph_index As returned by ttf_codepoint_to_glyph()
* @return advance width, or 0 if glyph_index is out of range
*/
uint16_t ttf_glyph_advance_width(const ttf_font_t *font, uint32_t glyph_index);
/** One outline point, in raw font design units (NOT scaled by unitsPerEm —
* that's the caller's job, same convention as ttf_glyph_header_t's bbox),
* expressed in Q48.16. Two's-complement negative values are expected and
* correct for q48_add/q48_sub and for q48_from_u64-style left-shift
* conversion; this module never calls q48_mul/q48_div on outline
* coordinates (see ttf.c's file header comment for why). */
typedef struct {
q48_16_t x, y;
uint8_t on_curve;
} ttf_point_t;
/** Simple- and composite-glyph outline, flattened to one point list plus
* per-contour end indices (TrueType convention: contour_ends[c] is the
* index of the LAST point of contour c, inclusive; points are shared
* across contours only in the sense that contour c+1 starts right after
* contour_ends[c]). Caller supplies both backing arrays — this module
* never allocates. */
typedef struct {
ttf_point_t *points;
uint32_t max_points;
uint32_t point_count;
uint16_t *contour_ends;
uint32_t max_contours;
uint32_t contour_count;
} ttf_outline_t;
/**
* ttf_glyph_outline - Extract a glyph's outline (simple or composite,
* recursively resolving composite components) into caller-supplied
* buffers.
*
* Composite components with a non-identity transform (any scale/rotation/
* skew, i.e. anything but a pure (dx,dy) translation) or with
* point-matched (rather than xy-offset) placement args return
* TTF_ERR_UNSUPPORTED rather than silently producing a wrong outline —
* see ttf.c's file header comment for why, and check that limitation
* before relying on this for an arbitrary font.
*
* @return TTF_OK, or a TTF_ERR_* code (including TTF_ERR_TOO_MANY_POINTS/
* _CONTOURS if a caller buffer is too small, and TTF_ERR_TOO_DEEP
* if composite nesting exceeds TTF_MAX_COMPOSITE_DEPTH)
*/
int ttf_glyph_outline(const ttf_font_t *font, uint32_t glyph_index, ttf_outline_t *out);
/** A caller-owned 8-bit-per-pixel bitmap. `ttf_rasterize_glyph()` writes
* `fill_value` into covered pixels and leaves everything else untouched
* (it does not clear the buffer first — caller's job, so repeated
* rasterization into the same bitmap, e.g. for a text run, composites
* correctly without an extra clear between glyphs). */
typedef struct {
uint8_t *pixels;
uint32_t width;
uint32_t height;
} ttf_bitmap_t;
/**
* ttf_rasterize_glyph - Flatten a glyph's outline (quadratic Bezier
* contours, fixed segment count per curve — see ttf.c's file header
* comment) and fill it into `out` using the even-odd rule (FABRIC.md item
* 4.3.7c; see that item's completion note for why even-odd rather than
* nonzero winding — correct for the v1 glyph repertoire's non-self-
* intersecting nested contours, not necessarily for an arbitrary font).
* No antialiasing (explicitly deferred, per 4.3.7c's own "done when"
* clause). Never allocates — flattening uses a fixed-size local buffer
* bounded by TTF_RASTER_MAX_POINTS/TTF_RASTER_MAX_CONTOURS.
*
* @param scale Font-design-units-to-pixels scale, Q48.16, e.g.
* q48_div(q48_from_u64(size_px), q48_from_u64(font->units_per_em))
* @param origin_x Pixel-space X (Q48.16) of the glyph's (0,0) font origin
* within `out`
* @param origin_y Pixel-space Y (Q48.16) of the glyph's baseline (font
* y=0) within `out`; font Y-up is flipped to raster
* Y-down internally
* @return TTF_OK, or a TTF_ERR_* code (including TTF_ERR_TOO_MANY_POINTS/
* _CONTOURS if the flattened outline exceeds the local buffer)
*/
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);
/* Glyph raster cache (FABRIC.md item 4.3.7d). Fixed-size, caller-owned
* slot array -- no allocation, same convention as the rest of this
* module. Every cached bitmap is a fixed TTF_CACHE_BITMAP_DIM square,
* rasterized with the fixed origin (TTF_CACHE_MARGIN,
* size_px + TTF_CACHE_MARGIN) -- i.e. glyph (0,0)/baseline sits at that
* pixel within the bitmap on every cache entry, not just fitted to each
* glyph's own bounding box. Callers positioning text (4.3.7e) need to
* know this fixed convention. */
#define TTF_CACHE_MAX_SIZE_PX 64
#define TTF_CACHE_MARGIN 8
#define TTF_CACHE_BITMAP_DIM (TTF_CACHE_MAX_SIZE_PX + TTF_CACHE_MARGIN * 2)
#define TTF_CACHE_BITMAP_BYTES (TTF_CACHE_BITMAP_DIM * TTF_CACHE_BITMAP_DIM)
typedef struct {
int valid;
const ttf_font_t *font;
uint32_t codepoint;
uint32_t size_px;
uint32_t width, height;
uint32_t hits; /* incremented on every cache hit; 4.3.7d's own
* "measurable" verification reads this. */
uint8_t pixels[TTF_CACHE_BITMAP_BYTES];
} ttf_raster_cache_slot_t;
typedef struct {
ttf_raster_cache_slot_t *slots;
uint32_t slot_count;
uint32_t evict_next; /* round-robin index used once every slot is full */
} ttf_raster_cache_t;
/** ttf_raster_cache_init - Bind a caller-supplied slot array to `cache`
* and mark every slot empty. */
void ttf_raster_cache_init(ttf_raster_cache_t *cache, ttf_raster_cache_slot_t *slots,
uint32_t slot_count);
/**
* ttf_raster_cache_get - Look up (font, codepoint, size_px); on a miss,
* rasterize and insert (evicting round-robin if every slot is full).
* `out` borrows the winning slot's buffer directly -- valid until that
* slot is evicted by a later call.
*
* @param was_hit If non-NULL, set to 1 on a cache hit, 0 if this call
* rasterized and inserted
* @return TTF_OK, TTF_ERR_UNSUPPORTED if size_px > TTF_CACHE_MAX_SIZE_PX,
* or a ttf_rasterize_glyph() TTF_ERR_* code on a miss that failed
* to rasterize
*/
int ttf_raster_cache_get(ttf_raster_cache_t *cache, const ttf_font_t *font,
uint32_t codepoint, uint32_t size_px,
ttf_bitmap_t *out, int *was_hit);
#ifdef __STARKERNEL__
#include "capsule.h"
/**
* ttf_load_from_capsule - Resolve a font capsule by name and parse it,
* zero-copy (FABRIC.md item 4.3.7b). `out` borrows the capsule payload
* directly from `arena` — no kmalloc, no decode step, since the capsule
* is already a raw-byte match of the source `.ttf` (see
* capsules/fonts/README.md and FABRIC.md §27.7's 2026-08-10 correction:
* capsule storage needs no hex/base64 text-encoding, `tools/mkcapsule.c`
* already embeds arbitrary files as raw bytes). Validates the capsule's
* content hash (`capsule_validate(..., verify_hash=1)`) before parsing.
*
* @param capsule_name Colon-separated capsule name, e.g.
* "fonts:JetBrainsMono-Regular.ttf"
* @return TTF_OK, TTF_ERR_TABLE_MISSING if the capsule isn't found, or a
* capsule-validation/ttf_parse TTF_ERR_* code
*/
int ttf_load_from_capsule(
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena,
const char *capsule_name,
ttf_font_t *out);
#endif /* __STARKERNEL__ */
#ifdef __cplusplus
}
#endif
#endif /* STARKERNEL_TTF_H */