mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-19 02:20:02 -05:00
Upload, install, convert and manage PS5 game packages over the network. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
163 lines
5.5 KiB
Python
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()
|