요구사항
Intuitive, Lightweight, Open Source
작업 흐름
자료 구조
plain text
font-pipeline/
├── 01_canvas.html # [Step 1: drawing2png] 브라우저 드로잉 웹앱
├── 02_png/ # Step 1 결과물 (0041.png 등 비트맵 덤프)
├── 03_svg/ # [Step 2: png2svg] VTracer로 추출된 벡터 패스
├── 04_build.py # [Step 2, 3, 4] 파이프라인 일괄 실행 스크립트
├── 05_dist/ # [Step 3, 4 최종물] TTF (설치용) 및 WOFF2 (웹 CDN용)
└── README.md단계별 상세 사양
Step 1: drawing2png (01_canvas.html)
- 입력: 브라우저 캔버스 내 마우스 / 스타일러스 펜 드로잉
- 기술 도구: HTML5 Canvas API (Vanilla JS)
- 출력 위치:
02_png/[Unicode_HEX].png(예:0041.png, 1000×1000, 32-bit RGBA) - 상세 규격 (1000 UPM 기준):
- 동작 방식:
Enter입력 시 가이드라인 레이어를 제외한 블랙(#000000) 획 레이어만 투명 배경 PNG로 자동 덤프 후 다음 글자로 전환.
Step 2: png2svg (04_build.py 내부 호출)
- 입력:
02_png/*.png - 기술 도구:
vtracer(Rust CLI) - 출력 위치:
03_svg/*.svg - 실행 명령:
- 동작 방식: 픽셀 손떨림 노이즈 필터링 후 단일
<path>형태의 벡터 패스 추출.
Step 3: svg2ttf (04_build.py 내부 처리)
- 입력:
03_svg/*.svg - 기술 도구: Python
fonttools(fontTools.fontBuilder+fontTools.pens.ttGlyphPen) - 출력 위치:
05_dist/MyFont-Regular.ttf - 동작 방식:
Step 4: ttf2woff2 (04_build.py 내부 호출)
- 입력:
05_dist/MyFont-Regular.ttf - 기술 도구:
pyftsubset(fonttools내장) +brotli - 출력 위치:
05_dist/MyFont.woff2 - 실행 명령:
- 동작 방식: 불필요한 메타데이터 제거 및 Brotli 압축을 적용하여 20~30KB 내외의 초경량 웹폰트 생성.
Optional: CDN 호스팅 및 엣지 배포
- 스토리지: Cloudflare R2 (
assets/fonts/MyFont.woff2) - 엔드포인트:
[https://cdn.mydomain.com/fonts/](https://cdn.mydomain.com/fonts/) - Cloudflare 설정:
Step 2, 3, 4 자동화 스크립트 (04_build.py)
plain text
#!/usr/bin/env python3
import os
import glob
import subprocess
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.svgLib.path import SVGPath
def main():
os.makedirs("./03_svg", exist_ok=True)
os.makedirs("./05_dist", exist_ok=True)
# Step 2: png2svg (vtracer)
print(">> [Step 2: png2svg] VTracer 실행 중...")
subprocess.run([
"vtracer",
"--input", "./02_png",
"--output", "./03_svg",
"--colormode", "bw",
"--filter_speckle", "4"
], check=True)
# Step 3: svg2ttf (fonttools)
print(">> [Step 3: svg2ttf] TTF 마스터 컴파일 중...")
fb = FontBuilder(unitsPerEm=1000, isTTF=True)
svg_files = sorted(glob.glob("./03_svg/*.svg"))
glyph_names = [".notdef"] + [f"uni{os.path.splitext(os.path.basename(f))[0].upper()}" for f in svg_files]
fb.setupGlyphOrder(glyph_names)
# (좌표계 변환: Canvas Y하향 -> Font Y상향 및 Glyph Pen 바인딩)
# ...
fb.save("./05_dist/MyFont-Regular.ttf")
# Step 4: ttf2woff2 (pyftsubset)
print(">> [Step 4: ttf2woff2] WOFF2 압축 변환 중...")
subprocess.run([
"pyftsubset", "./05_dist/MyFont-Regular.ttf",
"--unicodes=U+0020-007E",
"--flavor=woff2",
"--output-file=./05_dist/MyFont.woff2"
], check=True)
print(">> 전체 빌드 완료: 05_dist/MyFont-Regular.ttf, 05_dist/MyFont.woff2")
if __name__ == "__main__":
main()