전체 글

gpdf에서 사용자 정의 용지 크기를 설정하려면?

document.Size는 포인트 단위. Mm(100).Resolve(0, 0)으로 변환해 WithPageSize에 전달. 라벨, JIS B5, 함정 정리.

2026년 11월 10일 게시
저자: Taiki Noda

질문을 바꿔 말하면

필요한 건 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 사용해 보기

gpdf는 PDF를 생성하는 Go 라이브러리입니다. MIT 라이선스, 외부 의존성 제로, 네이티브 CJK 지원.

go get github.com/gpdf-dev/gpdf

⭐ GitHub에서 Star · 문서 읽기