#!/usr/bin/env python3 """Generate common/lib/brandMediaGlyphs.ts: the Archilyzer Media lockup's letters as outlines. The lockup (common/lib/brandMedia.ts mediaLockupSvg) is the `Archi|lyzer` wordmark over `MEDIA`, all Archivo at wdth 118. It is drawn as PATHS, not , so anything that rasterises it -- rsvg-convert, satori's , ImageMagick -- needs no font installed and cannot substitute one. This script is where those paths come from. For each run (`Archi` at wght 720, `lyzer` at 380, `MEDIA` at 700) it pins the variable font to that instance with the fontTools instancer and records, per glyph, the advance and the outline, plus the GPOS pair kerning between neighbours inside the run. (A browser does not kern ACROSS two spans of different weight -- they are two shaping runs -- so there is no kerning between `Archi` and `lyzer`, and none is recorded.) Coordinates are font units (the em is `unitsPerEm`), with y pointing DOWN and the baseline at 0, which is what an SVG `translate(x baseline) scale(px / unitsPerEm)` wants. The font is the copy vendored for the umtool report-to-video preset, umtool/report-to-video/fonts/Archivo[wdth,wght].ttf, so the outlines and the Pango-rendered titles beside them come from the same bytes. `--font` takes another path; `--download` fetches google/fonts' copy instead (into a temp dir, not the repo). python3 common/bin/gen-media-glyphs.py # rewrite common/lib/brandMediaGlyphs.ts python3 common/bin/gen-media-glyphs.py --stdout # print it (the parity test diffs this) Needs fontTools (>= 4.40, for the instancer's partial-instance API). """ import argparse import hashlib import os import sys import tempfile import urllib.request import fontTools from fontTools.pens.svgPathPen import SVGPathPen from fontTools.pens.transformPen import TransformPen from fontTools.ttLib import TTFont from fontTools.varLib import instancer HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.normpath(os.path.join(HERE, "..", "..")) VENDORED = os.path.join(REPO, "umtool", "report-to-video", "fonts", "Archivo[wdth,wght].ttf") OUT = os.path.join(REPO, "common", "lib", "brandMediaGlyphs.ts") FONT_URL = "https://raw.githubusercontent.com/google/fonts/main/ofl/archivo/Archivo%5Bwdth%2Cwght%5D.ttf" WDTH = 118 # key, text, weight -- the three runs of plans/brand-and-themes.md "S4": the wordmark's # lead and suffix, and MEDIA. RUNS = [ ("archi", "Archi", 720), ("lyzer", "lyzer", 380), ("media", "MEDIA", 700), ] def num(v): """Font units to one decimal, without a trailing .0 -- stable text for the diff.""" r = round(float(v), 1) if r == int(r): return str(int(r)) return f"{r:.1f}" def glyph_path(glyphset, name): """The outline as SVG path data, y flipped so the baseline is 0 and down is +y.""" pen = SVGPathPen(glyphset, ntos=num) glyphset[name].draw(TransformPen(pen, (1, 0, 0, -1, 0, 0))) return pen.getCommands() def _value_x(v): return (getattr(v, "XAdvance", 0) or 0) if v is not None else 0 def _pair_subtables(font): """Every PairPos subtable of the GPOS 'kern' feature, in lookup order, unwrapped from Extension (type 9) lookups.""" if "GPOS" not in font: return [] gpos = font["GPOS"].table if not gpos.FeatureList or not gpos.LookupList: return [] indices = set() for rec in gpos.FeatureList.FeatureRecord: if rec.FeatureTag == "kern": indices.update(rec.Feature.LookupListIndex) subs = [] for li in sorted(indices): lookup = gpos.LookupList.Lookup[li] for sub in lookup.SubTable: kind = lookup.LookupType if kind == 9: kind = sub.ExtensionLookupType sub = sub.ExtSubTable if kind == 2: subs.append(sub) return subs def pair_kerning(font, left, right): """The XAdvance adjustment GPOS 'kern' applies to `left` followed by `right`. Resolved the way a shaper resolves one pair: the first subtable that covers `left` and answers for `right` wins. A format-1 subtable with no record for the pair passes; a format-2 (class) subtable that covers `left` always answers, zero included. """ for sub in _pair_subtables(font): cov = sub.Coverage.glyphs if left not in cov: continue if sub.Format == 1: for rec in sub.PairSet[cov.index(left)].PairValueRecord: if rec.SecondGlyph == right: return _value_x(rec.Value1) elif sub.Format == 2: c1 = sub.ClassDef1.classDefs.get(left, 0) c2 = sub.ClassDef2.classDefs.get(right, 0) return _value_x(sub.Class1Record[c1].Class2Record[c2].Value1) return 0 def run_data(varfont_path, text, wght): font = TTFont(varfont_path) inst = instancer.instantiateVariableFont(font, {"wght": wght, "wdth": WDTH}) cmap = inst.getBestCmap() hmtx = inst["hmtx"] glyphset = inst.getGlyphSet() glyphs = [] for ch in text: name = cmap[ord(ch)] g = inst["glyf"][name] g.recalcBounds(inst["glyf"]) # the instancer moves points; the stored bbox is the default master's # Ink extent: `left` / `right` from the glyph origin, and y down, so `top` is # how far it rises above the baseline (negative) and `bottom` how far it # descends (positive for a descender). top, bottom = (-g.yMax, -g.yMin) if g.numberOfContours else (0, 0) left, right = (g.xMin, g.xMax) if g.numberOfContours else (0, 0) glyphs.append({"char": ch, "name": name, "advance": hmtx[name][0], "left": left, "right": right, "top": top, "bottom": bottom, "d": glyph_path(glyphset, name)}) kerning = [pair_kerning(inst, a["name"], b["name"]) for a, b in zip(glyphs, glyphs[1:])] os2 = inst["OS/2"] return { "glyphs": glyphs, "kerning": kerning, "capHeight": os2.sCapHeight, "upem": inst["head"].unitsPerEm, } def render_ts(font_path): sha = hashlib.sha256(open(font_path, "rb").read()).hexdigest() runs = {key: run_data(font_path, text, wght) for key, text, wght in RUNS} upem = {r["upem"] for r in runs.values()} caps = {r["capHeight"] for r in runs.values()} assert len(upem) == 1, f"unitsPerEm differs across instances: {upem}" assert len(caps) == 1, f"cap height differs across weights: {caps}" out = [ "// GENERATED by common/bin/gen-media-glyphs.py -- DO NOT EDIT. Re-run it instead:", "// python3 common/bin/gen-media-glyphs.py", "// brandMedia.test.ts diffs this file against a fresh run -- under the fontTools version", "// recorded below; another version may round the instancer differently, so the test skips.", "//", "// The Archilyzer Media lockup's letters as outlines: Archivo[wdth,wght].ttf pinned to", f"// wdth {WDTH}, one instance per run. Font units, y down, baseline at 0; `kerning[i]` is", "// the GPOS pair adjustment between glyph i and glyph i + 1 of the same run.", "", "// `left` / `right` / `top` / `bottom`: the glyph's ink box from its origin, y down", "// (`top` < 0 is above the baseline).", "export type MediaGlyph = {", " char: string;", " advance: number;", " left: number;", " right: number;", " top: number;", " bottom: number;", " d: string;", "};", "export type MediaRun = {", " text: string;", " wght: number;", " glyphs: ReadonlyArray;", " kerning: ReadonlyArray;", "};", "", "export const MEDIA_GLYPH_SOURCE = {", ' font: "Archivo[wdth,wght].ttf",', f' sha256: "{sha}",', f" wdth: {WDTH},", f' fontTools: "{fontTools.version}",', "} as const;", "", f"export const MEDIA_UNITS_PER_EM = {upem.pop()};", f"export const MEDIA_CAP_HEIGHT = {caps.pop()};", "", "export const MEDIA_RUNS = {", ] for key, text, wght in RUNS: r = runs[key] out.append(f" {key}: {{") out.append(f' text: "{text}",') out.append(f" wght: {wght},") out.append(" glyphs: [") for g in r["glyphs"]: out.append( f' {{ char: "{g["char"]}", advance: {g["advance"]}, left: {g["left"]}, right: {g["right"]}, ' f'top: {g["top"]}, bottom: {g["bottom"]},' ) out.append(f' d: "{g["d"]}" }},') out.append(" ],") out.append(f" kerning: [{', '.join(str(k) for k in r['kerning'])}],") out.append(" },") out.append("} as const satisfies Record;") out.append("") return "\n".join(out) def main(): ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) ap.add_argument("--font", help="Archivo[wdth,wght].ttf to read (default: the umtool vendored copy)") ap.add_argument("--download", action="store_true", help="fetch google/fonts' copy into a temp dir") ap.add_argument("--stdout", action="store_true", help="print instead of writing " + os.path.relpath(OUT, REPO)) args = ap.parse_args() font = args.font or VENDORED if args.download: tmp = tempfile.mkdtemp(prefix="archivo-") font = os.path.join(tmp, "Archivo[wdth,wght].ttf") urllib.request.urlretrieve(FONT_URL, font) if not os.path.exists(font): sys.exit(f"no font at {font} -- pass --font or --download") ts = render_ts(font) if args.stdout: sys.stdout.write(ts) else: with open(OUT, "w", encoding="utf-8") as f: f.write(ts) print(f"wrote {os.path.relpath(OUT, REPO)}", file=sys.stderr) if __name__ == "__main__": main()