gpdf에서 사용자 정의 용지 크기를 설정하려면?
document.Size는 포인트 단위. Mm(100).Resolve(0, 0)으로 변환해 WithPageSize에 전달. 라벨, JIS B5, 함정 정리.
질문을 바꿔 말하면
필요한 건 A4도 Letter도 아닙니다. 4×6인치 택배 송장, 80mm 폭 감열 영수증, 100×148mm 엽서, 또는 JIS B5 — 귀찮게도 이건 document.B5 상수와 다릅니다. WithPageSize는 document.Size를 받습니다. 임의의 치수로 어떻게 만들고, 만들 때 어디서 막히는가?
TL;DR
document.Size는 PDF 포인트(1/72인치) 단위의 float64 필드 두 개일 뿐입니다. Resolve로 변환해서 넘기면 됩니다:
label := document.Size{
Width: document.In(4).Resolve(0, 0), // 288 pt
Height: document.In(6).Resolve(0, 0), // 432 pt
}
doc := template.New(template.WithPageSize(label))
정상 경로는 이게 전부입니다. 나머지는 일이 꼬이는 세 가지 경우에 대한 이야기입니다.
코드
완전한 4×6인치 택배 송장 프로그램입니다. gpdf v1.0.17에서 검증했으며, 출력의 /MediaBox는 [0 0 288 432]입니다.
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
// mm converts millimetres to PDF points, the unit document.Size uses.
func mm(v float64) float64 { return document.Mm(v).Resolve(0, 0) }
// in converts inches to PDF points.
func in(v float64) float64 { return document.In(v).Resolve(0, 0) }
func main() {
// A 4×6 inch shipping label: 288 × 432 pt.
label := document.Size{Width: in(4), Height: in(6)}
doc := template.New(
template.WithPageSize(label),
template.WithMargins(document.UniformEdges(document.Mm(6))),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("SHIP TO", template.Bold(), template.FontSize(10))
c.Text("Jane Doe", template.FontSize(18), template.Bold())
c.Text("742 Evergreen Terrace")
c.Text("Springfield, OR 97477")
c.Spacer(document.Mm(6))
c.QRCode("TRACK-1Z999AA10123456784", template.QRSize(document.Mm(40)))
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("label.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
mm / in 두 헬퍼는 프로젝트에 복사해 둘 만합니다. 어차피 직접 쓰게 됩니다.
Size가 Value가 아닌 이유
gpdf의 다른 곳에서는 document.Mm(20)을 넘기면 포인트 변환은 레이아웃 엔진이 나중에 처리합니다. 여백, Spacer, QR 크기 — 전부 document.Value입니다. 그래서 자연스럽게 이렇게 쓰고 싶어집니다:
document.Size{Width: document.Mm(100), Height: document.Mm(150)} // 컴파일 안 됨
컴파일되지 않습니다. Size의 정의가 이렇기 때문입니다:
type Size struct {
Width, Height float64
}
Size가 좌표계의 맨 바닥이기 때문입니다. Value는 Pct나 Em이 될 수 있고, 이를 해석하려면 부모 크기나 폰트 크기가 필요합니다. 페이지가 바로 그 "부모"이고, 그 위에는 퍼센트의 기준이 될 것이 없습니다. 그래서 페이지는 구체적인 숫자여야 하고, PDF의 /MediaBox가 포인트로 기록되므로 그 숫자는 포인트입니다.
Resolve(parentSize, fontSize)는 레이아웃 엔진이 내부적으로 호출하는 메서드입니다. 절대 단위에서는 두 인자가 모두 무시되므로 Resolve(0, 0)은 그냥 "포인트로 줘"라는 뜻입니다. 계수는 고정: Mm ×2.83465, Cm ×28.3465, In ×72.
그리고 함께 따라오는 함정. 이건 컴파일이 됩니다:
document.Size{Width: 100, Height: 150} // mm라고 생각했지만
결과는 35 × 53mm 페이지. 에러도 경고도 없습니다 — 100pt는 완벽하게 유효한 너비니까요. PDF를 열고 송장이 우표만 한 걸 보고서야 알게 됩니다. 이 글에서 하나만 가져간다면: 포인트로 생각하고 있는 게 아니라면 Size에 맨 숫자를 쓰지 마세요.
방향과 JIS B5 문제
너비가 먼저, 높이가 나중. 가로로 하려면 직접 바꾸거나 Landscape()를 호출합니다:
template.WithPageSize(document.A4.Landscape())
Landscape()는 높이가 너비보다 클 때만 바꾸므로, 이미 가로인 크기에 호출해도 아무 일도 없습니다. 크기가 설정 파일에서 오고 방향을 모를 때 편리합니다.
참고로: gpdf의 document.B5는 ISO B5(176 × 250mm)입니다. 한국과 일본 사무실에서 흔히 말하는 B5는 JIS B5(182 × 257mm)인 경우가 많습니다. JIS 크기는 사용자 정의로 만듭니다:
jisB5 := document.Size{Width: mm(182), Height: mm(257)}
jisB4 := document.Size{Width: mm(257), Height: mm(364)}
문서당 하나의 크기
AddPage(size)는 없습니다. WithPageSize는 문서 옵션이고, 모든 페이지 — 페이지네이터가 자동으로 만드는 넘침 페이지 포함 — 가 이를 사용합니다.
A4 안내문 뒤에 4×6 송장을 한 파일로 묶어야 한다면, 따로 생성해서 병합합니다:
merged, err := gpdf.Merge([]gpdf.Source{
{Data: coverA4}, // document.A4로 생성
{Data: labelPDF}, // 4×6 크기로 생성
})
병합 후에도 각 페이지는 자신의 /MediaBox를 유지합니다 — 확인했습니다: 595.28 × 841.89 다음에 288 × 432.
JSON 스키마는 이름만 지원
JSON 스키마의 page.size는 이름이 있는 크기만 받습니다 — "A0"–"A10", "B0"–"B10", "C0"–"C10", "DL", "Letter", "Legal" — 그리고 "orientation": "landscape". "100mm x 150mm" 같은 문자열은 unknown page size: "100mm x 150mm"로 실패합니다.
하지만 Go에서 넘긴 옵션이 스키마보다 우선합니다. 그러니 템플릿은 JSON으로 두고 크기는 코드에서 지정하면 됩니다:
doc, err := gpdf.NewDocumentFromJSON(schema, data,
gpdf.WithPageSize(document.Size{Width: mm(80), Height: mm(200)}),
)
NewDocumentFromTemplate에서도 마찬가지입니다.
작은 용지와 기본 여백 20mm
이 글을 쓰다가 실제로 밟은 함정이라 별도 섹션으로 둡니다.
template.New의 기본 여백은 사방 20mm입니다. A4에서는 적당합니다. 하지만 50 × 30mm 상품 라벨에서는 위아래 40mm 여백 때문에 본문 높이가 −10mm가 됩니다. 60 × 40 포인트 페이지(또 맨 숫자 함정)에 5mm 여백이면 본문 높이는 약 12pt — 12pt 글자 한 줄도 안 들어갑니다.
v1.0.16까지는 두 경우 모두 Generate()가 에러를 반환하지 않았습니다. 아예 반환하지 않았습니다. 페이지네이터가 절대 들어가지 않을 행을 계속 배치하려고 했습니다. 공통 원인을 찾기 전까지 테스트 프로그램 세 개가 멈췄고, 솔직히 그 30분은 정말 허무했습니다. 게다가 수정 스크린샷을 찍다가 네 번째가 여전히 멈췄습니다. 열보다 넓은 단어가 중간에서 잘리면서 남은 텍스트에 페이지마다 공백이 하나씩 늘어나, 멈춘 것처럼 보이지 않았던 겁니다. 이건 전혀 예상하지 못했습니다.
v1.0.17부터는 마이크로초 만에 에러를 반환합니다:
import "github.com/gpdf-dev/gpdf/document/layout"
data, err := doc.Generate()
if errors.Is(err, layout.ErrContentDoesNotFit) {
// layout: content does not fit on an empty page: body area is
// 28.35 x 0.00 pt on a 141.73 x 85.04 pt page; ...
log.Fatal(err)
}
본문 영역 숫자만 봐도 대개 원인을 알 수 있습니다. 높이가 0 이하라면 여백, 머리글, 바닥글이 페이지를 다 차지한 것입니다.
그러니 A6보다 작은 용지에서는 여전히 여백을 명시적으로 지정하세요. 에러는 뭔가 잘못됐다고 알려줄 뿐, 여백을 대신 정해 주지는 않습니다.
영수증은 좀 더 친절한 경우입니다. 80mm 폭에 높이가 고정된 페이지는 그냥 페이지가 나뉩니다 — 80 × 120mm 페이지에 40행을 넣으니 3페이지가 됐습니다. 하나로 이어진 긴 용지를 원한다면 높이를 미리 정해야 합니다. gpdf는 내용에 맞춰 페이지를 늘려 주지 않습니다.
관련 레시피
- gpdf의 12열 그리드는 어떻게 동작하나? — 열 너비는 설정한 페이지에 대한 퍼센트
- 페이지 번호, 머리글, 바닥글을 제대로 넣기 — 작은 용지에서는 머리글도 본문 높이를 잡아먹음
- Go로 50줄 이내에 인보이스 PDF 생성하기 — 비교용 A4 전체 문서
- 기본 사용법 가이드 —
template.New의 모든 옵션
gpdf 사용해 보기
gpdf는 PDF를 생성하는 Go 라이브러리입니다. MIT 라이선스, 외부 의존성 제로, 네이티브 CJK 지원.
go get github.com/gpdf-dev/gpdf