Description
When an AutoRow partially fits at the bottom of a page — some columns fit, others overflow — gpdf renders the partial content on the current page and re-renders the full row on the next page.
The result is duplicate content: a fragment of the row appears at the bottom of page N, overlapping the footer, and the complete row appears again at the top of page N+1.
The root cause is that the horizontal Box produced by RowBuilder.build() is created with a zero-value BreakPolicy. When the page's vertical layout engine processes the row and finds BreakInside == BreakAuto, it keeps whatever partial content fits on the current page and moves the overflow to the next page (line 185 of document/layout/block.go). The guard that would prevent the split — BreakInside == BreakAvoid — is never set on row boxes.
Splitting a row between its columns is almost never desirable in a grid layout system: a row is a logical unit, and a partial rendering produces incorrect output.
Steps to Reproduce
package main
import (
"os"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/pdf"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := template.New(
template.WithPageSize(document.A4),
template.WithMargins(document.Edges{
Top: document.Mm(13), Right: document.Mm(13),
Bottom: document.Mm(19), Left: document.Mm(13),
}),
template.WithDefaultFont("Helvetica", 9),
)
doc.Footer(func(p *template.PageBuilder) {
p.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.PageNumber(template.AlignRight(), template.FontSize(8),
template.TextColor(pdf.Gray(0.5)))
})
})
})
page := doc.AddPage()
// Fill the page with enough rows to push the last group near the bottom.
for range 18 {
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("Filler row — some content here")
})
})
}
// This row is tall enough (image + text ≈ 18 mm) that it partially overflows.
// Col 1 text fits in the remaining space; col 2 image does not.
page.AutoRow(func(r *template.RowBuilder) {
r.Col(9, func(c *template.ColBuilder) {
c.Text("GROUP HEADER", template.Bold(), template.FontSize(12))
c.Text("Patient name", template.FontSize(10))
c.Spacer(document.Mm(2))
})
r.Col(3, func(c *template.ColBuilder) {
// Tall image — does not fit in the remaining space.
c.Image(makePNG(80, 80), template.FitWidth(document.Mm(20)))
})
})
f, _ := os.Create("/tmp/break_test.pdf")
defer f.Close()
doc.Render(f)
}
(Replace makePNG(80, 80) with any PNG image bytes.)
Open /tmp/break_test.pdf. The last row is visible at the bottom of page 1 (partial: text only, overlapping the footer) and again in full at the top of page 2.
Expected Behavior
The row is treated as an atomic unit. Because it does not fit in its entirety on page 1, the whole row moves to page 2. Page 1 ends cleanly with the filler rows and the footer. The group header appears once, at the top of page 2.
Actual Behavior
The row is split: col 1 text ("GROUP HEADER", "Patient name") renders at the bottom of page 1, overlapping the footer. The complete row — both columns — renders again at the top of page 2.
The content is duplicated and the footer is obscured.
Go Version
go1.26.2 darwin/arm64
gpdf Version
v1.0.6
PDF Viewer (if relevant)
No response
Description
When an
AutoRowpartially fits at the bottom of a page — some columns fit, others overflow — gpdf renders the partial content on the current page and re-renders the full row on the next page.The result is duplicate content: a fragment of the row appears at the bottom of page N, overlapping the footer, and the complete row appears again at the top of page N+1.
The root cause is that the horizontal
Boxproduced byRowBuilder.build()is created with a zero-valueBreakPolicy. When the page's vertical layout engine processes the row and findsBreakInside == BreakAuto, it keeps whatever partial content fits on the current page and moves the overflow to the next page (line 185 ofdocument/layout/block.go). The guard that would prevent the split —BreakInside == BreakAvoid— is never set on row boxes.Splitting a row between its columns is almost never desirable in a grid layout system: a row is a logical unit, and a partial rendering produces incorrect output.
Steps to Reproduce
(Replace
makePNG(80, 80)with any PNG image bytes.)Open
/tmp/break_test.pdf. The last row is visible at the bottom of page 1 (partial: text only, overlapping the footer) and again in full at the top of page 2.Expected Behavior
The row is treated as an atomic unit. Because it does not fit in its entirety on page 1, the whole row moves to page 2. Page 1 ends cleanly with the filler rows and the footer. The group header appears once, at the top of page 2.
Actual Behavior
The row is split: col 1 text ("GROUP HEADER", "Patient name") renders at the bottom of page 1, overlapping the footer. The complete row — both columns — renders again at the top of page 2.
The content is duplicated and the footer is obscured.
Go Version
go1.26.2 darwin/arm64
gpdf Version
v1.0.6
PDF Viewer (if relevant)
No response