Files
Martino Ferrari ed3a8ce645 licence: MIT LICENSE and SPDX identifiers in every first-party file
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.
2026-09-28 22:13:40 +02:00

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())