#!/usr/bin/env python3 """生成 vendor/fonts/manifest.json。 数据源:vendor/fonts/tools/fonts.conf(字体元数据)+ vendor/fonts/*.woff2(实际产物)。 每次跑完 subset-fonts.sh 会自动调用;也可单独运行: python3 vendor/fonts/tools/gen-manifest.py """ from __future__ import annotations import hashlib import json import subprocess import sys from datetime import date from pathlib import Path HERE = Path(__file__).resolve().parent OUT_DIR = HERE.parent ROOT = OUT_DIR.parent.parent # 仓库根,用于生成 vendor/fonts/... 形式的相对路径 CONF = HERE / "fonts.conf" WEIGHTS = (("regular", 400), ("bold", 700)) SUBSET_DESC = "ASCII + GB2312 一级/二级汉字 + 中文标点 + 数字符号" def charset_count() -> int: """复用 subset-fonts.sh 的字符集生成逻辑,保证清单与产物一致。""" code = ( "chars=set()\n" "for c in range(0x20,0x7F): chars.add(chr(c))\n" "for hi in range(0xA1,0xFF):\n" " for lo in range(0xA1,0xFF):\n" " try: chars.add(bytes((hi,lo)).decode('gb2312'))\n" " except UnicodeDecodeError: pass\n" "chars.update(' ·—…‘’“”※→←↑↓√×÷±≈≠≤≥∞°℃℉¥€£©®™№§¶†‡•‰′″〇')\n" "chars.update('「」『』【】〔〕〖〗《》〈〉')\n" "chars.update('0123456789')\n" "chars.update('!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~')\n" "print(len(chars))\n" ) return int(subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, check=True).stdout) def human(n: int) -> str: return f"{n / 1024 / 1024:.2f} MB" if n >= 1024 * 1024 else f"{n / 1024:.0f} KB" def read_conf() -> list[dict[str, str]]: rows = [] for line in CONF.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#"): continue fid, name, family, category, license_, source, reg, bold = (p.strip() for p in line.split("|")) rows.append( { "id": fid, "displayName": name, "fontFamily": family, "category": category, "license": license_, "source": source, "regular": reg, "bold": bold, } ) return rows def main() -> int: fonts = [] grand_total = 0 for row in read_conf(): weights = [] font_total = 0 for weight_name, weight_class in WEIGHTS: path = OUT_DIR / f"{row['id']}-{weight_name}.woff2" if not path.is_file(): print(f"缺少产物: {path},请先运行 subset-fonts.sh", file=sys.stderr) return 1 data = path.read_bytes() size = len(data) font_total += size weights.append( { "weight": weight_class, "style": "normal", "file": path.name, "path": str(path.relative_to(ROOT)), "bytes": size, "size": human(size), "sha256": hashlib.sha256(data).hexdigest(), } ) grand_total += font_total fonts.append( { "id": row["id"], "displayName": row["displayName"], "fontFamily": row["fontFamily"], "cssVar": f"--font-{row['id']}", "category": row["category"], "license": row["license"], "source": row["source"], "totalBytes": font_total, "totalSize": human(font_total), "weights": weights, } ) manifest = { "version": 1, "generatedAt": date.today().isoformat(), "generator": "vendor/fonts/tools/subset-fonts.sh", "subset": { "charset": SUBSET_DESC, "charCount": charset_count(), "format": "woff2", "layoutFeatures": "kern,liga,clig,calt,ccmp,locl,mark,mkmk", }, "totalBytes": grand_total, "totalSize": human(grand_total), "fonts": fonts, } out = OUT_DIR / "manifest.json" out.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(f"manifest.json: {len(fonts)} 套字体,{len(fonts) * 2} 个文件,合计 {human(grand_total)}") return 0 if __name__ == "__main__": raise SystemExit(main())