gpdf에 커스텀 TrueType 폰트를 추가하려면?
TTF 바이트를 읽어 gpdf.WithFont로 패밀리를 등록하고 이름으로 참조한다. Inter, Roboto, 아이콘 폰트까지 어떤 TrueType이든 같은 방식으로 작동한다.
다시 말하면
.ttf 파일이 있다 — 브랜드용 Inter, 코드 블록용 JetBrains Mono, 아이콘용 글리프 폰트. 이걸 gpdf 문서에 넣고 c.Text(...)에서 이름으로 참조하려면?
빠른 답
TTF 바이트를 읽는다. gpdf.WithFont("내패밀리", bytes)를 NewDocument에 전달한다. 그리고 template.FontFamily(...)로 "내패밀리"를 참조하거나, gpdf.WithDefaultFont로 기본값 설정.
패밀리 이름은 임의 문자열이다. 폰트 내부의 name 테이블과 아무 관계 없다 — gpdf가 FontFamily 옵션을 해결할 때 쓰는 lookup 키일 뿐. 짧게 짓는 게 좋다.
동작하는 코드
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
regular, err := os.ReadFile("Inter-Regular.ttf")
if err != nil {
log.Fatal(err)
}
doc := gpdf.NewDocument(
gpdf.WithPageSize(gpdf.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
gpdf.WithFont("Inter", regular),
gpdf.WithDefaultFont("Inter", 11),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("Quarterly Report", template.FontSize(28))
c.Text("Generated with gpdf and Inter.")
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("report.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
Inter-Regular.ttf를 main.go 옆에 두자 (rsms.me/inter에서 다운로드). go run main.go. 끝.
gpdf가 바이트로 하는 일
Generate()가 호출되면 gpdf는 TrueType 테이블 (cmap, glyf, loca, hmtx …)을 순수 Go로 파싱한다 — FreeType도 CGO도 없다. 렌더링되는 텍스트를 훑어 실제 사용된 코드 포인트를 모으고, 그 코드 포인트만 남기도록 글리프 테이블을 서브셋팅 한다. PDF에는 Type0 / CIDFontType2 폰트로, 필요한 글리프만 임베드된다.
실제 효과: 600 KB짜리 Inter-Regular.ttf도 문단 두어 개만 썼다면 PDF 내부 서브셋은 12 KB 정도다. 브랜드 폰트를 쓰면서도 파일이 부풀지 않는다.
Bold와 Italic은 각자 파일이 필요하다
여기서 사람들이 걸려 넘어진다. gpdf는 bold나 italic을 합성하지 않는다 — "더 굵게 그리는" 알고리즘이 없다. 스타일 플래그에서 변형 ID를 만들어 lookup 한다:
Bold() | Italic() | Lookup 키 |
|---|---|---|
| 아니오 | 아니오 | Inter |
| 예 | 아니오 | Inter-Bold |
| 아니오 | 예 | Inter-Italic |
| 예 | 예 | Inter-BoldItalic |
Inter-Bold를 등록하지 않았다면 lookup은 조용히 일반 Inter로 폴백한다. PDF는 출력되지만 모두 일반 굵기. 경고는 없다.
네 변형 모두 등록:
regular, _ := os.ReadFile("Inter-Regular.ttf")
bold, _ := os.ReadFile("Inter-Bold.ttf")
italic, _ := os.ReadFile("Inter-Italic.ttf")
boldItalic, _ := os.ReadFile("Inter-BoldItalic.ttf")
doc := gpdf.NewDocument(
gpdf.WithFont("Inter", regular),
gpdf.WithFont("Inter-Bold", bold),
gpdf.WithFont("Inter-Italic", italic),
gpdf.WithFont("Inter-BoldItalic", boldItalic),
gpdf.WithDefaultFont("Inter", 11),
)
폰트가 한 가지 굵기만 제공한다면 (아이콘 폰트나 디스플레이 폰트는 흔하다), 그 폰트에는 template.Bold()나 template.Italic()을 아예 쓰지 말자. 변형 자체가 없는 건 괜찮다. 잘못된 변형으로 폴백하는 게 "굵은 글씨가 안 굵다" 이슈의 원인이다.
폰트를 바이너리에 임베드
os.ReadFile을 시작 시점에 호출하는 건 개발에선 괜찮다. 운영에서 폰트는 프로그램의 일부 — 바이너리에 들어가야 한다:
import _ "embed"
//go:embed fonts/Inter-Regular.ttf
var interRegular []byte
doc := gpdf.NewDocument(
gpdf.WithFont("Inter", interRegular),
)
go build가 바이트를 바이너리에 굽는다. "배포 이미지의 .ttf가 어디로 갔지"를 금요일 저녁에 추적할 일이 없어진다.
아이콘 폰트도 동일
Font Awesome, TTF로 내보낸 Material Symbols, IcoMoon, 자체 브랜드 글리프 셋 — 전부 TrueType 파일이다. 같은 방식으로 등록한다:
icons, _ := os.ReadFile("MaterialSymbols-Regular.ttf")
doc := gpdf.NewDocument(
gpdf.WithFont("Icons", icons),
gpdf.WithDefaultFont("Inter", 11), // 본문 기본
)
// 컬럼 안에서:
c.Text("", template.FontFamily("Icons"), template.FontSize(20)) // "home" 아이콘
Unicode 이스케이프는 폰트 문서가 명시한 값. gpdf는 그 글리프가 아이콘인지 신경 쓰지 않는다 — 그냥 코드 포인트로 보고, 글자와 똑같이 서브셋팅 한다.
자주 하는 실수
- 호출 측 패밀리 이름 오타.
template.FontFamily("Intr")는 조용히 기본 폰트로 폴백한다. 에러도 경고도 없다. 갑자기 Helvetica처럼 보이면 가장 먼저 의심한다. - 컨테이너에서
//go:embed를 안 씀. 다듬어진 Docker 컨텍스트가.ttf를 떨어뜨리고, 런타임 폴백이 작동하고, 고객 메일에서 알게 된다. 임베드. - 폰트의 PostScript 이름을 패밀리로 사용. "Inter-Regular"는 PostScript 이름. 이걸
WithFont에 넘기면 bold lookup이 "Inter-Regular-Bold"를 찾고 — 존재하지 않는다. 깔끔한 루트 패밀리 ("Inter")를 쓰고 변형 접미사가 스타일을 처리하게 둔다.
관련 레시피
- gpdf에 일본어 폰트를 임베드하려면? — 같은 메커니즘, CJK 관련 메모 포함
- Bold와 Italic을 함께 쓰려면? — 변형 리졸버 상세
- 내 PDF에 두부 박스가 나오는 이유는? — 폰트가 코드 포인트를 커버하지 않을 때 일어나는 일
gpdf 써보기
gpdf는 Go용 PDF 생성 라이브러리다. MIT, 외부 의존 없음, TrueType 처리는 순수 Go.
go get github.com/gpdf-dev/gpdf