Como definir um tamanho de papel personalizado no gpdf?
document.Size usa pontos: converta com document.Mm(100).Resolve(0, 0) e passe para WithPageSize. Etiquetas, cupons, JIS B5 e as armadilhas.
A pergunta, com outras palavras
A página que você precisa não é A4 nem Letter. É uma etiqueta de envio de 4×6 polegadas, um cupom térmico de 80 mm, um cartão-postal de 100×148 mm ou um JIS B5 — que, para complicar, não é a mesma coisa que a constante document.B5. WithPageSize recebe um document.Size. Como montar um com medidas arbitrárias, e o que dá errado quando você faz isso?
TL;DR
document.Size são dois campos float64 em pontos PDF (1/72 de polegada). Converta com Resolve e passe adiante:
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))
No caminho feliz, é só isso. O resto do post trata das três formas de dar errado.
O código
Uma etiqueta de envio 4×6 polegadas completa. Verificado com gpdf v1.0.17 — o /MediaBox gerado é [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)
}
}
Vale copiar os helpers mm / in para o seu projeto. Você vai escrevê-los de qualquer jeito.
Por que Size não é um Value
No resto do gpdf você passa document.Mm(20) e o motor de layout calcula os pontos depois. Margens, spacers, tamanho de QR — tudo é document.Value. Então é natural tentar isto:
document.Size{Width: document.Mm(100), Height: document.Mm(150)} // não compila
Não compila, porque Size é definido assim:
type Size struct {
Width, Height float64
}
O motivo é que Size é a base do sistema de coordenadas. Um Value pode ser Pct ou Em, e resolvê-los exige um tamanho pai ou um tamanho de fonte. A página é o pai; não existe nada acima dela para servir de base a uma porcentagem. Então a página precisa ser números concretos, e esses números são pontos porque é assim que o /MediaBox de um PDF é escrito.
Resolve(parentSize, fontSize) é o método que o motor chama internamente. Para unidades absolutas os dois argumentos são ignorados, então Resolve(0, 0) significa só "me dê pontos". Os fatores são fixos: Mm ×2.83465, Cm ×28.3465, In ×72.
E a armadilha que vem junto. Isto compila:
document.Size{Width: 100, Height: 150} // você pensou em mm
e gera uma página de 35 × 53 mm. Sem erro, sem aviso — 100 pt é uma largura perfeitamente válida. Você descobre quando abre o PDF e a etiqueta parece um selo postal. Se for levar uma coisa só deste post: nunca escreva um número solto em um Size, a menos que esteja pensando em pontos.
Orientação e o problema do JIS B5
Largura primeiro, altura depois. Para paisagem, troque você mesmo ou chame Landscape():
template.WithPageSize(document.A4.Landscape())
Landscape() só troca se a altura for maior que a largura, então em um tamanho já em paisagem não faz nada. Útil quando o tamanho vem de configuração e você não sabe a orientação.
Uma observação: o document.B5 do gpdf é B5 ISO, 176 × 250 mm. O B5 japonês é JIS B5, 182 × 257 mm. No Brasil, o caso mais comum de tamanho personalizado é o cupom térmico de 80 mm e a etiqueta de envio — por exemplo, o DANFE simplificado de etiqueta usado por marketplaces costuma sair em 100 × 150 mm:
jisB5 := document.Size{Width: mm(182), Height: mm(257)}
etiqueta := document.Size{Width: mm(100), Height: mm(150)}
Um tamanho por documento
Não existe AddPage(size). WithPageSize é uma opção do documento, e todas as páginas — incluindo as de transbordo criadas pelo paginador — usam esse tamanho.
Se você precisa de uma carta A4 seguida de uma etiqueta 4×6 no mesmo arquivo, gere separadamente e mescle:
merged, err := gpdf.Merge([]gpdf.Source{
{Data: coverA4}, // gerado com document.A4
{Data: labelPDF}, // gerado com o tamanho 4×6
})
Cada página mantém seu próprio /MediaBox após a mesclagem — conferimos: 595.28 × 841.89 e depois 288 × 432.
Schema JSON: só nomes
O page.size do schema JSON aceita tamanhos nomeados — "A0"–"A10", "B0"–"B10", "C0"–"C10", "DL", "Letter", "Legal" — mais "orientation": "landscape". Uma string como "100mm x 150mm" falha com unknown page size: "100mm x 150mm".
Mas as opções passadas pelo Go têm prioridade sobre o schema. Então mantenha o template em JSON e defina o tamanho no código:
doc, err := gpdf.NewDocumentFromJSON(schema, data,
gpdf.WithPageSize(document.Size{Width: mm(80), Height: mm(200)}),
)
O mesmo vale para NewDocumentFromTemplate.
Páginas pequenas e a margem de 20 mm
Essa nos pegou enquanto escrevíamos o post, então ganhou seção própria.
template.New usa por padrão margens de 20 mm nos quatro lados. No A4, faz sentido. Em uma etiqueta de produto de 50 × 30 mm, 40 mm de margem vertical deixam −10 mm de altura de conteúdo. Em uma página de 60 × 40 pontos (a armadilha do número solto de novo) com margens de 5 mm, sobram uns 12 pt — menos que uma linha de texto de 12 pt.
Nos dois casos, até a v1.0.16, Generate() não retornava erro. Ele simplesmente não retornava. O paginador continuava tentando posicionar uma linha que nunca ia caber. Três programas de teste travaram antes de acharmos o fator comum, e sinceramente foi meia hora jogada fora. E, enquanto tirávamos as capturas da correção, um quarto caso ainda travava: uma palavra mais larga que a coluna era quebrada no meio, e o texto restante ganhava um espaço a cada página, então nunca parecia parado. Esse a gente não previu.
Desde a v1.0.17, ele falha em microssegundos:
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)
}
Os números da área de conteúdo costumam bastar para achar o erro. Altura zero ou negativa significa que as margens, o cabeçalho ou o rodapé comeram a página inteira.
Então: em qualquer coisa menor que A6, continue definindo as margens explicitamente. O erro avisa que algo está errado; ele não escolhe as margens por você.
Cupons são o caso mais amigável. Uma página de 80 mm de largura com altura fixa simplesmente pagina — 40 linhas em uma página de 80 × 120 mm deram três páginas. Se você quer uma tira contínua, precisa escolher a altura antes; o gpdf não estica a página para caber o conteúdo.
Receitas relacionadas
- Como funciona a grade de 12 colunas no gpdf? — as larguras das colunas são porcentagens da página que você definir
- Números de página, cabeçalhos e rodapés que simplesmente funcionam — em páginas pequenas o cabeçalho também come altura
- Gere uma fatura em PDF com Go em menos de 50 linhas — um documento A4 completo para comparar
- Guia de uso básico — todas as opções de
template.New
Experimente o gpdf
gpdf é uma biblioteca Go para gerar PDFs. Licença MIT, zero dependências externas, suporte nativo a CJK.
go get github.com/gpdf-dev/gpdf