fix(text): carry a glyph's negative bearing and harden the bake's extents

This commit is contained in:
Martino Ferrari committed 2026-09-28 16:32:18 +02:00
1 parent b0f9fbbb35
commit d853db62ae
5 files changed
+470 -60

No files matched your search

+38 -14
View File
@@ -44,6 +44,18 @@ namespace plot::text {
/// face's own, so a proportional run advances per glyph while every rect stays
/// one size.
///
/// **No ink is trimmed away, in either direction.** The cell is as wide as the
/// widest advance, the widest ink *to the right* of the pen, **and** the deepest
/// ink to its left: an italic glyph reaches back over the pen that advances it
/// (half the italic faces on a stock Linux box do, `U+006A` being the usual
/// offender), so the cell sits a texel or two to the right of the pen and
/// `Glyph::xoff` is the matching non-positive bearing -- the pipeline places the
/// cell corner at `pen + xoff`, which is exactly what makes a negative bearing
/// expressible. Bounding ink on one side and clipping it on the other would be an
/// inconsistency, and a silent one: the bitmap `stb_truetype` hands over is
/// already trimmed to its ink, so a cell that could not hold it would drop the
/// columns it did not fit without any error to report.
///
/// **What is rejected, and how.** `from_file()` parses and bakes first and
/// installs the result only on success, so a failure leaves the object exactly as
/// it was -- a host can try a file, keep the font it already had, and read why
@@ -53,11 +65,17 @@ namespace plot::text {
/// is `bool` plus out-params, the same one every other fallible `plot` call uses,
/// and no `Result<T>` is introduced for a font.
///
/// **Trust.** stb_truetype's own header says it does no range checking of the
/// offsets in a font file, so a file crafted to be hostile can read memory. This
/// class parses the host's fonts, not untrusted input: a *random* file is
/// rejected, a *malicious* one is out of scope, and the same is true of
/// everything else in `third_party/`.
/// **Trust, and what this class does not check.** The file is assumed to be well
/// formed. What *is* checked here, before the vendored parser sees a byte, is that
/// the input is an sfnt (or a collection) whose table directory fits inside the
/// bytes read -- because that parser's own table scan reads `numTables` records
/// whatever the file's length is (its header says so in capitals, and warns
/// against untrusted files). Nothing *inside* those tables is validated by us:
/// stb_truetype does no range checking of the offsets it finds there, so a font
/// crafted to be hostile can still read memory, and a font this class accepts is
/// not a font that has been vetted. Untrusted fonts are out of scope: bake fonts
/// you or the user trust, and treat a font from the network the way you would
/// treat any other parser input from it.
///
/// **Where it lives.** `plot_core` does not provide this class: it is built by
/// the optional `plot_font_ttf` target, and this header is deliberately absent
@@ -90,10 +108,12 @@ class TrueTypeFont final : public render::TextBackend {
/// another file.
///
/// False with `*st` filled in (and `*out` untouched) for a path that is not a
/// readable regular file, a file the parser cannot accept, a non-positive or
/// non-finite `px`, and a bake in which no requested codepoint produced ink --
/// the last is the `tools/fonts/gen_atlas.py` rule, where a blank cell that
/// nobody notices is exactly the failure this feature exists to remove.
/// readable regular file, a file the parser cannot accept, a non-positive,
/// non-finite or absurdly large `px` (an em beyond a million pixels is refused
/// as out of range, like a coverage set whose atlas would not fit -- both before
/// anything is allocated), and a bake in which no requested codepoint produced
/// ink: a bake of nothing but blank cells and the fallback box is refused rather
/// than shipped, the rule `tools/fonts/gen_atlas.py` applies at authoring time.
/// `*out` must not be null. It allocates, so a failure to allocate is reported
/// as `ErrorCode::OutOfMemory` rather than thrown.
static bool from_file(const char* utf8_path, f32 px, TrueTypeFont* out, Status* st = nullptr);
@@ -140,11 +160,15 @@ class TrueTypeFont final : public render::TextBackend {
f32 em_px() const override;
/// The glyph for `codepoint`: its cell as a normalised rect (the same rect for
/// every glyph -- the fixed-cell contract), its bearing into the cell -- zero,
/// because the pen starts at the cell's left edge and the ink is placed inside
/// it -- and the face's advance. Never null once a face is loaded: a codepoint
/// outside the coverage maps to the fallback box, so a missing glyph is visible
/// rather than blank. Null for a backend with no face.
/// every glyph -- the fixed-cell contract), its bearing, and the face's advance.
/// The bearing is `xoff = -shift`, zero for a face whose ink stays right of
/// every pen, and negative when some covered glyph reaches back over it -- the
/// cell corner goes at `pen + xoff`, which is what keeps that overhang inside
/// the cell instead of clipped (`yoff` is always zero: the baseline is
/// `ascent()` below the cell's top, and no glyph's ink reaches above it
/// unaccounted for). Never null once a face is loaded: a codepoint outside the
/// coverage maps to the fallback box, so a missing glyph is visible rather than
/// blank. Null for a backend with no face.
const render::Glyph* glyph(u32 codepoint) const override;
/// `(cell_width() / atlas_width(), 1)` -- the atlas is a single row of cells.