Files
ps5upload/scripts/bundle-fonts.py
T
phantomptrandClaude Opus 5.5 c14782944d PS5Upload v5.35.0
Upload, install, convert and manage PS5 game packages over the network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 04:31:13 -07:00

163 lines
5.5 KiB
Python

# scripts/bundle-fonts.py
#
# Download Noto font subsets from Google Fonts and bundle them into
# the Tauri desktop client so CJK + Indic scripts render without a
# network fetch at runtime. Outputs woff2 files into
# `client/src/assets/fonts/` and a companion `client/src/fonts.css`
# with @font-face rules that reference them via Vite's import-
# relative URLs.
#
# Run from the repo root: python3 scripts/bundle-fonts.py
#
# The current build doesn't import fonts.css from main.tsx by
# default — system fonts cover the English UI. Run this script
# (and accept the injected import) when you're ready to ship the
# broader i18n font coverage.
import os
import re
import urllib.request
import urllib.parse
from urllib.error import URLError, HTTPError
REPO_ROOT = os.getcwd()
OUT_DIR = os.path.join(REPO_ROOT, "client", "src", "assets", "fonts")
CSS_OUT = os.path.join(REPO_ROOT, "client", "src", "fonts.css")
MAIN_TSX = os.path.join(REPO_ROOT, "client", "src", "main.tsx")
FAMILIES = [
("Noto Sans", [400, 500, 600, 700]),
("Noto Sans Devanagari", [400, 500, 600, 700]),
("Noto Sans Bengali", [400, 500, 600, 700]),
("Noto Sans Thai", [400, 500, 600, 700]),
("Noto Sans KR", [400, 500, 600, 700]),
]
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
"Accept": "text/css,*/*;q=0.1",
}
def fetch_text(url: str) -> str:
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req) as res:
return res.read().decode("utf-8")
def fetch_binary(url: str) -> bytes:
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req) as res:
return res.read()
def slugify(name: str) -> str:
return re.sub(r"(^-|-$)", "", re.sub(r"[^a-z0-9]+", "-", name.lower()))
def ensure_fonts_import() -> None:
"""Inject `import "./fonts.css"` into main.tsx right after the
existing `./index.css` import. Idempotent — re-running is a
no-op if the import is already present. Skips silently when
main.tsx isn't at the expected path (dev running from a branch
with a different layout)."""
if not os.path.exists(MAIN_TSX):
return
with open(MAIN_TSX, "r", encoding="utf-8") as f:
text = f.read()
if './fonts.css' in text:
return
anchor = 'import "./index.css";'
if anchor not in text:
return
text = text.replace(anchor, f'{anchor}\nimport "./fonts.css";')
with open(MAIN_TSX, "w", encoding="utf-8") as f:
f.write(text)
def ext_from_format(fmt: str, url: str) -> str:
fmt = fmt.lower()
if "woff2" in fmt:
return "woff2"
if "woff" in fmt:
return "woff"
if "truetype" in fmt or "ttf" in fmt:
return "ttf"
if "opentype" in fmt or "otf" in fmt:
return "otf"
url_ext = os.path.splitext(url.split("?")[0])[1].lstrip(".")
return url_ext or "bin"
def main() -> None:
os.makedirs(OUT_DIR, exist_ok=True)
url_to_file = {}
css_blocks = []
total_blocks = 0
for family, weights in FAMILIES:
weights_str = ";".join(str(w) for w in weights)
family_param = urllib.parse.quote(family).replace("%20", "+")
url = (
"https://fonts.googleapis.com/css2?family="
f"{family_param}:wght@{weights_str}&display=swap"
)
try:
css = fetch_text(url)
except (HTTPError, URLError) as exc:
print(f"Failed to fetch CSS for {family}: {exc}")
continue
blocks = re.findall(r"@font-face\s*\{[\s\S]*?\}", css)
if not blocks:
print(f"No @font-face blocks for {family}. First 200 chars:\n{css[:200]}")
for block in blocks:
total_blocks += 1
match = re.search(
r"url\(([^)]+)\)\s*format\(['\"]([^'\"]+)['\"]\)",
block,
)
if not match:
continue
font_url = match.group(1)
font_fmt = match.group(2)
filename = url_to_file.get(font_url)
if not filename:
weight_match = re.search(r"font-weight:\s*([0-9]+)", block)
weight = weight_match.group(1) if weight_match else "400"
style_match = re.search(r"font-style:\s*([^;]+)", block)
style = style_match.group(1).strip() if style_match else "normal"
family_slug = slugify(family)
index = len(url_to_file) + 1
ext = ext_from_format(font_fmt, font_url)
filename = f"{family_slug}-w{weight}-{style}-{index}.{ext}"
try:
font_data = fetch_binary(font_url)
except (HTTPError, URLError) as exc:
print(f"Failed to fetch font {font_url}: {exc}")
continue
with open(os.path.join(OUT_DIR, filename), "wb") as f:
f.write(font_data)
url_to_file[font_url] = filename
local_block = block.replace(font_url, f"../assets/fonts/{filename}")
css_blocks.append(local_block)
header = "/* Bundled fonts for multilingual UI */\n"
with open(CSS_OUT, "w", encoding="utf-8") as f:
f.write(header + "\n\n".join(css_blocks) + "\n")
ensure_fonts_import()
print(
f"Done. Fonts: {len(url_to_file)} files, "
f"{len(css_blocks)} @font-face blocks (parsed {total_blocks} blocks)."
)
if __name__ == "__main__":
main()