www.git

www

~ Personal site — projects, posts, about. Built with sqzass.
git clone https://github.com/debuman/www.git

tools/fonts.py (4188B)

      1 #!/usr/bin/env python3
      2 """디스플레이 서체를 이 사이트가 실제로 쓰는 글자만큼으로 줄인다.
      3 
      4 한글 웹폰트는 통째로 받으면 2MB 급이다. 그래서 두 가지를 한다.
      5 
      6 1. **라틴과 한글을 따로 낸다.** `unicode-range` 가 붙어 있으면 브라우저는 그
      7    페이지에 실제로 있는 문자에 해당하는 파일만 받는다. 영문 페이지는 한글
      8    파일을 아예 받지 않는다.
      9 2. **한글은 쓰는 글자만 남긴다.** 상용 2350자를 다 넣으면 1MB 인데, 이 사이트가
     10    쓰는 것은 그 4분의 1도 안 된다.
     11 
     12 2 번의 대가는 명확하다 — **콘텐츠에 새 글자가 들어오면 폰트를 다시 만들어야
     13 한다.** 잊으면 그 글자만 시스템 폰트로 떨어져 조용히 어긋난다. 그래서
     14 `--check` 가 있고, CI 가 빌드 전에 그걸 돌린다. 빠진 글자는 경고가 아니라
     15 에러다.
     16 
     17     python3 tools/fonts.py            # 내려받고 서브셋을 다시 만든다
     18     python3 tools/fonts.py --check    # 커버리지만 확인한다 (네트워크 불필요)
     19 """
     20 
     21 import subprocess
     22 import sys
     23 import urllib.request
     24 from pathlib import Path
     25 
     26 ROOT = Path(__file__).resolve().parent.parent
     27 OUT = ROOT / "static" / "fonts"
     28 CACHE = Path("/tmp/sqzer-fonts")
     29 
     30 # Noto Sans KR Variable. wght 100~900 이 한 파일에 있어서, 초경량 제목부터
     31 # 굵은 라벨까지 파일 하나로 낸다.
     32 SRC_URL = "https://github.com/google/fonts/raw/main/ofl/notosanskr/NotoSansKR%5Bwght%5D.ttf"
     33 SRC = CACHE / "NotoSansKR.ttf"
     34 
     35 # 라틴·구두점·기호. 한글이 한 글자도 없는 페이지가 받는 전부.
     36 LATIN = (
     37     "U+0020-007E,U+00A0-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+2000-206F,"
     38     "U+20A0-20BF,U+2122,U+2190-21BB,U+2212,U+2500-257F,U+25A0-25FF,U+2605-2606"
     39 )
     40 # 한글 음절·자모·전각. 실제 쓰는 글자만 남기므로 범위가 아니라 --text 로 준다.
     41 JAMO = "U+3130-318F,U+FF01-FF60"
     42 
     43 
     44 def used_hangul() -> str:
     45     """content/ · templates/ · i18n/ 에 실제로 등장하는 한글 음절."""
     46     chars = set()
     47     for sub in ("content", "templates", "i18n"):
     48         for path in (ROOT / sub).rglob("*"):
     49             if path.suffix not in (".md", ".html", ".toml"):
     50                 continue
     51             for ch in path.read_text(encoding="utf-8"):
     52                 if "가" <= ch <= "힣":
     53                     chars.add(ch)
     54     return "".join(sorted(chars))
     55 
     56 
     57 def subset(args: list[str], out: Path) -> None:
     58     subprocess.run(
     59         [sys.executable, "-m", "fontTools.subset", str(SRC),
     60          "--flavor=woff2", "--layout-features=*", f"--output-file={out}", *args],
     61         check=True,
     62     )
     63     print(f"  {out.name:24} {out.stat().st_size // 1024:>4} KB")
     64 
     65 
     66 def check() -> int:
     67     """폰트가 콘텐츠의 모든 한글을 담고 있는지. 하나라도 없으면 에러."""
     68     from fontTools.ttLib import TTFont
     69 
     70     target = OUT / "display-kr.woff2"
     71     if not target.exists():
     72         print(f"{target} 가 없습니다 — python3 tools/fonts.py 를 먼저 돌리세요", file=sys.stderr)
     73         return 1
     74 
     75     covered = set()
     76     for table in TTFont(target)["cmap"].tables:
     77         covered |= set(table.cmap)
     78 
     79     missing = sorted(ch for ch in used_hangul() if ord(ch) not in covered)
     80     if missing:
     81         print(
     82             f"폰트에 없는 글자 {len(missing)}자: {''.join(missing[:40])}"
     83             f"{'…' if len(missing) > 40 else ''}\n"
     84             "python3 tools/fonts.py 로 서브셋을 다시 만드세요.",
     85             file=sys.stderr,
     86         )
     87         return 1
     88     print(f"ok — 한글 {len(used_hangul())}자 전부 포함")
     89     return 0
     90 
     91 
     92 def build() -> int:
     93     OUT.mkdir(parents=True, exist_ok=True)
     94     CACHE.mkdir(parents=True, exist_ok=True)
     95     if not SRC.exists():
     96         print(f"내려받는 중: {SRC_URL}")
     97         urllib.request.urlretrieve(SRC_URL, SRC)
     98 
     99     hangul = used_hangul()
    100     print(f"쓰는 한글 {len(hangul)}자")
    101     subset([f"--unicodes={LATIN}"], OUT / "display-latin.woff2")
    102     subset([f"--unicodes={JAMO}", f"--text={hangul}"], OUT / "display-kr.woff2")
    103     return 0
    104 
    105 
    106 if __name__ == "__main__":
    107     sys.exit(check() if "--check" in sys.argv else build())