gpdf에서 Noto Sans JP를 사용하려면?
gpdf.WithFont에 static 버전 NotoSansJP-Regular.ttf를 등록합니다. variable 폰트를 쓰지 않는 이유와 17,000개 글리프가 PDF에서 40 KB 미만까지 줄어드는 서브셋 구조를 설명합니다.
질문을 다시 정리하면
gpdf 문서에 일본어를 렌더링하려 하고, 폰트는 Noto Sans JP — Google이 배포하는 SIL OFL 라이선스의 고딕체, JIS 영역을 완전히 커버하는 그 폰트 — 를 쓰기로 정했다. Google Fonts의 zip은 이미 받았다. 여기서부터 알고 싶은 세 가지: 어느 파일을 고를지, 어느 굵기를 등록할지, zip 안에 숨어 있는 한 가지 함정은 무엇인지.
결론 (TL;DR)
zip을 풀면 static/NotoSansJP-Regular.ttf 를 씁니다 — zip 루트의 variable 폰트가 아닙니다. 이 파일을 gpdf.WithFont("NotoSansJP", bytes)에 넘기고 기본 폰트로 지정하면 끝. gpdf는 약 17,000개 글리프 중 실제로 렌더링된 글리프만 서브셋해서 PDF에 포함합니다 — 일반적인 청구서 한 장은 최종 PDF에 20–40 KB 정도의 폰트 데이터를 담게 됩니다.
완전한 예제
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
font, err := os.ReadFile("NotoSansJP-Regular.ttf")
if err != nil {
log.Fatal(err)
}
doc := gpdf.NewDocument(
gpdf.WithPageSize(gpdf.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
gpdf.WithFont("NotoSansJP", font),
gpdf.WithDefaultFont("NotoSansJP", 11),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("請求書", template.FontSize(28), template.Bold())
c.Text("Noto Sans JP、これで十分。")
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("invoice.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
Google Fonts에서 zip을 받아 풀고, static/NotoSansJP-Regular.ttf를 main.go 옆에 두고 go run main.go를 실행하면 한 페이지짜리 PDF가 나옵니다.
variable 폰트가 아닌 static TTF를 고르기
Google Fonts에서 Get font → Download all, zip 압축을 풀면 보기에 비슷하지만 성격이 전혀 다른 두 그룹이 보입니다:
- zip 루트의
NotoSansJP-VariableFont_wght.ttf— weight 100–900을 한 파일에 담은 variable 폰트, 약 7 MB static/디렉토리 —NotoSansJP-Thin.ttf부터NotoSansJP-Black.ttf까지 굵기별로 분리된 9개의 TTF, 각 약 5 MB
static/ 쪽을 쓰세요.
gpdf의 TrueType 파서는 일부러 범위를 좁게 잡아 뒀습니다. 글리프 아웃라인, 복합 글리프, cmap, hmtx — 고정 굵기 텍스트를 렌더링하는 데 필요한 테이블은 다 다룹니다. 하지만 variable 폰트를 진짜 가변적으로 만드는 fvar / gvar / HVAR 테이블은 읽지 않습니다. VariableFont_wght.ttf를 넘기면 파서가 깔끔히 에러를 내거나, 운이 나쁘면 기본 인스턴스의 글리프만 뽑고 당신이 지정했다고 생각한 weight 축을 조용히 무시합니다.
파일 크기 측면에서도 static 쪽이 유리합니다. variable 폰트는 weight 축 위의 모든 인스턴스 아웃라인을 한 파일에 담는 — 그게 설계 의도입니다. Regular만 쓴다면 나머지 8개 weight 만큼의 데이터를 그냥 실어 나르게 됩니다. static Regular가 5 MB, variable이 7 MB. 서브셋으로 둘 다 줄어들긴 하지만 입력은 static이 깔끔합니다.
핵심은 이 네 줄
의미 있는 건 생성자 옵션뿐입니다:
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", font),
gpdf.WithDefaultFont("NotoSansJP", 11),
)
폰트 패밀리명 ("NotoSansJP") 은 임의로 정해도 됩니다. gpdf는 이를 조회 키로만 씁니다 — 파일 경로도 아니고, 폰트 메타데이터에서 읽는 이름도 아닙니다. 팀에서 "body"나 "jp", "Noto"가 더 읽기 좋다면 그걸 쓰세요. 나중에 template.FontFamily(...)에 같은 이름을 넘기기만 하면 됩니다.
WithDefaultFont는 매번 c.Text 호출에 template.FontFamily("NotoSansJP")를 쓰지 않게 해 줍니다. 이걸 빼면 gpdf는 Helvetica로 폴백하는데, Helvetica는 CJK 코드포인트를 하나도 커버하지 않아서 — 제목만 제대로 나오고 본문 전체가 두부 네모 (□□□)가 된 PDF가 나옵니다. 왜 제목만 멀쩡한지 한 시간쯤 헤매게 됩니다.
어느 굵기를 등록해야 할까
청구서·영수증·업무용 리포트라면 Regular와 Bold 두 개면 충분합니다:
reg, _ := os.ReadFile("NotoSansJP-Regular.ttf")
bold, _ := os.ReadFile("NotoSansJP-Bold.ttf")
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", reg),
gpdf.WithFont("NotoSansJP-Bold", bold),
gpdf.WithDefaultFont("NotoSansJP", 11),
)
-Bold 서픽스로 등록하면 template.Bold()가 자동으로 잡습니다. -Italic, -BoldItalic도 같은 규약. 다만 Noto Sans JP에는 이탤릭이 없습니다 — CJK 폰트는 자연스러운 기울임 형태가 없어서 Noto 계열에도 이탤릭이 존재하지 않습니다. 일본어에서 강조가 필요하면 색상·크기·굵기로 대체하세요.
브로슈어나 헤드라인에 Medium이나 SemiBold가 필요하면 원하는 서픽스로 등록하고 패밀리명으로 직접 참조하면 됩니다:
gpdf.WithFont("NotoSansJP-Medium", medium)
// ...
c.Text("見出し", template.FontFamily("NotoSansJP-Medium"))
서픽스 기반 Bold/Italic 숏컷은 -Bold / -Italic / -BoldItalic 이 세 개에만 자동 연결됩니다. 나머지는 패밀리명으로 명시적으로 부릅니다.
서브셋 후의 실제 크기
Noto Sans JP Regular는 디스크에서 약 5 MB. 이 숫자를 보고 별도 폰트 CDN을 꾸리거나 PDF 후처리로 폰트를 빼내는 팀이 가끔 있는데, gpdf에서는 둘 다 필요 없습니다.
실제로 PDF에 들어가는 양은 이 정도:
| 문서 | 사용 글리프 | PDF 내 폰트 데이터 |
|---|---|---|
| 한 줄 영수증 (~15자) | ~14 | ~11 KB |
| 일반 청구서 (~200자) | ~80 | ~28 KB |
| 10페이지 리포트 (~8,000자) | ~900 | ~180 KB |
| 사전 수준 풀셋 (JIS Level 1) | ~6,800 | ~2.1 MB |
(gpdf v1.0, 정적 서브셋 활성화. 글리프 ID가 CFF와 hmtx 어디에 떨어지느냐에 따라 몇 KB 편차)
최종 50 KB짜리 청구서 PDF라면 그중 절반 이상이 폰트 데이터입니다. 그래도 서브셋 없이 5 MB를 통째로 넣는 것에 비하면 오차 수준이고, 뷰어는 즉시 엽니다.
Noto Sans JP와 Noto Sans CJK JP — 혼동 금지
일본어를 처리할 수 있다고 주장하는 Noto 패밀리가 두 개 있고, 이름이 비슷해서 서로 호환된다고 착각하기 쉽습니다. 실제로는 완전히 다릅니다.
Noto Sans JP가 쓰려는 쪽입니다. TTF 배포, 단일 언어, 굵기별로 파일이 분리. Google Fonts에서 받는 것이 이것입니다.
Noto Sans CJK JP는 CJK 전체를 아우르는 슈퍼 패밀리. OpenType Collection (.ttc) 형식으로, 일본어·간체 중국어·번체 중국어·한국어 글리프를 한자 통합 (Han unification) 방식으로 한 파일에 담아 배포합니다. 초기 Noto 릴리스와 notofonts.github.io/noto-cjk에 있는 것은 이쪽입니다.
gpdf는 TTF를 바로 지원합니다. TTC는 컨테이너 포맷이라 WithFont에 바이트를 넘기기 전에 face 인덱스를 골라야 하고, 각 face 안의 cmap은 특정 CJK 로케일에 맞춰져 있어 한자 통합에 관한 선택을 암묵적으로 하는 꼴이 됩니다. JP 전용 TTF를 고르면 이런 선택이 명시적이 됩니다.
새 프로젝트면 Noto Sans JP를 씁니다. 레거시 프로젝트에 NotoSansCJK-Regular.ttc가 이미 있다면 pyftsubset이나 fonttools로 JP face만 추출해 TTF로 저장소에 커밋하는 편이 안전합니다.
바이너리에 폰트 임베드하기
PDF 생성기는 대개 컨테이너에서 돕니다. 폰트를 함께 배포하는 가장 깔끔한 방법은 바이너리에 컴파일해 넣는 것:
package main
import (
_ "embed"
"github.com/gpdf-dev/gpdf"
)
//go:embed NotoSansJP-Regular.ttf
var notoJP []byte
func main() {
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", notoJP),
gpdf.WithDefaultFont("NotoSansJP", 11),
)
// ...
}
바이너리는 약 8 MB에서 약 13 MB로 늘어납니다. 대신 Docker 이미지 산출물이 두 개가 아니라 하나가 되고, COPY --from=builder /app /app 만으로 충분하며, 폰트 파일 누락으로 망가진 컨테이너를 누군가 올릴 일도 없습니다. 하루에 수천 개 PDF를 생성하는 배치 작업이라면 이 쪽이 올바른 기본값입니다.
관련 읽을거리
- gpdf에서 일본어 폰트를 임베드하려면? — CJK TTF 전반에 적용되는 일반 레시피
- gofpdf가 아카이브되었다. gpdf 마이그레이션 가이드 —
AddUTF8Font에서 옮겨 오는 매핑 - Go PDF 라이브러리 쇼다운 2026 — CJK 처리 관점의 비교
- 폰트 가이드 —
WithFont완전 레퍼런스
gpdf를 써 보기
gpdf는 Go용 PDF 생성 라이브러리입니다. MIT, 외부 의존성 제로, 네이티브 CJK 지원.
go get github.com/gpdf-dev/gpdf