¿Cómo defino un tamaño de papel personalizado en gpdf?
document.Size va en puntos: conviértelo con document.Mm(100).Resolve(0, 0) y pásalo a WithPageSize. Etiquetas, tickets, JIS B5 y las trampas.
La pregunta, en otras palabras
La página que necesitas no es A4 ni Letter. Es una etiqueta de envío de 4×6 pulgadas, un ticket térmico de 80 mm, una postal de 100×148 mm o un JIS B5, que, para complicar las cosas, no es lo mismo que la constante document.B5. WithPageSize recibe un document.Size. ¿Cómo se construye uno con medidas arbitrarias y qué falla cuando lo haces?
TL;DR
document.Size son dos campos float64 en puntos PDF (1/72 de pulgada). Convierte con Resolve y pásalo:
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))
Eso es todo en el caso feliz. El resto del artículo trata de las tres formas en que se tuerce.
El código
Una etiqueta de envío de 4×6 pulgadas completa. Verificado con gpdf v1.0.17: el /MediaBox resultante es [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 la pena copiar los helpers mm / in a tu proyecto. Los vas a escribir de todas formas.
Por qué Size no es un Value
En el resto de gpdf pasas document.Mm(20) y el motor de maquetación calcula los puntos después. Márgenes, spacers, tamaño de QR: todo es document.Value. Así que lo natural es intentar esto:
document.Size{Width: document.Mm(100), Height: document.Mm(150)} // no compila
No compila, porque Size está definido así:
type Size struct {
Width, Height float64
}
La razón es que Size es la base del sistema de coordenadas. Un Value puede ser Pct o Em, y para resolverlos hace falta un tamaño padre o un tamaño de fuente. La página es el padre; no hay nada por encima de lo que sacar un porcentaje. Así que la página tiene que ser números concretos, y esos números van en puntos porque así se escribe el /MediaBox de un PDF.
Resolve(parentSize, fontSize) es el método que el motor llama internamente. Para unidades absolutas ignora ambos argumentos, así que Resolve(0, 0) significa simplemente "dame puntos". Los factores son fijos: Mm ×2.83465, Cm ×28.3465, In ×72.
Y la trampa que viene con ello. Esto sí compila:
document.Size{Width: 100, Height: 150} // pensabas en mm
y te da una página de 35 × 53 mm. Sin error, sin aviso: 100 pt es un ancho perfectamente válido. Te enteras cuando abres el PDF y la etiqueta parece un sello postal. Si te llevas una sola cosa de este artículo: nunca escribas un número suelto en un Size salvo que estés pensando en puntos.
Orientación y el problema del JIS B5
Primero el ancho, luego el alto. Para apaisado, intercámbialos tú o llama a Landscape():
template.WithPageSize(document.A4.Landscape())
Landscape() solo intercambia si el alto es mayor que el ancho, así que sobre un tamaño ya apaisado no hace nada. Útil cuando el tamaño viene de configuración y no sabes en qué sentido está.
Un apunte: document.B5 en gpdf es B5 ISO, 176 × 250 mm. El B5 japonés es JIS B5, 182 × 257 mm. Lo mismo pasa con formatos locales como el oficio (216 × 330 mm en varios países de Latinoamérica): son tamaños personalizados.
jisB5 := document.Size{Width: mm(182), Height: mm(257)}
oficio := document.Size{Width: mm(216), Height: mm(330)}
Un tamaño por documento
No existe AddPage(size). WithPageSize es una opción del documento, y todas las páginas —incluidas las de desbordamiento que crea el paginador— lo usan.
Si necesitas una carta A4 seguida de una etiqueta 4×6 en un solo archivo, genéralos por separado y combínalos:
merged, err := gpdf.Merge([]gpdf.Source{
{Data: coverA4}, // generado con document.A4
{Data: labelPDF}, // generado con el tamaño 4×6
})
Cada página conserva su /MediaBox tras la combinación —lo comprobamos: 595.28 × 841.89 y luego 288 × 432.
Esquema JSON: solo nombres
El page.size del esquema JSON acepta tamaños con nombre —"A0"–"A10", "B0"–"B10", "C0"–"C10", "DL", "Letter", "Legal"— más "orientation": "landscape". Una cadena como "100mm x 150mm" falla con unknown page size: "100mm x 150mm".
Pero las opciones pasadas desde Go tienen prioridad sobre el esquema. Así que deja la plantilla en JSON y fija el tamaño en código:
doc, err := gpdf.NewDocumentFromJSON(schema, data,
gpdf.WithPageSize(document.Size{Width: mm(80), Height: mm(200)}),
)
Funciona igual con NewDocumentFromTemplate.
Páginas pequeñas y el margen de 20 mm
Esta nos mordió mientras escribíamos el artículo, así que tiene sección propia.
template.New usa por defecto márgenes de 20 mm en los cuatro lados. En A4 tiene sentido. En una etiqueta de producto de 50 × 30 mm, 40 mm de margen vertical dejan −10 mm de alto de contenido. En una página de 60 × 40 puntos (otra vez la trampa del número suelto) con márgenes de 5 mm, quedan unos 12 pt: menos que una línea de texto de 12 pt.
En ambos casos, hasta v1.0.16, Generate() no devolvía un error. Simplemente no volvía. El paginador seguía intentando colocar una fila que nunca cabría. Se nos colgaron tres programas de prueba antes de dar con el factor común, y la verdad, fue media hora tonta. Y mientras hacíamos las capturas del arreglo, un cuarto caso seguía colgado: una palabra más ancha que la columna se partía a mitad, y el texto restante ganaba un espacio en cada página, así que nunca parecía atascado. Ese no lo vimos venir.
Desde v1.0.17 falla en microsegundos:
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)
}
Las cifras del área de contenido suelen bastar para ver el error. Un alto de cero o negativo significa que los márgenes, la cabecera o el pie se comieron la página.
Así que: en cualquier cosa más pequeña que A6, sigue definiendo los márgenes explícitamente. El error te avisa de que algo va mal; no elige los márgenes por ti.
Los tickets son el caso amable. Una página de 80 mm de ancho con alto fijo pagina sin más: 40 filas en una página de 80 × 120 mm nos dieron tres páginas. Si quieres una tira continua, tienes que elegir el alto de antemano; gpdf no estira la página para ajustarse al contenido.
Recetas relacionadas
- ¿Cómo funciona la cuadrícula de 12 columnas en gpdf? — los anchos de columna son porcentajes de la página que definas
- Números de página, cabeceras y pies que simplemente funcionan — en páginas pequeñas la cabecera también resta alto
- Genera una factura PDF en Go en menos de 50 líneas — un documento A4 completo para comparar
- Guía de uso básico — todas las opciones de
template.New
Prueba gpdf
gpdf es una biblioteca Go para generar PDF. Licencia MIT, cero dependencias externas, soporte nativo de CJK.
go get github.com/gpdf-dev/gpdf