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, …).
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 falseWhat 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.