¿Cómo anidar un Row dentro de un Col en gpdf?
No se puede — ColBuilder no tiene método Row en gpdf. La cuadrícula de 12 columnas es plana a propósito. Tres idiomas reemplazan los rows anidados.
La pregunta, en otras palabras
Vienes de Bootstrap o Tailwind, donde .row y .col se anidan libremente. Puedes meter un .row dentro de un .col dentro de otro .row y la cuadrícula sigue cascadeando. Te sientas con gpdf, ves el mismo idioma r.Col(span, fn), y vas a buscar c.Row(...) dentro del callback de la columna. No está. ¿Fue un olvido?
TL;DR
No. La cuadrícula de 12 columnas de gpdf es plana a propósito. ColBuilder solo acepta contenido — Text, Image, Table, Box, List, Spacer — y Row / AutoRow viven en PageBuilder, no en ColBuilder. Si llegaste aquí buscando la sintaxis, no existe. Sigue leyendo para las tres cosas que la reemplazan.
La forma de la API
Esto es lo que realmente contiene el conjunto de métodos de ColBuilder (desde gpdf/template/grid.go):
func (c *ColBuilder) Text(text string, opts ...TextOption)
func (c *ColBuilder) Image(src []byte, opts ...ImageOption)
func (c *ColBuilder) Box(fn func(c *ColBuilder), opts ...BoxOption)
func (c *ColBuilder) Table(header []string, rows [][]string, opts ...TableOption)
func (c *ColBuilder) Line(opts ...LineOption)
func (c *ColBuilder) List(items []string, opts ...ListOption)
func (c *ColBuilder) Spacer(height document.Value)
// …PageNumber, TotalPages, RichText, QRCode, Barcode
No hay Row. No hay AutoRow. No hay Col. La ruta Col → Row no existe como método, y c.Box(fn, ...) es lo más cercano — pero Box acepta otro *ColBuilder, no un row. Puedes anidar columnas dentro de columnas (más o menos, vía Box), pero no puedes abrir un nuevo row horizontal dentro de una columna. Esa es la restricción.
Idioma 1 — Rows hermanos al nivel de página
Esto es lo que el 90% de los patrones "row anidado" realmente quieren.
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(
gpdf.WithPageSize(document.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(15))),
)
page := doc.AddPage()
// Lo que querías escribir (pero no puedes):
//
// page.AutoRow(func(r *template.RowBuilder) {
// r.Col(8, func(c *template.ColBuilder) {
// c.Row(...) ❌ no existe
// })
// })
// Lo que escribes en su lugar:
page.AutoRow(func(r *template.RowBuilder) {
r.Col(8, func(c *template.ColBuilder) {
c.Text("Título del artículo", template.FontSize(18), template.Bold())
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("2026-05-16")
})
})
page.AutoRow(func(r *template.RowBuilder) {
r.Col(8, func(c *template.ColBuilder) {
c.Text("El párrafo de entrada ocupa la misma columna de 8.")
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("por Taiki Noda")
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
_ = os.WriteFile("flat.pdf", data, 0o644)
}
Los dos AutoRow comparten los mismos spans 8+4, así que las columnas se alinean visualmente. No hay sub-cuadrícula; hay una secuencia plana de rows que casualmente usan el mismo reparto. La salida es idéntica a la que obtendrías de un layout CSS que anidara .row dentro de .col-8 — porque lo único que la forma anidada te compraba era localidad sintáctica, y gpdf prefiere que gastes ese presupuesto en consistencia de ancho.
Idioma 2 — c.Box para agrupamiento visual
Si la motivación real era "quiero una tarjeta con borde con dos elementos apilados dentro de esta columna", querías Box, no un sub-row:
page.AutoRow(func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) {
c.Box(func(c *template.ColBuilder) {
c.Text("Facturar a", template.Bold())
c.Text("Acme S.L.")
c.Text("Madrid, España")
},
template.WithBoxBorder(template.Border(
template.BorderWidth(document.Pt(1)),
template.BorderColor(pdf.RGBHex(0xBDBDBD)),
)),
template.WithBoxPadding(document.UniformEdges(document.Mm(4))),
)
})
r.Col(6, func(c *template.ColBuilder) {
c.Box(func(c *template.ColBuilder) {
c.Text("Enviar a", template.Bold())
c.Text("Igual que facturación")
},
template.WithBoxPadding(document.UniformEdges(document.Mm(4))),
)
})
})
El *ColBuilder que recibe Box apila su contenido verticalmente. Tampoco puedes dividir un Box horizontalmente — para eso, vuelves al Idioma 1. Pero para el patrón de "tarjeta" que la sintaxis de row anidado suele perseguir, esta es la herramienta correcta. La línea c.Box en gpdf/template/grid.go:246 es la única anidación que hace la cuadrícula, y es deliberadamente unidimensional.
Idioma 3 — Planear la sub-cuadrícula directo en 12 columnas
A veces realmente quieres un layout de 2 columnas dentro de lo que parece una sección de media página: una miniatura y un pie de foto dentro de la mitad izquierda, un párrafo a la derecha. El instinto es Col(6) > Row > Col(6) + Col(6). El equivalente plano es simplemente Col(3) + Col(3) + Col(6):
page.AutoRow(func(r *template.RowBuilder) {
r.Col(3, func(c *template.ColBuilder) {
c.Image(thumbBytes)
})
r.Col(3, func(c *template.ColBuilder) {
c.Text("Foto por Ansel Adams", template.Italic())
c.Text("1942")
})
r.Col(6, func(c *template.ColBuilder) {
c.Text("El párrafo del cuerpo ocupa la mitad derecha de la página.")
})
})
3 + 3 juntos suman 6, así que el par miniatura/pie ocupa exactamente la mitad izquierda. Doce factoriza en 2, 3, 4 y 6, así que una cuadrícula anidada casi siempre se aplana limpio. Si tu cuadrícula anidada era Col(8) > Row > Col(7) + Col(5), eso no se aplana — pero esos números tampoco significan nada en un documento real. Elige la versión plana que sí significa algo.
Por qué no hay anidación
Una cuadrícula plana resuelve anchos en una pasada. El row es un porcentaje del ancho de página menos márgenes. Cada Col(span) es span / 12 de eso. Listo. No hay recursión, no hay ancho-de-un-ancho-de-un-ancho, no hay contexto padre que enhebrar por el motor de layout. La línea en grid.go que calcula el ancho de columna es literalmente una línea:
Width: document.Pct(float64(col.span) / float64(gridColumns) * 100),
Añade anidación y esa línea se vuelve un recorrido de árbol. De repente necesitas decidir qué significa Col(6) dentro de Col(8) dentro de Col(12) — ¿es 6 el 50% de la columna padre, el 50% del row, o el 50% de la página? Bootstrap eligió "50% del padre" y añadió breakpoints y gutters para hacerlo soportable. Los PDF no tienen breakpoints. Los PDF no tienen contenedor fluido. Pedir prestada la sintaxis de anidación importaría tres problemas que no tenemos, a cambio de un atajo sintáctico que no necesitamos.
"Pero realmente quiero localidad sintáctica"
Justo. La desventaja de aplanar es que dos llamadas a AutoRow que pertenecen conceptualmente juntas pueden separarse en el código mientras editas. Un pequeño helper cierra la brecha:
func card(page *template.PageBuilder, title, body string) {
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text(title, template.Bold())
})
})
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text(body)
})
})
}
La localidad vive en tu función, no en la API. gpdf no incluye card porque son tres líneas y tu versión va a ajustarse mejor a tu documento que la nuestra.
Recetas relacionadas
- ¿Cómo funciona la cuadrícula de 12 columnas en gpdf? — la cuadrícula en detalle
- Generar un PDF de factura en Go en menos de 50 líneas — un layout de cuadrícula plana que sostiene un documento entero
- Layout guide — referencia completa de rows, columnas y Box
Prueba gpdf
gpdf es una biblioteca Go para generar PDFs. Licencia MIT, cero dependencias externas, soporte nativo CJK.
go get github.com/gpdf-dev/gpdf