gpdf 푸터에 페이지 번호 X / Y를 한 줄로 표시하려면?
PageNumberPlaceholder와 TotalPagesPlaceholder를 c.Text 하나에 넣으면 됩니다. v1.0.14+는 치환 후 정렬도 유지합니다.
질문을 바꿔 말하면
푸터에 Page 3 of 12라고 표시하고 싶습니다. 한 줄, 문자열 하나로, 푸터의 다른 요소처럼 오른쪽 정렬해서요. gpdf에는 c.PageNumber()와 c.TotalPages()가 있지만 각각 독립된 요소입니다. 열 네 개를 이어 붙이지 않고 글자와 두 숫자를 한 줄에 넣으려면 어떻게 해야 할까요?
TL;DR
두 플레이스홀더는 export된 문자열 상수입니다. 평범한 문자열 중간에 넣으면 됩니다.
c.Text("Page "+document.PageNumberPlaceholder+" of "+document.TotalPagesPlaceholder,
template.AlignRight())
gpdf는 페이지 분할 후 모든 페이지에서 이를 치환합니다. gpdf v1.0.14 이상에서는 치환 후 줄을 다시 측정하므로, 페이지 번호가 9에서 10으로 바뀌어도 오른쪽 정렬이 유지됩니다.
코드
400행짜리 테이블이 A4 9페이지로 넘어가는 문서에 푸터를 붙인 예입니다. gpdf v1.0.17에서 확인했습니다.
package main
import (
"fmt"
"log"
"os"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/pdf"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
pageOf := "Page " + document.PageNumberPlaceholder +
" of " + document.TotalPagesPlaceholder
doc := template.New(
template.WithPageSize(document.A4),
template.WithMargins(document.UniformEdges(document.Mm(20))),
)
doc.Footer(func(p *template.PageBuilder) {
p.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text(pageOf,
template.AlignRight(),
template.FontSize(9),
template.TextColor(pdf.Gray(0.4)),
)
})
})
})
rows := make([][]string, 0, 400)
for i := 1; i <= 400; i++ {
rows = append(rows, []string{fmt.Sprint(i), fmt.Sprintf("Line item %03d", i)})
}
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Table([]string{"#", "Description"}, rows)
})
})
out, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("page-x-of-y.pdf", out, 0o644); err != nil {
log.Fatal(err)
}
}
pdftotext -layout page-x-of-y.pdf -를 실행하면 페이지마다 Page 1 of 9부터 Page 9 of 9까지 출력됩니다. 따로 설정할 것은 없습니다.
왜 동작하는가
c.PageNumber()에 특별한 마법은 없습니다. 구현은 "내용이 document.PageNumberPlaceholder인 document.Text를 추가한다"가 전부입니다. c.TotalPages()도 상수만 다를 뿐 같습니다. 플레이스홀더는 NUL 바이트로 감싼 센티널 문자열("\x00GPDF_PAGE\x00", "\x00GPDF_TOTAL\x00")이며, 실제 텍스트와 절대 겹치지 않도록 고른 값입니다.
치환은 페이지네이터가 전체 페이지 수를 정한 뒤에 일어납니다. 모든 페이지에 배치된 텍스트 노드를 순회하면서 두 플레이스홀더에 대해 strings.ReplaceAll을 실행합니다. 노드에 플레이스홀더만 들어 있든, 문장 중간에 하나 끼어 있든 상관하지 않습니다. 그래서 "Page " + PageNumberPlaceholder + " of " + TotalPagesPlaceholder도 전용 메서드와 똑같이 처리됩니다.
헤더와 푸터 노드는 한 번만 레이아웃되어 물리 페이지 간에 공유되므로, 리졸버는 페이지별 복사본을 다룹니다. 3페이지 푸터에는 3, 4페이지에는 4가 들어가고, *template.Document 자체는 변경되지 않으므로 두 번째로 렌더링해도 플레이스홀더가 그대로 남아 있습니다.
이 마지막 부분은 놓치기 쉽습니다. 같은 방법이 본문에서도 통하므로 표지 문구에 쓸 수 있습니다.
c.Text(fmt.Sprintf("This report has %s pages.", document.TotalPagesPlaceholder))
같은 9페이지 문서의 1페이지에 This report has 9 pages.가 출력됩니다.
예전 글에서 열 네 개를 쓰라고 한 이유
5월에 쓴 Go PDF에서 페이지 번호, 헤더, 푸터를 제대로 넣기에서는 한 줄짜리 "Page X of Y"를 아쉬운 점으로 꼽고 열 네 개로 나누는 우회 방법을 소개했습니다. 당시 gpdf에서는 맞는 조언이었고, 이유는 측정 순서에 있습니다.
레이아웃 엔진은 페이지 수가 정해지기 전에 텍스트의 줄바꿈과 정렬을 끝냅니다. v1.0.13까지는 원래의 센티널 문자열이 들어간 상태로 이 작업을 했습니다. \x00GPDF_TOTAL\x00은 11바이트라서, 엔진은 Page 1 of 9를 실제보다 훨씬 넓다고 판단했습니다. 위 프로그램을 v1.0.13으로 다시 돌려서 얼마나 어긋나는지 확인했습니다.
| 푸터 레이아웃 | v1.0.13 | v1.0.17 |
|---|---|---|
| 12칸 열, 오른쪽 정렬 | 오른쪽 끝이 x = 424.6 pt — 여백까지 114 pt 모자람 | 9페이지 모두 오른쪽 끝이 x = 538.6 pt(여백 위치) |
| 3칸 열, 오른쪽 정렬 | Page 1 of가 한 줄, 9는 다음 줄로 넘어감 | Page 1 of 9가 한 줄 |
| 두 자리 페이지(32페이지 실행) | — | Page 9 of 32에서 Page 10 of 32까지 오른쪽 끝이 538.6 pt로 유지 |
114포인트는 40 mm입니다. 오른쪽 정렬 푸터에서 이 정도면 커닝 문제처럼 보이지 않습니다. 누가 Tab 키를 누른 것처럼 보입니다. 더 좁은 열에서는 상황이 더 나빴는데, 이를 고친 커밋에 따르면 줄바꿈 처리기가 센티널 자체를 쪼개서 ?GP나 DF 같은 조각이 그대로 인쇄되었습니다.
v1.0.14(커밋 b58626a)는 두 가지를 바꿨습니다. 레이아웃 단계에서는 각 플레이스홀더를 숫자 한 자리("0")로 측정하고 줄바꿈한 뒤, 각 줄의 내용에 플레이스홀더를 다시 복원합니다. 그다음 ResolvePageNumbersWithFonts가 치환된 모든 줄을 실제 폰트로 다시 측정하고, 가운데 정렬과 오른쪽 정렬 줄을 기준점으로 되돌립니다. Page 10 of 32가 한 자리 더 넓은데도 Page 9 of 32와 같은 x에서 끝나는 것은 이 두 번째 단계 덕분입니다.
v1.0.13 이하에 머물러야 한다면 예전 글의 다중 열 레이아웃이 여전히 동작합니다. 플레이스홀더가 각자 자기 열에 단독으로 들어가기 때문입니다. 그렇지 않다면 업그레이드하세요.
go get github.com/gpdf-dev/gpdf@latest
변형
슬래시 형식. document.PageNumberPlaceholder + " / " + document.TotalPagesPlaceholder는 3 / 12가 됩니다.
fmt.Sprintf. 플레이스홀더는 평범한 문자열이므로 %s를 씁니다. %d는 쓰지 마세요. 아직 숫자가 없습니다. 플레이스홀더를 쓰는 이유가 바로 그것입니다.
fmt.Sprintf("Page %s of %s", document.PageNumberPlaceholder, document.TotalPagesPlaceholder)
한국어 등 CJK. 해당 문자를 포함하는 폰트를 등록하고 텍스트에 지정하세요. PageNumberPlaceholder + " / " + TotalPagesPlaceholder + " 페이지" 같은 형식도 똑같이 치환됩니다. 폰트 서브셋에는 실제로 쓴 글리프만 들어갑니다.
JSON과 Go 템플릿. 계속 {"type": "pageNumber"}와 {"type": "totalPages"}를 별도 요소로 쓰세요. v1.0.17에서는 JSON 문자열에 \u0000GPDF_PAGE\u0000을 직접 써도 동작합니다. 하지만 상수의 이름이 API이고 값은 구현 세부 사항입니다. 프로덕션 템플릿을 그 값이 바뀌지 않는다는 데 걸지는 않겠습니다.
아직 할 수 없는 것은 페이지에 따른 분기입니다. 푸터 클로저는 자신이 1페이지를 렌더링하는지 알지 못하므로, "표지에는 번호를 넣지 않기"는 다른 방법이 필요합니다. 그건 별도 레시피로 다룹니다.
관련 레시피
- Go PDF에서 페이지 번호, 헤더, 푸터를 제대로 넣기 — 헤더/푸터 모델 전체와 2단계 리졸버의 위치
- 테이블을 여러 페이지에 걸쳐 출력하려면? — 이 예제의 넘침 페이지가 생기는 원리
- gpdf에서 Col을 행의 오른쪽 끝에 붙이려면? — 번호를 오른쪽 별도 열에 두고 싶을 때
- 기본 사용법 가이드 —
template.New의 모든 옵션
gpdf 사용해 보기
gpdf는 PDF를 생성하는 Go 라이브러리입니다. MIT 라이선스, 외부 의존성 제로, 네이티브 CJK 지원.
go get github.com/gpdf-dev/gpdf