diff --git a/FABRIC.md b/FABRIC.md index 394a594..6416a52 100644 --- a/FABRIC.md +++ b/FABRIC.md @@ -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. diff --git a/include/starkernel/ttf.h b/include/starkernel/ttf.h new file mode 100644 index 0000000..6657a28 --- /dev/null +++ b/include/starkernel/ttf.h @@ -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 +#include +#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 */ \ No newline at end of file diff --git a/src/starkernel/hal/ttf.c b/src/starkernel/hal/ttf.c new file mode 100644 index 0000000..ae066e2 --- /dev/null +++ b/src/starkernel/hal/ttf.c @@ -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; +} \ No newline at end of file diff --git a/tools/ttftest.c b/tools/ttftest.c new file mode 100644 index 0000000..844ba4c --- /dev/null +++ b/tools/ttftest.c @@ -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 + */ + +#include +#include +#include + +#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 \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; +} \ No newline at end of file