Files
plotter/tools/fonts/gen_atlas.py
T

646 lines
26 KiB
Python
Executable File

#!/usr/bin/env python3
"""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("// 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
fresh, _ = generate(args.font, args.em, args.supersample)
if committed is None:
return _fail(EXIT_MISMATCH, f"{out} does not exist; run {SCRIPT} to generate it")
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())