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) errorThe 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 falseBalancing 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.