The repository had no licence at all, so every source was all-rights-reserved by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino Ferrari) and a two-line SPDX header to each of the 113 first-party sources -- 48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the grant travels with each file rather than relying on the root document alone. Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely vendored code is untouched: third_party/stb_truetype.h carries no header of ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py emit() now writes the two lines into its header comment and the file was regenerated -- gen_atlas.py --check passes and the regeneration adds only those comment lines to the previously committed file. README's provenance section now states MIT and links LICENSE. No code, test or public API change.
656 lines
26 KiB
Python
Executable File
656 lines
26 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: MIT
|
|
# SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
"""Bake the embedded mono font's anti-aliased atlas, at authoring time.
|
|
|
|
Reads a TrueType/OpenType file and writes a C++ translation unit holding a
|
|
greyscale coverage atlas, one fixed cell per glyph, plus the per-glyph rect,
|
|
bearing and advance table the text pipeline stages from.
|
|
|
|
tools/fonts/gen_atlas.py [--font PATH] [--em N] [--supersample N] [--out PATH] [--check]
|
|
|
|
This is an **authoring-time** tool. `plot_core` and every public header under
|
|
`include/plot/` stay third-party-free: nothing here runs at build time, the
|
|
atlas is committed as data, and `fontTools` is never needed to compile or run the
|
|
library. Regenerating is a deliberate act followed by a commit.
|
|
|
|
Rasterisation is ours, not a rasteriser's: the glyph outlines are flattened with
|
|
`fontTools.pens`, scanline-filled at `supersample` times the em, and box-filtered
|
|
down to one coverage byte per texel. Nothing shells out.
|
|
|
|
Determinism
|
|
-----------
|
|
Same font file plus the same em size and supersample factor must produce a
|
|
byte-identical translation unit -- `--check` depends on it, and so does a
|
|
reviewable diff. Everything emitted is derived from integers (cell geometry, the
|
|
font's own advance units) or from a fixed, sorted walk of the coverage list; no
|
|
timestamp, no set/dict iteration order, no float formatting, no host path beyond
|
|
the source font's file name.
|
|
|
|
Blank ink is a hard failure
|
|
---------------------------
|
|
Every requested codepoint must rasterise to at least one inked texel. A silent
|
|
blank cell is exactly the failure that makes a font swap look like it worked, so
|
|
the generator exits non-zero instead. The single documented exception is U+0020,
|
|
which is blank *by definition* -- an intentional blank with a full advance.
|
|
|
|
Provenance
|
|
----------
|
|
The default face is Adwaita Mono, `AdwaitaMono-Regular.ttf`, released under the
|
|
SIL Open Font License 1.1 (a copy is committed at `licences/OFL-1.1.txt`). The
|
|
backend is named neutrally (`EmbeddedMonoFont`) because OFL reserves the font
|
|
name for modified redistributions. The generated file records the source file
|
|
name, the font's own name/version strings, the em size, the supersample factor
|
|
and the exact coverage set.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import math
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
DEFAULT_FONT = "/usr/share/fonts/Adwaita/AdwaitaMono-Regular.ttf"
|
|
DEFAULT_EM = 13
|
|
DEFAULT_SUPERSAMPLE = 4
|
|
DEFAULT_OUT = "src/render/embedded_mono_font.cpp"
|
|
|
|
SCRIPT = "tools/fonts/gen_atlas.py"
|
|
|
|
# The fallback box, in font units with the origin at the pen on the baseline, y up
|
|
# like the outlines it stands in for. Its two contours wind oppositely, so the
|
|
# non-zero fill leaves a hollow box: an outer rectangle and an inner hole, each
|
|
# stroke 100 units (1.3 px at 13 px em). It is authored here, not taken from the
|
|
# font -- it is what an uncovered codepoint draws.
|
|
FALLBACK_OUTER = ((60.0, -100.0), (580.0, -100.0), (580.0, 700.0), (60.0, 700.0))
|
|
FALLBACK_INNER = ((160.0, 0.0), (160.0, 600.0), (480.0, 600.0), (480.0, 0.0))
|
|
|
|
# The codepoint side of the coverage set: printable ASCII, the three codepoints
|
|
# `plot::format_si` emits, and the two dashes a plotting label uses. The fallback
|
|
# slot is appended after them.
|
|
ASCII = list(range(0x20, 0x7F))
|
|
SPECIALS = [0xB0, 0xB5, 0x03A9, 0x2013, 0x2014]
|
|
|
|
FALLBACK_CODEPOINT = 0xFFFFFFFF # not a Unicode scalar; cannot collide
|
|
|
|
# Sub-curve flattening steps. Fixed, not tolerance-adaptive: at a 13 px em they
|
|
# are far below one supersampled texel even at 4x, and a constant keeps the
|
|
# output independent of anything but the font file.
|
|
QUAD_STEPS = 12
|
|
CUBIC_STEPS = 16
|
|
|
|
EXIT_OK = 0
|
|
EXIT_MISMATCH = 1
|
|
EXIT_BAD_FONT = 2
|
|
EXIT_NO_FONTTOOLS = 3
|
|
|
|
|
|
def _fail(code: int, message: str) -> int:
|
|
print(f"{SCRIPT}: {message}", file=sys.stderr)
|
|
return code
|
|
|
|
|
|
def _require_fonttools():
|
|
try:
|
|
from fontTools.pens.basePen import BasePen # noqa: F401
|
|
from fontTools.ttLib import TTFont # noqa: F401
|
|
except ImportError:
|
|
raise SystemExit(
|
|
_fail(
|
|
EXIT_NO_FONTTOOLS,
|
|
"fontTools is not installed; this is an authoring-time tool. "
|
|
"Install it (pip install fonttools) to regenerate the atlas.",
|
|
)
|
|
)
|
|
|
|
|
|
def _contour_pen_class():
|
|
from fontTools.pens.basePen import BasePen
|
|
|
|
class ContourPen(BasePen):
|
|
"""Flattens one glyph to contour point lists in cell space.
|
|
|
|
`xf` maps a font-unit point (y up, origin at the pen on the baseline) to
|
|
supersampled cell coordinates (y down, origin at the cell's top-left),
|
|
so every geometric decision below is made once, here.
|
|
"""
|
|
|
|
def __init__(self, glyph_set, xf):
|
|
super().__init__(glyph_set)
|
|
self._xf = xf
|
|
self.contours = []
|
|
self._current = []
|
|
|
|
def _map(self, point):
|
|
return self._xf(point[0], point[1])
|
|
|
|
def _moveTo(self, pt):
|
|
self._current = [self._map(pt)]
|
|
|
|
def _lineTo(self, pt):
|
|
self._current.append(self._map(pt))
|
|
|
|
def _qCurveToOne(self, control, point):
|
|
p0 = self._current[-1]
|
|
p1 = self._map(control)
|
|
p2 = self._map(point)
|
|
for step in range(1, QUAD_STEPS + 1):
|
|
t = step / QUAD_STEPS
|
|
u = 1.0 - t
|
|
self._current.append(
|
|
(
|
|
u * u * p0[0] + 2.0 * u * t * p1[0] + t * t * p2[0],
|
|
u * u * p0[1] + 2.0 * u * t * p1[1] + t * t * p2[1],
|
|
)
|
|
)
|
|
|
|
def _curveToOne(self, c1, c2, point):
|
|
p0 = self._current[-1]
|
|
p1 = self._map(c1)
|
|
p2 = self._map(c2)
|
|
p3 = self._map(point)
|
|
for step in range(1, CUBIC_STEPS + 1):
|
|
t = step / CUBIC_STEPS
|
|
u = 1.0 - t
|
|
b0 = u * u * u
|
|
b1 = 3.0 * u * u * t
|
|
b2 = 3.0 * u * t * t
|
|
b3 = t * t * t
|
|
self._current.append(
|
|
(
|
|
b0 * p0[0] + b1 * p1[0] + b2 * p2[0] + b3 * p3[0],
|
|
b0 * p0[1] + b1 * p1[1] + b2 * p2[1] + b3 * p3[1],
|
|
)
|
|
)
|
|
|
|
def _closePath(self):
|
|
if len(self._current) >= 3:
|
|
self.contours.append(self._current)
|
|
self._current = []
|
|
|
|
def _endPath(self):
|
|
self._closePath()
|
|
|
|
return ContourPen
|
|
|
|
|
|
def _scanline_mask(contours, width, height):
|
|
"""Non-zero-winding scanline fill of `contours` into a `height` x `width` 0/1 grid.
|
|
|
|
The fill rule is non-zero rather than even-odd because TrueType orients a
|
|
hole opposite its outer contour and a font is entitled to overlapping
|
|
contours; even-odd would punch a hole out of any overlap.
|
|
"""
|
|
mask = [[0] * width for _ in range(height)]
|
|
edges = []
|
|
for contour in contours:
|
|
count = len(contour)
|
|
for i in range(count):
|
|
x0, y0 = contour[i]
|
|
x1, y1 = contour[(i + 1) % count]
|
|
if y0 != y1:
|
|
edges.append((x0, y0, x1, y1))
|
|
for row in range(height):
|
|
yc = row + 0.5
|
|
crossings = []
|
|
for x0, y0, x1, y1 in edges:
|
|
if (y0 <= yc) != (y1 <= yc):
|
|
t = (yc - y0) / (y1 - y0)
|
|
crossings.append((x0 + t * (x1 - x0), 1 if y1 > y0 else -1))
|
|
if not crossings:
|
|
continue
|
|
crossings.sort()
|
|
winding = 0
|
|
span_start = 0.0
|
|
for x, direction in crossings:
|
|
previous = winding
|
|
winding += direction
|
|
if previous == 0 and winding != 0:
|
|
span_start = x
|
|
elif previous != 0 and winding == 0:
|
|
first = max(0, int(math.ceil(span_start - 0.5)))
|
|
last = min(width, int(math.floor(x - 0.5)) + 1)
|
|
target = mask[row]
|
|
for column in range(first, last):
|
|
target[column] = 1
|
|
return mask
|
|
|
|
|
|
def _coverage_cell(contours, cell_w, cell_h, supersample):
|
|
"""Box-filter `contours` into `cell_h` rows of `cell_w` coverage bytes.
|
|
|
|
`contours` are in supersampled cell space, so each output texel averages the
|
|
`supersample` x `supersample` sub-samples that land on it -- a plain coverage
|
|
count, which is what makes the result greyscale.
|
|
"""
|
|
area = supersample * supersample
|
|
mask = _scanline_mask(contours, cell_w * supersample, cell_h * supersample)
|
|
rows = []
|
|
for ty in range(cell_h):
|
|
row = []
|
|
for tx in range(cell_w):
|
|
covered = 0
|
|
for sy in range(supersample):
|
|
sub = mask[ty * supersample + sy]
|
|
base = tx * supersample
|
|
for sx in range(supersample):
|
|
covered += sub[base + sx]
|
|
# Round-half-up in integers: deterministic, and independent of the
|
|
# host's floating-point formatting.
|
|
row.append((covered * 255 + area // 2) // area)
|
|
rows.append(row)
|
|
return rows
|
|
|
|
|
|
def _cell_transform(em, units_per_em, ascent_px, supersample):
|
|
scale = em / units_per_em
|
|
|
|
def xf(x_units, y_units):
|
|
return (x_units * scale * supersample, (ascent_px - y_units * scale) * supersample)
|
|
|
|
return xf
|
|
|
|
|
|
def _read_font(path):
|
|
from fontTools.ttLib import TTFont
|
|
|
|
if not os.path.isfile(path):
|
|
raise SystemExit(_fail(EXIT_BAD_FONT, f"no such font file: {path}"))
|
|
try:
|
|
return TTFont(path, recalcBBoxes=False, recalcTimestamp=False)
|
|
except Exception as exc: # fontTools raises a spread of parser errors
|
|
raise SystemExit(_fail(EXIT_BAD_FONT, f"cannot parse {path}: {exc}"))
|
|
|
|
|
|
def build(font_path, em, supersample):
|
|
"""Rasterise the coverage set at `em` px; returns the geometry and cells."""
|
|
font = _read_font(font_path)
|
|
head = font["head"]
|
|
os2 = font["OS/2"]
|
|
units_per_em = head.unitsPerEm
|
|
glyph_set = font.getGlyphSet()
|
|
cmap = font.getBestCmap()
|
|
hmtx = font["hmtx"]
|
|
|
|
name_record = font["name"]
|
|
|
|
def name(identifier, fallback):
|
|
value = name_record.getDebugName(identifier)
|
|
return value if value else fallback
|
|
|
|
scale = em / units_per_em
|
|
ascent_units = os2.sTypoAscender
|
|
descent_units = -os2.sTypoDescender
|
|
ascent_px = ascent_units * scale
|
|
|
|
contours_class = _contour_pen_class()
|
|
from fontTools.pens.boundsPen import BoundsPen
|
|
|
|
codepoints = list(ASCII) + list(SPECIALS)
|
|
|
|
# The cell: wide enough for the widest advance (the pen must never leave the
|
|
# cell) and for the widest ink; tall enough for the face's ascent-to-descent
|
|
# extent. Never trimmed to any one glyph's ink -- a fixed-cell grid is what
|
|
# lets the pipeline carry one rect size per draw.
|
|
widest_advance = 0
|
|
widest_ink = 0.0
|
|
for codepoint in codepoints:
|
|
glyph_name = cmap.get(codepoint)
|
|
if glyph_name is None:
|
|
raise SystemExit(
|
|
_fail(EXIT_BAD_FONT, f"{font_path} has no glyph for U+{codepoint:04X}")
|
|
)
|
|
advance, _bearing = hmtx[glyph_name]
|
|
widest_advance = max(widest_advance, advance)
|
|
bounds = BoundsPen(glyph_set)
|
|
glyph_set[glyph_name].draw(bounds)
|
|
if bounds.bounds is not None:
|
|
widest_ink = max(widest_ink, bounds.bounds[2])
|
|
|
|
cell_w = int(math.ceil(max(widest_advance * scale, widest_ink * scale) - 1e-9))
|
|
cell_h = int(math.ceil(ascent_px + descent_units * scale - 1e-9))
|
|
if cell_w <= 0 or cell_h <= 0:
|
|
raise SystemExit(_fail(EXIT_BAD_FONT, "the computed cell is empty"))
|
|
|
|
xf = _cell_transform(em, units_per_em, ascent_px, supersample)
|
|
|
|
cells = []
|
|
advances = []
|
|
for codepoint in codepoints:
|
|
glyph_name = cmap[codepoint]
|
|
pen = contours_class(glyph_set, xf)
|
|
glyph_set[glyph_name].draw(pen)
|
|
rows = _coverage_cell(pen.contours, cell_w, cell_h, supersample)
|
|
ink = sum(sum(row) for row in rows)
|
|
if codepoint != 0x20 and ink == 0:
|
|
# A silent blank cell is the failure mode this whole exercise exists
|
|
# to remove: refuse to ship it.
|
|
raise SystemExit(
|
|
_fail(
|
|
EXIT_BAD_FONT,
|
|
f"U+{codepoint:04X} ({glyph_name}) rasterised to no ink at "
|
|
f"{em} px; the cell is too small or the glyph is missing",
|
|
)
|
|
)
|
|
cells.append(rows)
|
|
advances.append(hmtx[glyph_name][0])
|
|
|
|
fallback_pen = contours_class(glyph_set, xf)
|
|
fallback_pen.contours = [
|
|
[fallback_pen._xf(x, y) for x, y in FALLBACK_OUTER],
|
|
[fallback_pen._xf(x, y) for x, y in FALLBACK_INNER],
|
|
]
|
|
fallback_rows = _coverage_cell(fallback_pen.contours, cell_w, cell_h, supersample)
|
|
if sum(sum(row) for row in fallback_rows) == 0:
|
|
raise SystemExit(_fail(EXIT_BAD_FONT, "the fallback box rasterised to no ink"))
|
|
cells.append(fallback_rows)
|
|
advances.append(widest_advance)
|
|
|
|
geometry = {
|
|
"em": em,
|
|
"supersample": supersample,
|
|
"units_per_em": units_per_em,
|
|
"scale": scale,
|
|
"ascent_units": ascent_units,
|
|
"descent_units": descent_units,
|
|
"ascent_px": ascent_px,
|
|
"cell_w": cell_w,
|
|
"cell_h": cell_h,
|
|
"glyph_count": len(codepoints) + 1,
|
|
"fallback_slot": len(codepoints),
|
|
"codepoints": codepoints,
|
|
"advances": advances,
|
|
"cells": cells,
|
|
}
|
|
provenance = {
|
|
"font_file": os.path.basename(font_path),
|
|
"font_name": name(4, "unknown"),
|
|
"font_version": name(5, "unknown"),
|
|
}
|
|
font.close()
|
|
return geometry, provenance
|
|
|
|
|
|
def _codepoint_label(codepoint):
|
|
if codepoint == FALLBACK_CODEPOINT:
|
|
return "fallback box"
|
|
return f"U+{codepoint:04X}"
|
|
|
|
|
|
def emit(geometry, provenance):
|
|
"""Render one `build()` result as the generated translation unit."""
|
|
em = geometry["em"]
|
|
supersample = geometry["supersample"]
|
|
units_per_em = geometry["units_per_em"]
|
|
cell_w = geometry["cell_w"]
|
|
cell_h = geometry["cell_h"]
|
|
glyph_count = geometry["glyph_count"]
|
|
fallback_slot = geometry["fallback_slot"]
|
|
atlas_w = cell_w * glyph_count
|
|
atlas_h = cell_h
|
|
codepoints = geometry["codepoints"]
|
|
advances = geometry["advances"]
|
|
slot_codepoints = list(codepoints) + [FALLBACK_CODEPOINT]
|
|
|
|
ascii_lo = ASCII[0]
|
|
ascii_hi = ASCII[-1]
|
|
|
|
lines = []
|
|
add = lines.append
|
|
|
|
add("// SPDX-License-Identifier: MIT")
|
|
add("// SPDX-FileCopyrightText: 2026 Martino Ferrari")
|
|
add("//")
|
|
add("// GENERATED FILE -- do not edit by hand.")
|
|
add("//")
|
|
add(f"// Regenerate with: {SCRIPT}")
|
|
add(f"// The generator takes the source font, the em size and the supersample factor;")
|
|
add(f"// its defaults bake the face this file holds. `{SCRIPT} --check`")
|
|
add("// regenerates into memory and fails if this file has drifted from its input.")
|
|
add("//")
|
|
add("// Provenance")
|
|
add("// source font : " + provenance["font_file"])
|
|
add("// font name : " + provenance["font_name"])
|
|
add("// font version : " + provenance["font_version"])
|
|
add("// licence : SIL Open Font License 1.1 (licences/OFL-1.1.txt)")
|
|
add(f"// units per em : {units_per_em}")
|
|
add(f"// em size : {em} px")
|
|
add(f"// supersample : {supersample}x ({supersample * supersample} samples per texel)")
|
|
add(f"// coverage : U+{ascii_lo:04X}..U+{ascii_hi:04X}, "
|
|
+ ", ".join(f"U+{cp:04X}" for cp in SPECIALS)
|
|
+ ", plus a fallback box")
|
|
add(f"// cell : {cell_w} x {cell_h} texels, one row of {glyph_count}")
|
|
add(f"// atlas : {atlas_w} x {atlas_h} texels")
|
|
add("//")
|
|
add("// The cell is the face's maximum advance wide and its ascent-to-descent extent")
|
|
add("// tall; every glyph is drawn into that cell with its bearing preserved, never")
|
|
add("// trimmed to its ink, because the pipeline stages one rect *origin* per glyph and")
|
|
add("// carries one rect *size* for the whole draw (see render::TextBackend::cell_uv).")
|
|
add("// Coverage is a plain supersampled coverage count, so a stroke's edges are grey")
|
|
add("// and not a 1-bit threshold.")
|
|
add("")
|
|
add("#include <plot/render/embedded_mono_font.hpp>")
|
|
add("")
|
|
add("#include <array>")
|
|
add("#include <utility>")
|
|
add("")
|
|
add("namespace plot::render {")
|
|
add("namespace {")
|
|
add("")
|
|
add("// Geometry, all of it derived from the bake above by the generator.")
|
|
add(f"constexpr u32 kEmPx = {em}u;")
|
|
add(f"constexpr u32 kSupersample = {supersample}u;")
|
|
add(f"constexpr u32 kUnitsPerEm = {units_per_em}u;")
|
|
add(f"constexpr u32 kCellW = {cell_w}u;")
|
|
add(f"constexpr u32 kCellH = {cell_h}u;")
|
|
add(f"constexpr u32 kGlyphCount = {glyph_count}u;")
|
|
add(f"constexpr u32 kFallbackSlot = {fallback_slot}u;")
|
|
add("constexpr u32 kAtlasW = kCellW * kGlyphCount;")
|
|
add("constexpr u32 kAtlasH = kCellH;")
|
|
add("constexpr u32 kAsciiFirst = " + f"{ascii_lo}u;")
|
|
add("constexpr u32 kAsciiGlyphs = " + f"{len(ASCII)}u;")
|
|
add("constexpr u32 kFallbackCodepoint = " + f"0x{FALLBACK_CODEPOINT:08X}u;")
|
|
add("")
|
|
add(f"constexpr u32 kAscentUnits = {geometry['ascent_units']}u;")
|
|
add("// Font pixels per font unit. The one non-integer factor, and the only place a")
|
|
add("// float enters: everything else here is an integer.")
|
|
add("constexpr f32 kScale = static_cast<f32>(kEmPx) / static_cast<f32>(kUnitsPerEm);")
|
|
add("constexpr f32 kAscentPx = static_cast<f32>(kAscentUnits) * kScale;")
|
|
add("")
|
|
add("// The public header repeats the em size, the supersample factor and the")
|
|
add("// fallback marker so a caller can read them without opening this table.")
|
|
add("// These asserts are what make that a pin and not a copy: re-baking at another")
|
|
add("// em or supersample factor, or moving the fallback marker, fails to compile")
|
|
add("// until the header agrees, so the two can never silently disagree.")
|
|
add("static_assert(kEmPx == EmbeddedMonoFont::kEmPx,")
|
|
add(" \"embedded_mono_font.hpp kEmPx disagrees with the bake\");")
|
|
add("static_assert(kSupersample == EmbeddedMonoFont::kSupersample,")
|
|
add(" \"embedded_mono_font.hpp kSupersample disagrees with the bake\");")
|
|
add("static_assert(kFallbackCodepoint == EmbeddedMonoFont::kFallbackCodepoint,")
|
|
add(" \"embedded_mono_font.hpp kFallbackCodepoint disagrees with the bake\");")
|
|
add("")
|
|
add("/// The codepoint each slot was baked for, in slot order; the fallback slot")
|
|
add("/// carries `kFallbackCodepoint`, which is not a Unicode scalar.")
|
|
add(f"constexpr u32 kCodepoints[kGlyphCount] = {{")
|
|
for slot, codepoint in enumerate(codepoints):
|
|
add(f" 0x{codepoint:04X}u, // [{slot:3d}] {_codepoint_label(codepoint)}")
|
|
add(f" 0x{FALLBACK_CODEPOINT:08X}u, // [{fallback_slot:3d}] {_codepoint_label(FALLBACK_CODEPOINT)}")
|
|
add("};")
|
|
add("")
|
|
add("/// The advance each slot contributes to the pen, in font units. The face is")
|
|
add("/// monospaced, so these are all equal -- but the table is per glyph, because the")
|
|
add("/// advance is the font's own `hmtx` value and not an assumption about the face.")
|
|
add(f"constexpr u32 kAdvanceUnits[kGlyphCount] = {{")
|
|
for slot, advance in enumerate(advances):
|
|
codepoint = codepoints[slot] if slot < len(codepoints) else FALLBACK_CODEPOINT
|
|
add(f" {advance}u, // [{slot:3d}] {_codepoint_label(codepoint)}")
|
|
add("};")
|
|
add("")
|
|
add("/// The packed atlas: one row of cells, top row first, straight coverage, 0 for")
|
|
add("/// background and 255 for full ink. GL_R8-ready, no dilation, no gamma.")
|
|
add(f"constexpr u8 kAtlas[kAtlasW * kAtlasH] = {{")
|
|
for row in range(atlas_h):
|
|
for slot in range(glyph_count):
|
|
values = geometry["cells"][slot][row]
|
|
body = ", ".join(f"0x{value:02X}" for value in values)
|
|
label = _codepoint_label(slot_codepoints[slot])
|
|
add(f" {body}, // [{slot:3d}] {label} row {row:2d}")
|
|
add("};")
|
|
add("")
|
|
add("/// One slot's glyph: the cell it occupies in the atlas, drawn from the cell's")
|
|
add("/// top-left (the pen) with no extra bearing -- the bearing is baked into the")
|
|
add("/// cell's ink -- and the advance the pen moves by.")
|
|
add("constexpr Glyph make_glyph(u32 slot) noexcept {")
|
|
add(" const f32 u0 = static_cast<f32>(slot * kCellW) / static_cast<f32>(kAtlasW);")
|
|
add(" const f32 du = static_cast<f32>(kCellW) / static_cast<f32>(kAtlasW);")
|
|
add(" const f32 dv = static_cast<f32>(kCellH) / static_cast<f32>(kAtlasH);")
|
|
add(" return Glyph{u0, 0.0f, u0 + du, dv, 0.0f, 0.0f,")
|
|
add(" static_cast<f32>(kAdvanceUnits[slot]) * kScale};")
|
|
add("}")
|
|
add("")
|
|
add("template <u32... Slots>")
|
|
add("constexpr std::array<Glyph, sizeof...(Slots)> make_glyphs(")
|
|
add(" std::integer_sequence<u32, Slots...>) noexcept {")
|
|
add(" return {make_glyph(Slots)...};")
|
|
add("}")
|
|
add("")
|
|
add("constexpr std::array<Glyph, kGlyphCount> kGlyphs =")
|
|
add(" make_glyphs(std::make_integer_sequence<u32, kGlyphCount>{});")
|
|
add("")
|
|
add("} // namespace")
|
|
add("")
|
|
add("u32 EmbeddedMonoFont::glyph_index(u32 codepoint) noexcept {")
|
|
add(" if (codepoint >= kAsciiFirst && codepoint < kAsciiFirst + kAsciiGlyphs) {")
|
|
add(" return codepoint - kAsciiFirst;")
|
|
add(" }")
|
|
add(" switch (codepoint) {")
|
|
for offset, codepoint in enumerate(SPECIALS):
|
|
add(f" case 0x{codepoint:04X}u:")
|
|
add(f" return kAsciiGlyphs + {offset}u;")
|
|
add(" default:")
|
|
add(" return kFallbackSlot;")
|
|
add(" }")
|
|
add("}")
|
|
add("")
|
|
add("f32 EmbeddedMonoFont::advance_codepoint(u32 codepoint) noexcept {")
|
|
add(" return static_cast<f32>(kAdvanceUnits[glyph_index(codepoint)]) * kScale;")
|
|
add("}")
|
|
add("")
|
|
add("f32 EmbeddedMonoFont::measure(const char* utf8) const {")
|
|
add(" if (utf8 == nullptr) {")
|
|
add(" return 0.0f;")
|
|
add(" }")
|
|
add(" f32 width = 0.0f;")
|
|
add(" for (const char* p = utf8; *p != '\\0';) {")
|
|
add(" width += advance_codepoint(next_codepoint(p));")
|
|
add(" }")
|
|
add(" return width;")
|
|
add("}")
|
|
add("")
|
|
add("f32 EmbeddedMonoFont::height() const { return static_cast<f32>(kCellH); }")
|
|
add("")
|
|
add("f32 EmbeddedMonoFont::ascent() const { return kAscentPx; }")
|
|
add("")
|
|
add("f32 EmbeddedMonoFont::em_px() const { return static_cast<f32>(kEmPx); }")
|
|
add("")
|
|
add("const Glyph* EmbeddedMonoFont::glyph(u32 codepoint) const {")
|
|
add(" return &kGlyphs[glyph_index(codepoint)];")
|
|
add("}")
|
|
add("")
|
|
add("Vec2 EmbeddedMonoFont::cell_uv() const {")
|
|
add(" return Vec2{static_cast<f32>(kCellW) / static_cast<f32>(kAtlasW),")
|
|
add(" static_cast<f32>(kCellH) / static_cast<f32>(kAtlasH)};")
|
|
add("}")
|
|
add("")
|
|
add("const u8* EmbeddedMonoFont::atlas_pixels() const { return kAtlas; }")
|
|
add("")
|
|
add("u32 EmbeddedMonoFont::atlas_width() const { return kAtlasW; }")
|
|
add("")
|
|
add("u32 EmbeddedMonoFont::atlas_height() const { return kAtlasH; }")
|
|
add("")
|
|
add("u32 EmbeddedMonoFont::revision() const { return 0u; }")
|
|
add("")
|
|
add("u32 EmbeddedMonoFont::glyph_count() noexcept { return kGlyphCount; }")
|
|
add("")
|
|
add("u32 EmbeddedMonoFont::codepoint_of_slot(u32 slot) noexcept {")
|
|
add(" return slot < kGlyphCount ? kCodepoints[slot] : kFallbackCodepoint;")
|
|
add("}")
|
|
add("")
|
|
add("u32 EmbeddedMonoFont::cell_width() noexcept { return kCellW; }")
|
|
add("")
|
|
add("u32 EmbeddedMonoFont::cell_height() noexcept { return kCellH; }")
|
|
add("")
|
|
add("Vec2 EmbeddedMonoFont::atlas_origin(u32 slot) noexcept {")
|
|
add(" const u32 s = slot < kGlyphCount ? slot : kFallbackSlot;")
|
|
add(" return Vec2{static_cast<f32>(s * kCellW), 0.0f};")
|
|
add("}")
|
|
add("")
|
|
add("const u8* EmbeddedMonoFont::atlas_image() noexcept { return kAtlas; }")
|
|
add("")
|
|
add("} // namespace plot::render")
|
|
add("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def generate(font_path, em, supersample):
|
|
geometry, provenance = build(font_path, em, supersample)
|
|
return emit(geometry, provenance), geometry
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
parser.add_argument("--font", default=DEFAULT_FONT, help="source TrueType/OpenType file")
|
|
parser.add_argument("--em", type=int, default=DEFAULT_EM, help="em size in pixels")
|
|
parser.add_argument(
|
|
"--supersample", type=int, default=DEFAULT_SUPERSAMPLE, help="coverage samples per texel"
|
|
)
|
|
parser.add_argument("--out", default=DEFAULT_OUT, help="translation unit to write")
|
|
parser.add_argument(
|
|
"--check",
|
|
action="store_true",
|
|
help="regenerate and fail if the committed file differs, instead of writing",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
if args.em <= 0 or args.supersample <= 0:
|
|
return _fail(EXIT_BAD_FONT, "em size and supersample factor must be positive")
|
|
|
|
_require_fonttools()
|
|
|
|
if args.check:
|
|
out = Path(args.out)
|
|
committed = out.read_bytes() if out.is_file() else None
|
|
# A missing committed file is drift whatever else is wrong with the
|
|
# machine, so it is reported before the font is read: otherwise a checkout
|
|
# with no atlas *and* no installed face would look like an environment
|
|
# gap (EXIT_BAD_FONT) instead of the failure it is.
|
|
if committed is None:
|
|
return _fail(EXIT_MISMATCH, f"{out} does not exist; run {SCRIPT} to generate it")
|
|
fresh, _ = generate(args.font, args.em, args.supersample)
|
|
if committed != fresh.encode():
|
|
return _fail(
|
|
EXIT_MISMATCH,
|
|
f"{out} differs from a fresh bake; regenerate with {SCRIPT} and commit the result",
|
|
)
|
|
print(f"{SCRIPT}: {out} matches a fresh bake", file=sys.stderr)
|
|
return EXIT_OK
|
|
|
|
text, geometry = generate(args.font, args.em, args.supersample)
|
|
Path(args.out).write_bytes(text.encode())
|
|
ink = sum(sum(row) for cell in geometry["cells"] for row in cell)
|
|
print(
|
|
f"{SCRIPT}: {args.out}: {geometry['glyph_count']} glyphs, "
|
|
f"cell {geometry['cell_w']}x{geometry['cell_h']}, "
|
|
f"atlas {geometry['cell_w'] * geometry['glyph_count']}x{geometry['cell_h']}, "
|
|
f"{args.em} px em, {args.supersample}x, {ink} ink units",
|
|
file=sys.stderr,
|
|
)
|
|
return EXIT_OK
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|