gpdf로 만든 PDF에서 일본어가 네모 (두부 문자) 로 나오는 이유와 해결법
일본어가 □ 로 나오는 건 대부분 폰트 미등록. 흔한 4가지 원인과 최단 수정법을 정리한다.
질문을 다시 말하면
gpdf로 일본어를 썼는데 출력된 PDF에서 그 문자들이 전부 빈 네모로 나온다. 이게 뭐고, 어떻게 해야 실제 일본어 글리프가 파일 안에 들어가는가.
빠른 답
이것이 두부 문자(tofu)다. PDF 뷰어가 임베드된 폰트에서 해당 유니코드 코드포인트의 글리프를 찾지 못해 자리 표시용 사각형을 그리는 것. 원인은 네 가지이고, 그중 하나가 압도적으로 많다.
빈도 순:
- CJK 폰트를 등록하지 않았다.
gpdf.NewDocument에WithFont호출이 없어서 문서가 PDF Base-14 폰트(Helvetica / Times / Courier)로 폴백한 상태. 이들 중 어느 것도 U+3040–U+9FFF를 커버하지 않는다. - CJK 폰트는 등록했는데
c.Text의 패밀리명이 다르다.WithFont("NotoSansJP", ...)는 설정했지만 텍스트에template.FontFamily("Arial")가 지정되어 있어서, gpdf가 Latin 폰트에서 일본어를 조회한다. - 폰트 파일 자체에 CJK 글리프가 없다. 디스크의 TTF가 Latin 서브셋(
NotoSans-Regular.ttf)이다. 이름은 맞아 보이지만 커버리지가 비어 있다. - gpdf에 도달하기 전에 바이트가 깨졌다. 문자열이 상류에서 Shift-JIS나 Latin-1로 디코딩돼, 렌더링하려는 것이 이미 일본어 코드포인트가 아니다. 네모가 아니라
縺ゅ→縺처럼 나오면 이 경우다.
원인 #1의 표준 수정
열에 아홉은 이것이다:
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("こんにちは、世界。")
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("hello.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
두 줄로 폰트 등록과 기본 설정이 끝난다. CGO도, AddUTF8Font 설정 단계도 필요 없다. □□□□□、□□。로 나오던 것이 이 코드와 실제 NotoSansJP-Regular.ttf를 나란히 두고 실행하면 제대로 된 글리프로 나온다.
NotoSansJP-Regular.ttf는 Google Fonts에서 받는다.
어떤 원인인지 판별하기
봐야 할 곳은 세 군데다. 문서를 만드는 부분, 텍스트를 쓰는 부분, TTF 파일 그 자체.
출력이 일관된 □□□ (모든 네모가 같은 모양)이라면 원인 1, 2, 3 중 하나. PDF에는 폰트가 임베드됐지만 그 폰트에 글리프가 없는 상태다. Acrobat에서 PDF를 열고 파일 → 속성 → 폰트에서 실제로 임베드된 폰트를 본다. Helvetica / Times / Courier뿐이면 원인 1. NotoSansJP가 나열돼 있는데도 네모면 원인 2 또는 3.
출력이 縺ゅ→縺이나 ã"ã‚"ã«ã¡ã¯ 같은 Latin 잡음이면 원인 4. 일본어 문자열이 gpdf에 도달하기 전에 다시 인코딩됐다. 가장 흔한 범인은 Excel이 Shift-JIS로 저장한 CSV를 os.ReadFile로 읽어 UTF-8로 그대로 쓰는 경우, 또는 charset=utf-8을 선언하지 않은 HTTP 엔드포인트. 고쳐야 할 건 디코더지 PDF가 아니다.
섞여서 나옴 — 일부는 정상, 일부는 네모 — 라면 폰트 커버리지가 부분적이라는 뜻. "일본어 지원"이라고 표기된 폰트가 히라가나·가타카나는 포함해도 鬱, 龠 같은 드문 한자를 빠뜨리는 경우가 있다. JIS X 0213을 커버하는 Noto Sans JP나 Source Han Sans JP로 바꾸면 해결된다.
원인 2 상세: 폰트는 맞는데 패밀리명이 틀림
이게 까다로운 이유는 폰트가 실제로 임베드됐기 때문이다 — 그냥 안 쓰일 뿐. 최소 재현:
doc := gpdf.NewDocument(
gpdf.WithFont("NotoSansJP", font),
// WithDefaultFont 누락
)
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("こんにちは") // 기본 폰트 = Helvetica로 그려짐
})
})
수정: NewDocument에 gpdf.WithDefaultFont("NotoSansJP", 12)를 추가하거나, 일본어를 쓰는 모든 c.Text에 template.FontFamily("NotoSansJP")를 넘긴다. WithFont의 패밀리명과 c.Text의 패밀리명은 대소문자까지 완전히 일치해야 한다. NotoSansJP와 notosansjp는 gpdf 입장에서 다른 폰트다.
원인 3 상세: 잘못된 TTF 파일
NotoSans-Regular.ttf와 NotoSansJP-Regular.ttf는 다른 파일이다. 전자는 CJK 커버리지가 0인 Latin 폰트. 후자는 약 17,000자의 일본어 컷. 디렉토리 리스트에서 거의 똑같이 보이고, 자동완성이 틀린 쪽을 집어주기 쉽다.
gpdf는 등록 시점에 글리프 커버리지를 검증하지 않는다. 바이트를 주면 믿는다. 실패는 렌더링 시점의 두부 문자로만 드러난다.
빠른 확인:
- macOS:
Font Book에서 파일을 더블 클릭하면 글리프 그리드가 나온다 - Linux:
otfinfo -u NotoSans-Regular.ttf가 유니코드 커버리지를 덤프한다 - 크로스플랫폼: fontTools의
ttx -t cmap NotoSans-Regular.ttf가 cmap 테이블을 XML로 내보낸다
목록에 U+3042 (あ)가 없으면 Latin 서브셋을 쥐고 있는 것이다.
원인 4 상세: 인코딩 손상
이건 사실 gpdf와 무관하다. c.Text에 넘긴 시점에 문자열이 이미 깨져 있다. 렌더 전에 출력해본다:
text := loadLabelFromSomewhere()
fmt.Printf("%q\n", text) // 실제 rune 출력
c.Text(text)
여기서 "あいうえ" 대신 "縺ゅ→縺"가 나오면 손상은 상류에서 일어났다. gpdf는 못 고친다 — UTF-8이 잘못 디코딩된 지점을 찾아 수정한다.
흔한 상류 범인:
- Excel이 Shift-JIS(CP949와 혼동 주의, 일본은 CP932)로 저장한 CSV를
os.ReadFile후 바로string()캐스팅 - 이미 mojibake를 저장하고 있는
latin1또는utf8mb3컬럼(utf8mb4아님) Content-Type: application/json; charset=utf-8선언이 없어서 Latin-1로 추측한 HTTP 응답
한 가지 잊기 쉬운 엣지 케이스
gpdf의 서브셋 고정은 Generate() 시점에 일어난다. 문서 구성 중에 こんにちは를 그린 뒤 나중에 鬱陶しい를 그려도, 두 번째도 서브셋에 제대로 추가된다. 하지만 이미 생성된 PDF를 Acrobat에서 열어 원본에 없던 한자를 타이핑하면 그 자리는 두부가 된다. 서브셋은 Generate() 순간에 얼어붙기 때문이다. PDF를 후편집하지 말고 Go에서 다시 Generate() 한다.
관련 레시피
- gpdf에서 일본어 폰트를 임베드하려면? — 볼드/이탤릭 변형과 다중 CJK 문서를 포함한
WithFont전체 안내 - gpdf에서 Noto Sans JP를 사용하려면? — 어떤 Noto 파일을 고를지,
go:embed로 배포를 어떻게 단순화할지 - Go에서 일본어 PDF 만들기 결정판 가이드(2026) — 폰트, 세로쓰기, ruby, 일본어 특유 레이아웃을 다루는 장편 가이드
gpdf를 써보자
gpdf는 Go용 PDF 생성 라이브러리다. MIT 라이선스, 외부 의존성 0, 네이티브 CJK 지원.
go get github.com/gpdf-dev/gpdf