Files
font/scripts/set-fontinfo.py
zeekay 9bb1a93a39 license: the built faces carry the upstream notice, like the exports already did
LICENSE.txt at the root lists both holders — Vercel/basement.studio for Geist
and Hanzo AI for Zen — and packages/zen/scripts/name.py writes both onto the
hand-exported Pixel cuts, its docstring stating the reason: OFL §2 binds each
copy of the Font Software to the notice, and a .woff2 served to a browser is a
copy travelling alone.

The fontmake-built families took the other path. Their fields come from the
Glyphs sources through scripts/set-fontinfo.py, and that wrote our line only —
so Zen and Zen Mono, the two faces every Hanzo surface actually loads, shipped
without the notice the licence beside them carries. Same repo, two answers, and
the wrong one was on the faces in production.

set-fontinfo.py now writes both, so a rebuild produces it directly.
packages/zen/scripts/notice.py repairs the binaries already built and
committed; it is idempotent and reports what it changed. 199 of 219 faces
gained the line — the 20 that did not are the Pixel cuts name.py had already
handled.

Family, uniqueID, version, manufacturer, designer and the vendor URLs are
untouched: those are ours, and §3 reserves nothing here — the upstream block
declares no Reserved Font Name.
2026-08-25 12:40:52 -07:00

71 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""Set the attribution fields in every Glyphs source.
These fields land in the built font's `name` table and OS/2 `achVendID`, so
they travel with a bare .woff2 wherever it is served. Run after editing a
source in Glyphs, which rewrites the file from its own UI fields.
nameID 0 carries the upstream copyright line above ours, for the reason
`packages/zen/scripts/name.py` already states about the hand exports: OFL §2
binds each copy of the Font Software to the notice, and a .woff2 served to a
browser IS a copy travelling alone. Family, uniqueID, version, manufacturer,
designer and the vendor URLs are not the notice — those are ours. LICENSE.txt
at the root already lists both holders; this keeps the binaries agreeing with it.
"""
import re
import sys
from pathlib import Path
FIELDS = {
"copyrights": (
"Copyright 2024 The Geist Project Authors "
"(https://github.com/vercel/geist-font)\n"
"Copyright 2026 Hanzo AI, Inc. (https://git.hanzo.ai/hanzoai/font)"
),
"designers": "Hanzo AI, Inc.",
"designerURL": "https://hanzo.ai",
"manufacturers": "Hanzo AI, Inc.",
"manufacturerURL": "https://hanzo.ai",
"vendorID": "HNZO",
}
SOURCES = Path(__file__).resolve().parent.parent / "sources"
def patch(text, key, value):
"""Replace the first `value = ...;` after `key = <key>;`.
Glyphs writes two shapes: a bare `value` for scalars, and a `values = (…)`
list of per-language entries. Both begin with the same `value =` token, so
one pattern covers each.
"""
pattern = re.compile(
r"(key = " + re.escape(key) + r";(?:.*?\n){0,6}?\s*value = )"
r'("(?:[^"\\]|\\.)*"|[^;\n]+)(;)',
re.DOTALL,
)
new, n = pattern.subn(lambda m: m.group(1) + f'"{value}"' + m.group(3),
text, count=1)
if n != 1:
raise SystemExit(f"{key}: expected 1 match, got {n}")
return new
def main():
files = sorted(SOURCES.glob("*.glyphspackage/fontinfo.plist"))
if not files:
raise SystemExit(f"no sources under {SOURCES}")
for path in files:
text = original = path.read_text(encoding="utf-8")
for key, value in FIELDS.items():
text = patch(text, key, value)
if text != original:
path.write_text(text, encoding="utf-8")
print(f"patched {path.relative_to(SOURCES.parent)}")
else:
print(f"unchanged {path.relative_to(SOURCES.parent)}")
if __name__ == "__main__":
sys.exit(main())