Skip to content

AutoRow split across pages produces duplicate content and overlaps footer #24

Description

@gemineo

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions