Flowing into regions

Flowing into regions

OutputPagesFromText owns the pages: it starts them, fills them and ships them out. FlowText is its counterpart for programs that lay out the pages themselves, with columns, frames or text next to objects they place on their own. It pours the blocks of a styled tree into rectangles the program hands out, one after the other, and gives each one back filled. Nothing is painted and no page is started; where a filled rectangle goes is up to the caller.

func (cb *CSSBuilder) FlowText(te *frontend.Text, r Regions) error

The regions

The caller implements Regions. FlowText asks Next for a region before the first block and whenever the content moves on, and hands each region back through Filled before it asks for the next one.

type Regions interface {
	// Next returns the region to fill: once before the first block, then
	// only when the content moves on. brk is "" for an automatic break, else
	// the forced break-before or break-after keyword that caused it (page,
	// column, left, right, …).
	Next(brk string) (Region, error)
	// Filled hands back every region exactly once, the last one included,
	// before the Next that follows it. When FlowText returns an error, the
	// region it was filling is not handed back.
	Filled(f Filled) error
}

A region is a rectangle with its position on the caller’s page. Headings and anchors take their page and position from it, and side floats with inside or outside their side from PageNum.

type Region struct {
	// Width and Height are the size of the rectangle.
	Width, Height bag.ScaledPoint
	// MarginBefore is the margin still open above the region, such as the
	// MarginAfter of a flow this one continues. It collapses with the first
	// block's margin-top in the first region and in a region after a forced
	// break. After an automatic break the margin at a region's top is
	// truncated (CSS Fragmentation 3 §5.2), so it is not used there.
	MarginBefore bag.ScaledPoint
	// PageNum is the 1-based number of the caller's page the region lies
	// on: headings and anchors take their page from it, and inside/outside
	// floats their side, odd being right. 0, the zero value, counts as an
	// even page, so a region that leaves it unset lies on a left page.
	PageNum int
	// Left and Top are the region's top-left corner in PDF coordinates on
	// the caller's page, where the caller places Filled.Box. Heading
	// positions for the outline are computed from them.
	Left, Top bag.ScaledPoint
}

Regions need not be the same size. When one is narrower or wider than the one before it, the blocks that are still to come are built again at the new width, and the rest of a paragraph or a table that is split between the two is broken again.

What comes back

Filled.Box is the region’s content, ready to be placed at its top left corner. Used says how much of the height it takes, and Fragments lists which of the flow’s children landed in the region: the blocks of the body, with their id, their position and whether they began in an earlier region or go on in the next. An editor that maps a click back to the source, or a program that keeps a figure next to the paragraph that cites it, reads them there.

type Filled struct {
	// Box is the region's content, to be placed at the region's top-left
	// corner. Box.Height == Used.
	Box *node.VList
	// Used is the height from the region's top edge to the bottom edge of
	// the last box in it, or of a side float that reaches further. It
	// exceeds the region's Height when a box too tall for the empty region
	// is placed in it anyway.
	Used bag.ScaledPoint
	// MarginAfter is the margin below the last box, as far as it reaches
	// below Used: the last block's margin-bottom at the end of the flow,
	// the margin spent at the foot of the region at an automatic break.
	MarginAfter bag.ScaledPoint
	// Fragments lists the parts of the flow's children in the region, top
	// to bottom.
	Fragments []Fragment
}
type Fragment struct {
	// ID is the child's id attribute, "" without one.
	ID string
	// Index is the child's position among the flow's children. Loose text
	// between blocks counts as an anonymous child of its own.
	Index int
	// Top is the fragment's top edge, measured from the region's top edge,
	// and Height its height, both without the child's margins.
	Top, Height bag.ScaledPoint
	// Continued is set when the child began in an earlier region, Continues
	// when it goes on in the next one.
	Continued, Continues bool
}

Example: two columns per page

This program sets a heading and a long paragraph in two columns on A5 pages. Next starts a new page every other region and at a forced break, except break-before: column, which only moves on to the next column. Filled places the column and prints its fragments.

package main

import (
    "fmt"
    "log"
    "strings"

    "github.com/boxesandglue/boxesandglue/backend/bag"
    "github.com/boxesandglue/boxesandglue/backend/document"
    "github.com/boxesandglue/boxesandglue/frontend"
    "github.com/boxesandglue/htmlbag"
)

// columns hands out two columns per page and places each filled column
// on its page.
type columns struct {
    doc    *document.PDFDocument
    page   *document.Page
    col    int // column on the current page, 0 or 1
    pageno int
    cur    htmlbag.Region
}

var (
    margin = bag.MustSP("15mm")
    gap    = bag.MustSP("5mm")
)

func (c *columns) Next(brk string) (htmlbag.Region, error) {
    // A forced break other than column (page, always, left, ...) starts a
    // new page, as does the end of the second column.
    if c.page == nil || c.col == 1 || (brk != "" && brk != "column") {
        if c.page != nil {
            c.page.Shipout()
        }
        c.page = c.doc.NewPage()
        c.pageno++
        c.col = 0
    } else {
        c.col++
    }
    width := (c.page.Width - 2*margin - gap) / 2
    c.cur = htmlbag.Region{
        Width:   width,
        Height:  c.page.Height - 2*margin,
        PageNum: c.pageno,
        Left:    margin + bag.ScaledPoint(c.col)*(width+gap),
        Top:     c.page.Height - margin,
    }
    return c.cur, nil
}

func (c *columns) Filled(f htmlbag.Filled) error {
    c.page.OutputAt(c.cur.Left, c.cur.Top, f.Box)
    for _, fr := range f.Fragments {
        fmt.Printf("page %d column %d: child %d %q at %s, continued %t, continues %t\n",
            c.pageno, c.col, fr.Index, fr.ID, fr.Top, fr.Continued, fr.Continues)
    }
    return nil
}

func main() {
    fd, err := frontend.New("columns.pdf")
    if err != nil {
        log.Fatal(err)
    }
    fd.Doc.DefaultPageWidth = bag.MustSP("148mm")
    fd.Doc.DefaultPageHeight = bag.MustSP("210mm")

    cb, err := htmlbag.New(fd, htmlbag.NewCSSParserWithDefaults())
    if err != nil {
        log.Fatal(err)
    }
    if err := cb.AddCSS(`body { font-family: serif; font-size: 10pt; line-height: 13pt }`); err != nil {
        log.Fatal(err)
    }
    para := strings.Repeat("Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 40)
    te, err := cb.HTMLToText(`<html><body><h1 id="intro">Two columns</h1><p id="text">` + para + `</p></body></html>`)
    if err != nil {
        log.Fatal(err)
    }
    cols := &columns{doc: fd.Doc}
    if err := cb.FlowText(te, cols); err != nil {
        log.Fatal(err)
    }
    cols.page.Shipout()
    if err := fd.Doc.Finish(); err != nil {
        log.Fatal(err)
    }
}

It prints:

page 1 column 0: child 0 "intro" at 6.7, continued false, continues false
page 1 column 0: child 1 "text" at 41.7, continued false, continues true
page 1 column 1: child 1 "text" at 0, continued true, continues false

What a region does not hold

Footnotes, float: top and float: bottom, position: absolute and fixed, and running elements belong to a page, not to a region. FlowText drops them with a warning. Side floats (float: left and right) are laid out inside the regions.

A region that is empty takes the first block even when it is taller than the region, so the flow always makes progress; Used is then larger than the region’s Height. Call FlowText with each styled tree before the next HTMLToText, as with OutputPagesFromText, and not from inside a method of your Regions.