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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2db4603d04
commit
715ebcc57e
@@ -131,6 +131,24 @@ int ttf_parse(const uint8_t *data, uint32_t size, ttf_font_t *out) {
|
||||
if (rc != TTF_OK) return rc;
|
||||
if (!in_bounds(out, out->glyf_off, out->glyf_len)) return TTF_ERR_OUT_OF_BOUNDS;
|
||||
|
||||
/* hhea (for numberOfHMetrics) + hmtx (advance widths) -- mandatory,
|
||||
* needed for ttf_glyph_advance_width() (4.3.7e's proportional
|
||||
* spacing). numberOfHMetrics is hhea's 18th field, a fixed 36-byte
|
||||
* table per spec. */
|
||||
{
|
||||
uint32_t hhea_off, hhea_len_unused, hmtx_len;
|
||||
rc = find_table(data, size, num_tables, TAG('h', 'h', 'e', 'a'), &hhea_off, &hhea_len_unused);
|
||||
if (rc != TTF_OK) return rc;
|
||||
if (!in_bounds(out, hhea_off, 36)) return TTF_ERR_BAD_TABLE;
|
||||
out->num_h_metrics = rd_u16(data, hhea_off + 34);
|
||||
|
||||
rc = find_table(data, size, num_tables, TAG('h', 'm', 't', 'x'), &out->hmtx_off, &hmtx_len);
|
||||
if (rc != TTF_OK) return rc;
|
||||
if (out->num_h_metrics == 0
|
||||
|| !in_bounds(out, out->hmtx_off, (uint32_t) out->num_h_metrics * 4))
|
||||
return TTF_ERR_BAD_TABLE;
|
||||
}
|
||||
|
||||
/* cmap is optional to overall parse success; codepoint lookup just
|
||||
* fails cleanly (TTF_GLYPH_MISSING) if it's absent or unrecognized. */
|
||||
{
|
||||
@@ -262,6 +280,16 @@ int ttf_glyph_header(const ttf_font_t *font, uint32_t glyph_index, ttf_glyph_hea
|
||||
return TTF_OK;
|
||||
}
|
||||
|
||||
uint16_t ttf_glyph_advance_width(const ttf_font_t *font, uint32_t glyph_index) {
|
||||
uint32_t idx, off;
|
||||
|
||||
if (!font || font->num_h_metrics == 0) return 0;
|
||||
idx = glyph_index < font->num_h_metrics ? glyph_index : (uint32_t) font->num_h_metrics - 1;
|
||||
off = font->hmtx_off + idx * 4;
|
||||
if (!in_bounds(font, off, 2)) return 0;
|
||||
return rd_u16(font->data, off);
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
* 4.3.7a — glyph outline extraction
|
||||
* ===========================================================================
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
#include "word_source/include/defer_words.h"
|
||||
#include "word_source/include/framebuffer_words.h"
|
||||
#include "word_source/include/keyboard_words.h"
|
||||
#include "word_source/include/ttf_words.h"
|
||||
|
||||
/**
|
||||
* @brief Registers a single FORTH word in the virtual machine
|
||||
@@ -132,6 +133,7 @@ void register_forth79_words(VM *vm) {
|
||||
register_defer_words(vm); /* Module 27: DEFER / IS late binding */
|
||||
register_framebuffer_words(vm); /* Module 28: Console fabric -- raw framebuffer primitives */
|
||||
register_keyboard_words(vm); /* Module 29: Console fabric -- raw keyboard scancode diagnostic */
|
||||
register_ttf_words(vm); /* Module 30: TrueType text entry point */
|
||||
|
||||
log_message(LOG_INFO, "FORTH-79 Standard word set registration complete");
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0
|
||||
*/
|
||||
|
||||
#ifndef TTF_WORDS_H
|
||||
#define TTF_WORDS_H
|
||||
|
||||
#include "vm.h"
|
||||
|
||||
/**
|
||||
* @defgroup ttf_words TrueType Text Words
|
||||
* @{
|
||||
*
|
||||
* @brief `TTF-TEXT`, the TrueType-backed text entry point (FABRIC.md item
|
||||
* 4.3.7e). Kernel-only; no-op on hosted builds (no framebuffer/capsule
|
||||
* system exists there).
|
||||
*
|
||||
* @par TTF-TEXT ( c-addr u x y size color -- )
|
||||
* Render a UTF-8 string via the v1 default font capsule
|
||||
* (`fonts:JetBrainsMono-Regular.ttf`, loaded and cached lazily on first
|
||||
* use). (x,y) is raster pixel space (top-left origin, Y-down) -- NOT
|
||||
* capsules/fabric.4th's Cartesian Y-up convention used by the stroke
|
||||
* font's `TEXT` (4.3.6f); the two are deliberately different coordinate
|
||||
* systems, not interchangeable. (x,y) is the first glyph's baseline-left
|
||||
* origin. `size` is capped at `TTF_CACHE_MAX_SIZE_PX` (see ttf.h).
|
||||
* Advances proportionally per glyph via the font's real hmtx advance
|
||||
* width, scaled to pixels.
|
||||
* @}
|
||||
*/
|
||||
|
||||
void register_ttf_words(VM *vm);
|
||||
|
||||
#endif /* TTF_WORDS_H */
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
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_words.c — TrueType text entry point (FABRIC.md item 4.3.7e).
|
||||
* Kernel-only; no-op on hosted builds. */
|
||||
|
||||
#include "include/ttf_words.h"
|
||||
#include "../../include/log.h"
|
||||
#include "../../include/word_registry.h"
|
||||
|
||||
#ifdef __STARKERNEL__
|
||||
#include "starkernel/framebuffer.h"
|
||||
#include "starkernel/ttf.h"
|
||||
#include "starkernel/capsule_generated.h"
|
||||
|
||||
#define TTF_WORDS_CACHE_SLOTS 32
|
||||
#define TTF_WORDS_DEFAULT_FONT "fonts:JetBrainsMono-Regular.ttf"
|
||||
|
||||
static ttf_font_t g_ttf_font;
|
||||
static int g_ttf_font_ready = 0;
|
||||
static ttf_raster_cache_slot_t g_ttf_cache_slots[TTF_WORDS_CACHE_SLOTS];
|
||||
static ttf_raster_cache_t g_ttf_cache;
|
||||
|
||||
static int ttf_words_ensure_font(void) {
|
||||
if (g_ttf_font_ready) return 1;
|
||||
if (ttf_load_from_capsule(
|
||||
capsule_get_directory(), capsule_get_descriptors(),
|
||||
capsule_get_names(), capsule_get_arena(),
|
||||
TTF_WORDS_DEFAULT_FONT, &g_ttf_font) != TTF_OK)
|
||||
return 0;
|
||||
ttf_raster_cache_init(&g_ttf_cache, g_ttf_cache_slots, TTF_WORDS_CACHE_SLOTS);
|
||||
g_ttf_font_ready = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Decode one UTF-8 codepoint from VM memory at *a (VM-space address),
|
||||
* with *len bytes known remaining. Mirrors capsules/fabric.4th's
|
||||
* DECODE-UTF8 exactly: same lead-byte-length table, same U+FFFD
|
||||
* fallback (advancing exactly one byte) on an invalid or truncated
|
||||
* sequence. Advances *a and *len past the consumed bytes. */
|
||||
static uint32_t ttf_words_decode_utf8(VM *vm, vaddr_t *a, cell_t *len) {
|
||||
uint8_t lead, b[4];
|
||||
uint32_t seqlen, i, cp;
|
||||
|
||||
if (*len <= 0 || !vm_addr_ok(vm, *a, 1)) {
|
||||
*len = 0;
|
||||
return 0xFFFD;
|
||||
}
|
||||
lead = vm_load_u8(vm, *a);
|
||||
|
||||
if (lead < 0x80) seqlen = 1;
|
||||
else if ((lead & 0xE0) == 0xC0) seqlen = 2;
|
||||
else if ((lead & 0xF0) == 0xE0) seqlen = 3;
|
||||
else if ((lead & 0xF8) == 0xF0) seqlen = 4;
|
||||
else seqlen = 0;
|
||||
|
||||
if (seqlen == 0 || (cell_t) seqlen > *len || !vm_addr_ok(vm, *a, seqlen)) {
|
||||
*a += 1;
|
||||
*len -= 1;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
for (i = 0; i < seqlen; i++) b[i] = vm_load_u8(vm, *a + i);
|
||||
|
||||
switch (seqlen) {
|
||||
case 1:
|
||||
cp = b[0];
|
||||
break;
|
||||
case 2:
|
||||
cp = ((uint32_t) (b[0] & 0x1F) << 6) | (b[1] & 0x3F);
|
||||
break;
|
||||
case 3:
|
||||
cp = ((uint32_t) (b[0] & 0x0F) << 12) | ((uint32_t) (b[1] & 0x3F) << 6) | (b[2] & 0x3F);
|
||||
break;
|
||||
default:
|
||||
cp = ((uint32_t) (b[0] & 0x07) << 18) | ((uint32_t) (b[1] & 0x3F) << 12)
|
||||
| ((uint32_t) (b[2] & 0x3F) << 6) | (b[3] & 0x3F);
|
||||
break;
|
||||
}
|
||||
|
||||
*a += seqlen;
|
||||
*len -= (cell_t) seqlen;
|
||||
return cp;
|
||||
}
|
||||
|
||||
/* TTF-TEXT ( c-addr u x y size color -- ) */
|
||||
static void ttf_word_text(VM *vm) {
|
||||
cell_t color, size, y, x, u, addr_cell;
|
||||
vaddr_t a;
|
||||
cell_t remaining;
|
||||
uint32_t pen_x;
|
||||
q48_16_t scale;
|
||||
|
||||
if (vm->dsp < 5) {
|
||||
log_message(LOG_ERROR, "TTF-TEXT: Stack underflow");
|
||||
vm->error = 1;
|
||||
return;
|
||||
}
|
||||
color = vm_pop(vm);
|
||||
size = vm_pop(vm);
|
||||
y = vm_pop(vm);
|
||||
x = vm_pop(vm);
|
||||
u = vm_pop(vm);
|
||||
addr_cell = vm_pop(vm);
|
||||
|
||||
if (!ttf_words_ensure_font()) {
|
||||
log_message(LOG_ERROR, "TTF-TEXT: font capsule load failed");
|
||||
vm->error = 1;
|
||||
return;
|
||||
}
|
||||
if (size <= 0 || size > TTF_CACHE_MAX_SIZE_PX) {
|
||||
log_message(LOG_ERROR, "TTF-TEXT: size out of range");
|
||||
vm->error = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
a = VM_ADDR(addr_cell);
|
||||
remaining = u;
|
||||
pen_x = (uint32_t) x;
|
||||
scale = q48_div(q48_from_u64((uint32_t) size), q48_from_u64(g_ttf_font.units_per_em));
|
||||
|
||||
while (remaining > 0) {
|
||||
uint32_t cp = ttf_words_decode_utf8(vm, &a, &remaining);
|
||||
ttf_bitmap_t bmp;
|
||||
int rc, was_hit;
|
||||
uint32_t gid, adv_units, px, py;
|
||||
q48_16_t adv_q;
|
||||
|
||||
rc = ttf_raster_cache_get(&g_ttf_cache, &g_ttf_font, cp, (uint32_t) size, &bmp, &was_hit);
|
||||
if (rc == TTF_OK) {
|
||||
for (py = 0; py < bmp.height; py++) {
|
||||
for (px = 0; px < bmp.width; px++) {
|
||||
if (bmp.pixels[py * bmp.width + px]) {
|
||||
fb_put_pixel(pen_x + px - TTF_CACHE_MARGIN,
|
||||
(uint32_t) y + py - (uint32_t) size - TTF_CACHE_MARGIN,
|
||||
(uint32_t) color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gid = ttf_codepoint_to_glyph(&g_ttf_font, cp);
|
||||
adv_units = ttf_glyph_advance_width(&g_ttf_font, gid);
|
||||
adv_q = q48_mul(q48_from_u64(adv_units), scale);
|
||||
pen_x += (uint32_t) q48_to_u64(adv_q);
|
||||
}
|
||||
}
|
||||
#endif /* __STARKERNEL__ */
|
||||
|
||||
void register_ttf_words(VM *vm)
|
||||
{
|
||||
#ifdef __STARKERNEL__
|
||||
register_word(vm, "TTF-TEXT", ttf_word_text);
|
||||
#else
|
||||
(void) vm;
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user