gpdf에서 일본어 폰트를 임베드하려면?
TTF 바이트를 gpdf.WithFont에 전달하면 끝. 서브셋 임베딩은 자동, CGO도 필요 없음. Go에서 일본어 PDF를 만드는 최단 경로.
질문을 다시 말하면
AddUTF8Font 절차, CGO 의존, 문서마다 5 MB짜리 폰트 전부 임베딩 — 이런 것들 없이 gpdf로 일본어(또는 CJK) PDF를 만드는 가장 짧은 방법은 무엇인가.
요점
TTF 바이트를 읽고, gpdf.WithFont("NotoSansJP", fontBytes)를 NewDocument에 전달하고, 필요하면 기본 폰트로 지정한다. 설정 3줄이면 gpdf가 실제로 사용된 글리프만 자동 서브셋 임베딩한다 — 5 MB 통째로 끌어안지 않는다.
완전한 예제
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", 12),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("こんにちは、世界。", template.FontSize(24), template.Bold())
c.Text("日本語 PDF、これだけ。")
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("hello.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
Google Fonts에서 NotoSansJP-Regular.ttf를 다운로드해 main.go 옆에 두고 go run main.go를 실행하면 일본어가 찍힌 한 장짜리 PDF가 나온다.
세 줄 뒤에서 벌어지는 일
내부에서는 두 가지 일이 일어나며, 둘 다 사용자가 신경 쓸 필요가 없다.
서브셋 임베딩. Noto Sans JP은 약 17,000개의 글리프를 담고 있고 Regular 단독으로 약 5 MB다. 폰트 전체를 그대로 임베드한다면 네 줄짜리 영수증 PDF도 5 MB를 넘어버린다. gpdf는 렌더링한 텍스트를 훑어 실제 사용된 글리프 ID만 뽑아 PDF에 기록한다. 짧은 청구서 한 장이라면 폰트 데이터는 보통 20–40 KB 수준이다.
gofpdf도 서브셋화는 가능했지만, AddUTF8Font가 파일 경로와 UTF-8 플래그를 받아 커서 이동 중에 로드하는 구조라 문서 중간에 폰트를 바꾸는 것이 번거로웠다. gpdf는 문서 생성 시 한 번만 등록하고, 이후 모든 c.Text는 패밀리 이름만 참조한다. 호출마다 준비 작업은 없다.
CGO를 쓰지 않는다. 이 점은 생각보다 크다. 다른 생태계에서는 폰트 처리가 FreeType이나 HarfBuzz를 거치는 일이 많은데, 그러면 C 의존이 생기고, 빌드 캐시 동작이 달라지며, Docker 이미지 레이어가 늘고, macOS에서 linux/arm64로 크로스 컴파일할 때 추가 설정이 필요해진다. gpdf는 TrueType 테이블을 순수 Go로 파싱한다. go build는 여전히 정적 바이너리를 만들고, distroless 컨테이너에 Go 바이너리와 TTF만 넣어 배포할 수 있다.
Bold / Italic 변형
일본어 Noto 패밀리는 굵기별로 파일이 나뉘어 있다. 굵은 글씨를 쓰려면 Bold TTF를 -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", 12),
)
이제 template.Bold()가 -Bold 변형을 집어든다. -Italic과 -BoldItalic도 같은 규약이다. 변형을 등록하지 않으면 합성 굵기로 폴백된다 — 화면상 읽히지만 타이포그래피적으로는 정확하지 않다. 실전 청구서라면 실제 굵기를 등록하자.
같은 문서에 한·중·일 섞기
패밀리는 몇 개든 등록 가능하다. gpdf는 이들을 독립적으로 관리한다. 텍스트 단위로 template.FontFamily(...)를 이용해 전환하면 된다:
jp, _ := os.ReadFile("NotoSansJP-Regular.ttf")
sc, _ := os.ReadFile("NotoSansSC-Regular.ttf")
kr, _ := os.ReadFile("NotoSansKR-Regular.ttf")
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", jp),
gpdf.WithFont("NotoSansSC", sc),
gpdf.WithFont("NotoSansKR", kr),
gpdf.WithDefaultFont("NotoSansJP", 12),
)
page.AutoRow(func(r *template.RowBuilder) {
r.Col(4, func(c *template.ColBuilder) {
c.Text("日本語")
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("中文", template.FontFamily("NotoSansSC"))
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("한국어", template.FontFamily("NotoSansKR"))
})
})
한자 통합(Han unification) 때문에 일본어와 중국어 간체는 유니코드 코드포인트를 공유하지만 실제 자형은 다르다. 같은 코드포인트라도 어떤 폰트를 쓰느냐에 따라 글자 모양이 달라진다 — 폰트 선택은 미학 문제가 아니라 정확성 문제다. 국가별 세금계산서나 운송장을 생성한다면 두 폰트 모두 등록해야 한다.
두부 문자 함정
일본어를 썼는데 WithFont를 빠뜨리면 gpdf는 Base-14 표준 PDF 폰트로 폴백한다. 거기엔 CJK 글리프가 없으므로 문자가 빈 사각형으로 렌더링된다. 유니코드 쪽에서 흔히 말하는 "두부 문자"다:
□□□□□、□□。
이 출력을 보면 원인은 하나다: CJK 폰트를 등록하지 않았거나, 해당 글리프가 없는 패밀리로 쓰고 있다. 해결책도 하나다: WithFont를 추가하고 WithDefaultFont로 기본값을 잡거나 c.Text에 template.FontFamily를 명시하자.
관련 글
- gofpdf가 아카이브됐다. gpdf로 이관하기. —
pdf.AddUTF8Font에서 옮겨가는 경우의 전체 매핑 - Go PDF 라이브러리 쇼다운 2026 — gofpdf / gopdf / Maroto / unipdf와 gpdf의 CJK 비교
- 폰트 가이드 —
WithFont전체 레퍼런스 및 변형 명명 규약
gpdf 써보기
gpdf는 Go용 PDF 생성 라이브러리다. MIT, 외부 의존 없음, CJK 기본 지원.
go get github.com/gpdf-dev/gpdf