epiphany/crates/epiphany-render-svg/tools/extract_bravura_outlines.py

108 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Extract genuine Bravura SMuFL glyph outlines into a Rust table.
Reproducible generator for `epiphany-render-svg`'s bundled outline data. It
fetches the official OFL `Bravura.otf` and the SMuFL `glyphnames.json`, then
emits `src/outlines_generated.rs` with each glyph's outline as an SVG path in
**staff-space**, **y-up** coordinates (the renderer's coordinate system).
Usage:
python3 -m venv .venv && . .venv/bin/activate && pip install fonttools
python3 extract_bravura_outlines.py > ../crates/epiphany-render-svg/src/outlines_generated.rs
The font is NOT vendored; only the generated Rust is committed. Bravura is
© Steinberg Media Technologies GmbH under the SIL Open Font License 1.1; the
extracted outlines are redistributed under the same license (see OFL.txt).
"""
import json, re, sys, urllib.request
FONT_URL = "https://raw.githubusercontent.com/steinbergmedia/bravura/master/redist/otf/Bravura.otf"
NAMES_URL = "https://raw.githubusercontent.com/w3c/smufl/gh-pages/metadata/glyphnames.json"
# Exactly the glyph set the v0 layout pipeline can name (layout-ir BRAVURA_METRICS).
NAMES = ["noteheadBlack","noteheadHalf","noteheadWhole","noteheadDoubleWhole",
"gClef","fClef","cClef","accidentalSharp","accidentalFlat","accidentalNatural",
"accidentalDoubleSharp","restWhole","restHalf","restQuarter","rest8th",
"flag8thUp","flag8thDown","augmentationDot","timeSig4","timeSigCommon",
"barlineSingle","barlineFinal","dynamicForte","dynamicPiano"]
def load():
from fontTools.ttLib import TTFont
import io
if "--local" in sys.argv:
font = TTFont("Bravura.otf"); names = json.load(open("glyphnames.json"))
else:
font = TTFont(io.BytesIO(urllib.request.urlopen(FONT_URL).read()))
names = json.loads(urllib.request.urlopen(NAMES_URL).read())
return font, names
def round_d(d, nd=4):
def r(m):
v = round(float(m.group(0)), nd)
s = f"{v:.{nd}f}".rstrip('0').rstrip('.')
return "0" if s in ("", "-0") else s
return re.sub(r'-?\d+\.?\d*(?:e-?\d+)?', r, d)
def main():
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.transformPen import TransformPen
from fontTools.pens.boundsPen import BoundsPen
font, glyphnames = load()
upm = font["head"].unitsPerEm
sp = upm / 4.0 # font units per staff space (SMuFL em = 4 staff spaces)
scale = 1.0 / sp
gs = font.getGlyphSet()
cmap = font.getBestCmap()
rows = []
for name in NAMES:
cp = int(glyphnames[name]["codepoint"].replace("U+", ""), 16)
g = cmap.get(cp)
if g is None:
print(f"// MISSING {name} U+{cp:04X}", file=sys.stderr); continue
pen = SVGPathPen(gs)
gs[g].draw(TransformPen(pen, (scale, 0, 0, scale, 0, 0)))
d = round_d(pen.getCommands())
bp = BoundsPen(gs); gs[g].draw(bp)
l, b, r, t = ([round(v * scale, 4) for v in bp.bounds] if bp.bounds else [0, 0, 0, 0])
rows.append((name, cp, d, (l, b, r, t)))
rows.sort()
o = []
o.append("//! GENERATED by `tools/extract_bravura_outlines.py` — do not edit by hand.")
o.append("//!")
o.append("//! Real Bravura SMuFL glyph outlines, extracted from the official OFL")
o.append(f"//! `Bravura.otf` (unitsPerEm = {upm}; 1 staff space = {sp:g} font units, since the")
o.append("//! SMuFL em is 4 staff spaces). Path `d` data is in **staff-space** units, **y-up**")
o.append("//! (musical convention, positive y = higher pitch), relative to each glyph's")
o.append("//! origin, rounded to 4 decimals. The renderer applies a single y-flip wrapper.")
o.append("//!")
o.append("//! Bravura is (c) Steinberg Media Technologies GmbH under the SIL Open Font")
o.append("//! License 1.1; these extracted outlines are redistributed under the same license")
o.append("//! (see `tools/OFL.txt`).")
o.append("")
o.append("/// One bundled glyph outline: SMuFL name, codepoint, the SVG path `d` in")
o.append("/// staff-space / y-up coordinates, and the outline's tight bounding box")
o.append("/// `[left, bottom, right, top]` in staff spaces.")
o.append("pub(crate) struct BravuraOutline {")
o.append(" pub name: &'static str,")
o.append(" pub codepoint: u32,")
o.append(" pub path: &'static str,")
o.append(" pub bbox: [f32; 4],")
o.append("}")
o.append("")
o.append("/// Every glyph the v0 layout pipeline can name (the `BRAVURA_METRICS` set), with")
o.append("/// its genuine Bravura outline. Sorted by name for deterministic binary search.")
o.append("pub(crate) const BRAVURA_OUTLINES: &[BravuraOutline] = &[")
for name, cp, d, (l, b, r, t) in rows:
o.append(" BravuraOutline {")
o.append(f" name: {json.dumps(name)},")
o.append(f" codepoint: 0x{cp:04X},")
o.append(f" path: {json.dumps(d)},")
o.append(f" bbox: [{l}, {b}, {r}, {t}],")
o.append(" },")
o.append("];")
sys.stdout.write("\n".join(o) + "\n")
print(f"// extracted {len(rows)}/{len(NAMES)} glyphs", file=sys.stderr)
if __name__ == "__main__":
main()