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, …). Before the first block, it is that block's
	// forced break-before, "" without one.
	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
	// Occupied is set for a region that lies below content the caller
	// placed itself. A block that does not fit moves on from it, as from a
	// region that holds a block, where an empty region would take it anyway.
	// A block moves on from an occupied region once: in the region after
	// that move, it is placed as in an empty one, so a flow through regions
	// that are all occupied ends. Margins at the top of an occupied region
	// are kept, as in the first region, unless it is a ColumnStart.
	Occupied bool
	// ColumnStart is set for a region at the top of a column after the
	// first, such as the second frame of an area below a block that spans
	// the page. After an automatic break, the margins at its top are
	// truncated even when it is Occupied, as in the second column of a
	// multi-column row below a spanner (CSS Fragmentation 3 §5.2).
	ColumnStart bool
	// 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
}

A region that lies below content the caller placed itself, such as the rest of a frame below a picture, sets Occupied. An empty region takes the next block even when it does not fit; an occupied one does not: the block moves on to the next region, as from a region that already holds a block. It does so only once, so a flow through regions that are all occupied still ends. ColumnStart marks a region at the top of a column after the first, where the margins at its top are truncated after an automatic break.

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, the html.Node they come from, 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
	// Node is the node of the document the child comes from: the element of
	// a block, the first node of loose text. nil for content htmlbag makes
	// up, such as a ::before between blocks. A node htmlbag sets no block
	// for, such as an empty paragraph or one with display: none, is in no
	// Fragment.
	Node *html.Node
}

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

Balancing the end of the flow

A Regions that also implements RemainingRegions names the regions still free on the current page. When its BalanceEnd returns true, FlowText balances the end of the flow: the part after the last forced break that ends on a page is set in the regions of that page only as high as it takes to hold it, as balanced columns are. Each region then ends on one line across the page, or at its own foot above that line. The regions before that page are filled as without it, and nothing changes for a Regions that does not implement the interface.

type RemainingRegions interface {
	Regions
	// Remaining returns, in order, the regions Next("") would hand out after
	// the current one, up to the end of its page. It changes nothing. A
	// supplier with nothing to balance against, such as a single region,
	// returns nil.
	Remaining() []Region
	// BalanceEnd reports whether FlowText balances the end of the flow. The
	// part of the flow after its last forced break that ends on a page is
	// then set in the regions of that page as high as it takes to hold it:
	// each region ends on one line across the page, or at its own foot
	// above that line. Regions before are filled as without it.
	BalanceEnd() bool
}

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. FlowText does not lay out column-count either: the content is set in one column, with a warning. Columns are regions the caller hands out, as in the example above.

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.

A method of your Regions may run a flow of its own on the same builder, such as a running header laid out with FlowText when Next starts a new page. The inner tree may come from an HTMLToText made there, and the outer flow goes on as before once the inner one returns. OutputPagesFromText cannot run inside a flow, and FlowText cannot run inside OutputPagesFromText.