#!/usr/bin/env python3 """Generate the aspect-adjusted Montserrat webfonts used by the site. The design calls for the typeface to be 110% as tall and 85% as wide as the original Montserrat. CSS cannot do this: `font-stretch` only *selects* a width variant from a family, it never scales, and Montserrat has no width axis (its variable build only has `wght`). So the adjustment is baked into the font files themselves. Scaling a font's aspect ratio means changing the relationship between the glyph *outlines* and the *advance widths*. If you scale the shapes but not the advances, the browser still reserves the original width and every line looks loosely spaced and wraps too early. So this script scales, in order: 1. every glyph outline coordinate (x * 0.85, y * 1.10) 2. every composite glyph component offset (e.g. the accent on "a-umlaut") 3. every advance width and side bearing (x * 0.85) <- affects layout 4. every vertical metric (ascent, descent, capHeight, xHeight) (y * 1.10) Non-uniform scaling invalidates TrueType hinting, so the hinting programs and the pixel-metric tables that reference the old coordinates are dropped. Usage: pip install "fonttools[woff]" # or: python3 -m venv .venv && ... python3 tools/rescale-aspect.py Pristine originals are cached in tools/_originals/ (git-ignored) and downloaded from a PINNED Fontsource version, so re-running always yields byte-identical output. The cache is disposable: delete it any time. """ import sys import urllib.request from pathlib import Path try: from fontTools.ttLib import TTFont from fontTools.ttLib.tables.ttProgram import Program except ImportError: sys.exit('fontTools is missing. Install it with: pip install "fonttools[woff]"') # ---- design parameters ---------------------------------------------------- SCALE_X = 0.85 # width -> 85% SCALE_Y = 1.10 # height -> 110% # Only the weights the site actually uses. Each unused weight is a file # nobody downloads, so there is no reason to ship it. WEIGHTS = (400, 700) # Pinned so the output never silently changes under you. FONTSOURCE_VERSION = "5.3.0" CDN = f"https://cdn.jsdelivr.net/fontsource/fonts/montserrat@{FONTSOURCE_VERSION}" ROOT = Path(__file__).resolve().parent.parent CACHE = ROOT / "tools" / "_originals" OUT = ROOT / "site" / "fonts" # Tables whose contents are tied to the old coordinate space. STALE_TABLES = ("hdmx", "LTSH", "VDMX", "gasp") def fetch_original(weight: int) -> Path: """Download the pristine woff2 into the cache if it isn't there yet.""" CACHE.mkdir(parents=True, exist_ok=True) dest = CACHE / f"montserrat-latin-{weight}-normal.woff2" if not dest.exists(): url = f"{CDN}/latin-{weight}-normal.woff2" print(f" downloading {url}") with urllib.request.urlopen(url) as response: dest.write_bytes(response.read()) return dest def transform(src: Path, dst: Path) -> None: font = TTFont(src) glyf = font["glyf"] order = font.getGlyphOrder() # 1 + 2. outlines and composite offsets for name in order: g = glyf[name] if g.isComposite(): # Every level's offset is scaled; nesting composes correctly # because (0.85*a + 0.85*b) == 0.85 * (a + b). for comp in g.components: comp.x = round(comp.x * SCALE_X) comp.y = round(comp.y * SCALE_Y) elif g.numberOfContours > 0: # Affine-scaling every point of a quadratic outline keeps it a # valid quadratic, so no curve conversion is needed. coords = g.coordinates for i in range(len(coords)): x, y = coords[i] coords[i] = (x * SCALE_X, y * SCALE_Y) g.program = Program() # hinting no longer matches the coordinates # 3. horizontal metrics -- this is the part that makes layout reflow hmtx = font["hmtx"] for name in order: advance, lsb = hmtx[name] hmtx[name] = (round(advance * SCALE_X), round(lsb * SCALE_X)) # 4. vertical metrics, so the line box grows to match the taller glyphs hhea, os2 = font["hhea"], font["OS/2"] for table, attr in ((hhea, "ascent"), (hhea, "descent"), (hhea, "lineGap"), (os2, "sTypoAscender"), (os2, "sTypoDescender"), (os2, "sTypoLineGap"), (os2, "usWinAscent"), (os2, "usWinDescent"), (os2, "sCapHeight"), (os2, "sxHeight")): value = getattr(table, attr, None) if value is not None: setattr(table, attr, round(value * SCALE_Y)) for table in STALE_TABLES: if table in font: del font[table] font.recalcBBoxes = True font.save(dst) font.close() def report(path: Path) -> None: f = TTFont(path) cmap = f.getBestCmap() r = f["glyf"][cmap[ord("R")]] umlaut = f["glyf"][cmap[ord("รค")]] print(f" {path.name} ({path.stat().st_size:,} bytes)") print(f" 'R' advance={f['hmtx']['R'][0]:4d} capHeight={f['OS/2'].sCapHeight}" f" xHeight={f['OS/2'].sxHeight}") print(f" umlauts still composite: {umlaut.isComposite()}" f" glyphs: {f['maxp'].numGlyphs}") f.close() if __name__ == "__main__": print(f"Montserrat {FONTSOURCE_VERSION} -> height {SCALE_Y:.0%}, width {SCALE_X:.0%}\n") OUT.mkdir(parents=True, exist_ok=True) for weight in WEIGHTS: original = fetch_original(weight) target = OUT / f"montserrat-latin-{weight}-aspect.woff2" transform(original, target) report(target) print("\nDone. Point your @font-face src: at the *-aspect.woff2 files.")