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())