Files
moli/scripts/generate-layout-test-font.py
ldm0 09257c15cf refactor(fonts): extract system font matching crate
Move Fontconfig matching, substitution policy, and the Fontique lookup cache into moli-system-fonts. Keep CSS family adaptation in moli-layout, migrate tests and fixtures, and retain the Skia notice in source comments.
2026-09-12 04:13:15 +08:00

156 lines
5.0 KiB
Python

# /// script
# requires-python = ">=3.11"
# dependencies = [
# "brotli==1.1.0",
# "fonttools==4.59.0",
# ]
# ///
"""Generate deterministic fonts for layout and system font matching tests."""
import hashlib
import os
from pathlib import Path
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.subset import Options, Subsetter
from fontTools.ttLib import TTFont
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "moli-layout" / "tests" / "fixtures"
TTF_PATH = FIXTURES / "moli-ahem.ttf"
SYSTEM_FONT_TTF_PATH = ROOT / "moli-system-fonts" / "tests" / "fixtures" / "moli-ahem.ttf"
HEBREW_EMOJI_PATH = FIXTURES / "moli-hebrew-emoji.ttf"
CJK_PATH = FIXTURES / "moli-cjk.ttf"
DEJAVU_SOURCE = Path(
os.environ.get(
"MOLI_DEJAVU_FONT_SOURCE",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
)
)
DROID_CJK_SOURCE = Path(
os.environ.get(
"MOLI_DROID_CJK_FONT_SOURCE",
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
)
)
DEJAVU_SOURCE_SHA256 = "57f73e11f51999432bf7ab22ce55b6f945d5eca1bf824404cfa9ec2e3718c84e"
DROID_CJK_SOURCE_SHA256 = "acb6440a713d880a13a21b468ba7cd43f5a2b2934972e51be791c880730777b8"
# OpenType stores seconds since 1904-01-01. This value is Unix epoch and avoids
# FontTools treating a deliberately deterministic zero as a Unix timestamp.
OPEN_TYPE_UNIX_EPOCH = 2_082_844_800
def empty_glyph():
pen = TTGlyphPen(None)
return pen.glyph()
def box_glyph():
pen = TTGlyphPen(None)
pen.moveTo((0, 0))
pen.lineTo((600, 0))
pen.lineTo((600, 800))
pen.lineTo((0, 800))
pen.closePath()
return pen.glyph()
def build_ttf() -> None:
builder = FontBuilder(1000, isTTF=True)
codepoints = list(range(0x21, 0x7F))
codepoints += [0x00A0, 0x00DF]
glyph_names = {
codepoint: (
f"uni{codepoint:04X}" if codepoint <= 0xFFFF else f"u{codepoint:05X}"
)
for codepoint in codepoints
}
glyph_order = [".notdef", "space", *glyph_names.values()]
builder.setupGlyphOrder(glyph_order)
# Keep glyph identities distinct so source/glyph assertions can distinguish
# transformed characters without depending on a platform font.
cmap = dict(glyph_names)
cmap[0x20] = "space"
builder.setupCharacterMap(cmap)
glyphs = {name: box_glyph() for name in glyph_names.values()}
glyphs.update({".notdef": box_glyph(), "space": empty_glyph()})
builder.setupGlyf(glyphs)
builder.setupHorizontalMetrics({glyph: (600, 0) for glyph in glyph_order})
builder.setupHorizontalHeader(ascent=800, descent=-200, lineGap=0)
builder.setupNameTable(
{
"familyName": "Moli Ahem",
"styleName": "Regular",
"uniqueFontIdentifier": "Moli Ahem Regular 1.0",
"fullName": "Moli Ahem Regular",
"psName": "MoliAhem-Regular",
"version": "Version 1.000",
}
)
builder.setupOS2(
sTypoAscender=800,
sTypoDescender=-200,
sTypoLineGap=0,
usWinAscent=800,
usWinDescent=200,
sxHeight=800,
sCapHeight=800,
fsSelection=1 << 6,
)
builder.setupPost()
builder.setupMaxp()
builder.setupHead(created=OPEN_TYPE_UNIX_EPOCH, modified=OPEN_TYPE_UNIX_EPOCH)
builder.save(TTF_PATH)
def verify_source(source: Path, expected_sha256: str) -> None:
if not source.is_file():
raise SystemExit(f"required fixed-font source does not exist: {source}")
actual_sha256 = hashlib.sha256(source.read_bytes()).hexdigest()
if actual_sha256 != expected_sha256:
raise SystemExit(
f"fixed-font source hash mismatch for {source}: "
f"expected {expected_sha256}, got {actual_sha256}"
)
def subset_font(source: Path, output: Path, codepoints: list[int]) -> None:
font = TTFont(source, recalcTimestamp=False)
options = Options()
options.recalc_timestamp = False
options.retain_gids = False
options.name_IDs = [0, 1, 2, 3, 4, 5, 6, 13, 14]
subsetter = Subsetter(options=options)
subsetter.populate(unicodes=codepoints)
subsetter.subset(font)
font["head"].created = OPEN_TYPE_UNIX_EPOCH
font["head"].modified = OPEN_TYPE_UNIX_EPOCH
font.save(output, reorderTables=False)
def convert(flavor: str) -> None:
font = TTFont(TTF_PATH, recalcTimestamp=False)
font.flavor = flavor
font.save(FIXTURES / f"moli-ahem.{flavor}", reorderTables=False)
def main() -> None:
FIXTURES.mkdir(parents=True, exist_ok=True)
verify_source(DEJAVU_SOURCE, DEJAVU_SOURCE_SHA256)
verify_source(DROID_CJK_SOURCE, DROID_CJK_SOURCE_SHA256)
build_ttf()
SYSTEM_FONT_TTF_PATH.parent.mkdir(parents=True, exist_ok=True)
SYSTEM_FONT_TTF_PATH.write_bytes(TTF_PATH.read_bytes())
convert("woff")
convert("woff2")
subset_font(DEJAVU_SOURCE, HEBREW_EMOJI_PATH, [0x20, 0x05D0, 0x05D1, 0x1F600])
subset_font(DROID_CJK_SOURCE, CJK_PATH, [0x20, 0x4E2D, 0x6587])
if __name__ == "__main__":
main()