layout

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package render lays HTML and CSS out onto a PDF page.

This file is its guardrail vocabulary, and it exists before the layout engine on purpose. §9 of the rendering proposal asks for the reporting layer to land *with* the engine rather than after it, and gives the reason: a reporting layer retrofitted onto a finished engine is how it becomes decorative. The engine grows into this, not the other way round.

What the guardrails are for

Layout degrades *silently*. That is its characteristic failure and the whole argument of §6. A clipped paragraph, a 3pt caption and a heading full of tofu all produce a valid PDF that a caller has no programmatic way to distrust — the file opens, the text is selectable, nothing errors. So every way this engine can quietly produce a document that is not what was asked for is a named rule with an identifier, and a caller can decide for each one whether it is worth failing over.

Index

Constants

View Source
const UserAgentCSS = `` /* 8670-byte string literal not displayed */

UserAgentCSS is the default stylesheet.

The lengths are the ones every browser converged on, and the em-based ones are em on purpose: a document that sets a larger font gets proportionally larger spacing, which is what an author expects and what a fixed pixel margin quietly fails to do.

Variables

View Source
var (
	A4     = PageSizePt(595.276, 841.89).WithMarginPt(56.7) // 20mm
	A5     = PageSizePt(419.528, 595.276).WithMarginPt(42.5)
	Letter = PageSizePt(612, 792).WithMarginPt(54) // 0.75in
	Legal  = PageSizePt(612, 1008).WithMarginPt(54)
)

The paper sizes a document generator is actually asked for.

View Source
var ErrNoResolver = errors.New("no resource resolver is configured, so nothing outside the document can be loaded")

ErrNoResolver is what the engine reports when a document refers to something and the caller configured no resolver.

View Source
var NoSource = Source{HTMLOffset: -1, CSSOffset: -1}

NoSource is a finding that is not tied to a place in the input.

Functions

func PathOf

func PathOf(n *html.Node) string

PathOf renders an element's position in the document, so a finding about a stylesheet shared by many elements can still say which one it is about.

It is a readable path rather than a selector that would round-trip: it names the element chain with the identifiers and classes that distinguish it, which is what someone reading a report needs.

func ShapedText

func ShapedText(v DrawText) string

ShapedText is the string handed to the shaper for one text run.

The run's own text is in logical order and carries no direction of its own: a run of punctuation between two Hebrew words is right-to-left because of characters that are in other runs by now. The shaper applies UAX #9 to the string it is given, so left to itself it would answer for that string rather than for the paragraph the run came out of, and a lone bracket would come out facing the wrong way.

So the direction the layout resolved is stated to it, in the one vocabulary a string has for saying so: an explicit right-to-left override in front of the text. That is exactly what the character means; it is a default-ignorable code point, so the shaper drops it before any glyph is chosen; and what comes back is the run's glyphs in the order they are drawn, with rule L4's mirroring applied.

The override goes here and not into the run's text, because the run's text is what a reader copies out of the finished page.

Types

type Box

type Box struct {
	Outer Outer
	Inner Inner

	// Element is the element that generated this box, or nil when nothing did.
	// An anonymous box has no element and no style of its own — it inherits
	// everything, which is what the specification means by anonymous.
	Element *html.Node

	// Style is the computed style of the generating element. An anonymous box
	// carries the style it inherits from, so that a consumer never has to walk
	// up looking for one.
	Style style.ComputedStyle

	// Text is the content of a text box, after white-space processing. It is
	// empty for every other kind.
	Text string

	// FontSize is the computed font-size, resolved here rather than in the
	// cascade because it is the one property whose value depends on its own
	// parent's computed value: "font-size: 2em" means twice the parent's size,
	// so it can only be resolved walking downwards.
	//
	// Only an element that *declared* one resolves; an element that inherited
	// its font-size takes its parent's number unchanged. See fontSizeOf for why
	// that distinction is the whole of the property's correctness.
	//
	// Every font-relative length in the box — its margins, its padding, its
	// line height — is measured against this, so it has to exist before layout
	// asks anything about geometry.
	FontSize style.Unit

	// ListItem marks a box that generates a marker — a bullet or a number.
	ListItem bool

	// ListValue is what a numbered marker counts to, taken from the "list-item"
	// counter rather than from the item's position among its siblings.
	//
	// ListNumbered says whether it means anything, because zero is a value a
	// list can legitimately be at — <ol start="0"> — and not a way of saying
	// there is no counter. Reading the zero as "unset" numbered that list from
	// one, which is the same shape of fault as a sentinel colliding with a real
	// value elsewhere in this engine.
	//
	// The two differ whenever a document says so, and documents do: <ol start>,
	// <li value>, a "counter-reset: list-item" on the list, an item that is not
	// the first child, or a list whose items are not siblings at all. Counting
	// siblings gets every one of those wrong, and gets them wrong quietly — the
	// list is numbered, just not with the numbers the author asked for.
	//
	// Where there is no counter — a "display: list-item" that no rule increments
	// — the position among the parent's list items is written here instead, by
	// the block walk, which is the only place that knows it. ListNumbered stays
	// false there, and says which of the two answered. Keeping the fallback in
	// the same field is what lets everything that draws a marker read one number:
	// it used to travel as an argument, and three of the five call sites had
	// nothing to pass and passed zero.
	ListValue    int
	ListNumbered bool

	// Replaced is the content of a replaced element — the decoded image an
	// <img> names — or nil for every other box.
	//
	// It is nil rather than empty when the content could not be loaded, and
	// that is the whole of what CSS means by an element being replaced: an
	// image that did not arrive makes the element an ordinary inline box
	// holding its alt text, not a replaced one holding nothing. Layout
	// therefore asks whether this is nil rather than asking what the element's
	// tag is, and an <img> with a broken src goes down exactly the same path as
	// a <span>.
	Replaced *ReplacedContent

	// Control is what makes a box a form control, or nil for every other box.
	//
	// It carries only what CSS cannot say — an intrinsic size in characters and
	// in lines — for the reason ReplacedContent gives about itself: being a
	// control changes where a box's auto width and height come from and nothing
	// else about it. See control.go, and note what it deliberately is not: no
	// PDF form field is produced from it and nothing about it is interactive.
	Control *Control

	// BackgroundImages is the pictures this box's background-image named, by the
	// reference the stylesheet wrote.
	//
	// It is a map rather than a slice because the layers are read later, by
	// layout, and matching a slice to them by index would depend on the loading
	// pass and the reading pass agreeing about a value they parse separately —
	// which is the sort of coupling that survives every test and breaks on the
	// document that repeats one file in two layers.
	//
	// A reference that failed to load is absent rather than present and nil, so
	// a layer naming it paints nothing. That is the same answer a broken <img>
	// gets and for the same reason: the picture is missing, not the box.
	BackgroundImages map[string]*ReplacedContent

	// TableWrapper marks the anonymous box §17.4 puts around a table to hold it
	// and its captions.
	//
	// It is a flag rather than an Inner of its own because the wrapper really is
	// an ordinary flow root — block layout, margin collapsing, floats and
	// positioning all treat it as one, and that is the point of it. The one thing
	// that is not ordinary is its width, which is the table's rather than its
	// containing block's, and that is the single question this answers.
	TableWrapper bool

	// Float and Clear are CSS 2.1 §9.5. They live on the box rather than being
	// read out of Style at layout time for the same reason Outer and Inner do:
	// whether a box is in the normal flow changes what the box tree itself is
	// allowed to do to it — an anonymous block box is generated around a run of
	// *in-flow* inline content, and a float in that run is not part of the run.
	Float FloatSide
	Clear ClearSide

	// Position is CSS 2.1 §9.3's scheme, and it lives here for the same reason
	// Float does and one more. An absolutely positioned box is out of flow, so
	// the anonymous-box rules have to know about it before layout runs; and
	// §9.7 blockifies it exactly as it blockifies a float, which is a
	// computed-value rule and so belongs to the stage that computes the box.
	Position PositionScheme

	// ZIndex and ZAuto are §9.9's stacking level. They are read here rather than
	// out of Style at paint time because painting order is decided by a
	// traversal that has to sort boxes against each other, and a sort key that
	// has to be re-parsed at every comparison is a sort that gets written once
	// and then quietly avoided.
	ZIndex int
	ZAuto  bool

	// Order is the box's index in document order, which is the tie-break
	// Appendix E uses between two positioned boxes at the same stacking level.
	//
	// It is recorded rather than derived because painting does not walk the tree
	// in document order any more: an absolutely positioned box is placed after
	// the flow has been laid out and hangs from whichever fragment could hold
	// it, so its position among its siblings has been lost by the time anything
	// needs to sort it. Two overlapping cards written one after the other would
	// otherwise stack in whichever order the placement pass happened to reach
	// them, which is stable, invisible and wrong.
	Order int

	Children []*Box
	Parent   *Box
	// contains filtered or unexported fields
}

Box is a node of the box tree.

func BuildBoxes

func BuildBoxes(doc *html.Node, styled style.Styled, rec *Recorder) *Box

BuildBoxes turns a styled document into a box tree.

The root box is the one the document element generated. It is nil when the document produces no boxes at all, which is what "html { display: none }" means and is not an error.

func (*Box) Anonymous

func (b *Box) Anonymous() bool

Anonymous reports whether a box was generated by the engine rather than by an element.

func (*Box) IsText

func (b *Box) IsText() bool

IsText reports whether a box is a run of text.

type Built

type Built struct {
	// Document is the parsed tree, present even when findings were raised.
	Document *html.Node
	// Root is the root of the box tree, nil when the document produces no
	// boxes — which "html { display: none }" legitimately does.
	Root *Box
	// Styles is every element's computed style.
	Styles map[*html.Node]style.ComputedStyle
	// Fonts is the set the document is to be laid out in: the caller's library
	// with the faces the document's own @font-face rules loaded over it. It is
	// never nil, and it is what a caller calling Layout directly must hand it —
	// passing Input.Fonts instead would lay the document out without the fonts
	// it brought.
	Fonts FontSet
	// Findings is everything worth telling the caller, ordered deterministically.
	Findings []Finding
	// Failed reports that something fired at Error severity, so a caller that
	// went on to render would be rendering something it was told not to.
	Failed bool
	// Truncated reports that the finding list was cut.
	Truncated bool
}

Built is the result of the stages that exist.

func Build

func Build(in Input) Built

Build parses, styles and boxes a document.

type ClearSide

type ClearSide uint8

ClearSide is the value of the clear property: which sides of earlier floats a box refuses to sit beside.

const (
	ClearNone ClearSide = iota
	ClearLeft
	ClearRight
	ClearBoth
)

func (ClearSide) String

func (c ClearSide) String() string

type Clip

type Clip struct {
	Rect   Rect
	Active bool
}

Clip is the area an operation may mark.

Active is not redundant with an empty rectangle, and conflating the two would be the classic sentinel collision: "clip: rect(0, 0, 0, 0)" asks for a clip that admits nothing, which is the *opposite* of no clip at all. The engine never emits an operation whose clip is active and empty — there would be nothing to draw — so the distinction is only ever read one way, but a reader of the display list should not have to know that to be safe.

type Composed

type Composed struct {
	// Ops is the display list, in paint order.
	Ops []Op
	// Root is the fragment tree the display list was painted from.
	Root *Fragment
	// Scale is the factor of §5: 1 when the content fitted, less when it had to
	// be shrunk. It is reported because a caller may want to refuse a document
	// that only fitted by being made small.
	Scale float64
	// NaturalSize is what the content needed at its natural size, before any
	// scaling. It is what a caller adjusting a template needs to know.
	NaturalSize Size
	// Findings is everything the guardrails raised, in a deterministic order.
	Findings []Finding
	// Refused is a rule having fired at Error severity. A backend that sees it
	// should produce nothing: the caller was told not to render, rather than
	// left to decide.
	//
	// It is not a summary of Findings and cannot be recomputed from them. A
	// rule counts the moment it fires, before the list deduplicates and before
	// the bound below cuts it — so a document refused by its six-hundredth
	// finding is refused with that finding nowhere in the list. This field is
	// the authority; the list is the explanation, when there is room for one.
	Refused bool

	// Truncated is the bound having stopped findings being recorded, so what
	// Findings holds is some of them rather than all of them.
	//
	// A backend that reports findings has to say so. Presenting a cut list as a
	// complete one is how "three problems" becomes what a reader believes about
	// a document with four hundred.
	Truncated bool
}

Composed is a document laid out and painted, ready for a backend.

func Compose

func Compose(in Input, opts Options) Composed

Compose is everything between a document and a backend: build the box tree, lay it out on the sheet, decide the scale, check that what came out is worth producing, and paint it.

It stops one step short of a document, and that step is the only one that knows what a document is. A backend takes Ops and writes them — into a PDF, into a raster, into a test — and everything above this line is the same whichever it is.

type Control

type Control struct {
	Kind controlKind

	// Chars is the intrinsic content width in "0" advances, or zero when the
	// control has none and shrinks to fit instead.
	Chars int
	// Lines is the intrinsic content height in line boxes, or zero when the
	// height is the content's own.
	//
	// It is a *used* height rather than a minimum, which is what makes a
	// textarea with twenty lines of text in it two lines tall and scrolled:
	// rows says how much of the control is on the page, not how much text it
	// holds.
	Lines int
}

Control is what a box needs to know to be laid out as a control.

It is a value on the Box for the same reason ReplacedContent is: being a control changes where a box's size comes from and nothing else about it. A control still floats, still positions, still takes part in a line, and every rule about those applies unchanged.

type DirResolver

type DirResolver struct {
	// contains filtered or unexported fields
}

DirResolver serves files from one directory and from nowhere else.

Containment is enforced by os.Root, which resolves every path component against the directory at the system-call level: a reference with "..", an absolute path, and a symbolic link pointing outside the directory are all refused by the kernel rather than by a string comparison this code performs. That distinction is the whole reason os.Root exists — a check on the name followed by an open is a race, and a check on the *resolved* name is one symlink away from being wrong.

A resolver holds an open directory handle and should be closed when the caller is done with it.

func NewDirResolver

func NewDirResolver(dir string) (*DirResolver, error)

NewDirResolver opens dir as the only place a document may read from.

func (*DirResolver) Close

func (d *DirResolver) Close() error

Close releases the directory handle.

func (*DirResolver) Resolve

func (d *DirResolver) Resolve(ref string) ([]byte, error)

Resolve reads one file from the rooted directory.

func (*DirResolver) WithMaxBytes

func (d *DirResolver) WithMaxBytes(n int64) *DirResolver

WithMaxBytes returns the resolver with a different size cap. A cap of zero or less is refused rather than treated as "no limit": an unbounded resolver is the thing this type exists to prevent, and a zero value in a configuration struct must not switch it off.

type DrawImage

type DrawImage struct {
	Rect  Rect
	Image image.Image
	// Key identifies the source bytes. Two elements naming one file carry the
	// same key, which is what lets the backend embed the picture once — a
	// document with a logo in a header repeated on every row would otherwise
	// carry it as many times as it is drawn.
	Key string
	// Clip is §11.1's clipping, when something clips this picture.
	//
	// It cannot be folded into Rect the way a fill's is, because Rect is where
	// the picture is *stretched to*: narrowing it would squeeze the whole
	// image into the visible strip rather than cutting the part that is not.
	// A backend must intersect its own clipping path with this.
	Clip Clip
}

DrawImage paints a decoded image to fill a rectangle.

The rectangle is the element's *content* box, which is where a replaced element's content goes: inside its padding, inside its border. The image is stretched to it rather than fitted, because the sizing rules upstream have already chosen a rectangle with the right shape — object-fit, which is what asks for anything else, is not implemented.

type DrawText

type DrawText struct {
	At   Point
	Text string
	// RTL says the run reads right to left.
	//
	// The text is in *logical* order — the order it is written and read, which
	// is what a reader copying it out of the page expects and what the string
	// here has to be for the text of the document to survive. Which way the
	// glyphs go is a separate fact, and it is one the backend has to be told
	// rather than one it can work out: a run of punctuation between two Hebrew
	// words is right-to-left because of its neighbours, and by the time the run
	// reaches a backend the neighbours are gone.
	RTL   bool
	Face  *shape.Face
	Size  style.Unit
	Color style.RGBA
	// CharSpacing is letter-spacing: an extra advance after every character.
	//
	// It is a property of the drawing rather than of the position because layout
	// already spent it — the run's width includes it, and the run after this one
	// is placed accordingly — so a backend that ignored it would draw the glyphs
	// bunched at the left of a gap the right size.
	CharSpacing style.Unit

	// Clip is §11.1's clipping, when something cuts this run.
	//
	// It is set only when the clip really does cut the run: a run wholly inside
	// its clip carries none, and one wholly outside is not emitted at all. That
	// is not an optimisation — it is what keeps the display list of a document
	// whose text merely happens to sit inside an "overflow: hidden" box
	// identical to the list of one that does not, which is what the reftest
	// comparison needs to be able to say the two look the same.
	Clip Clip
}

DrawText draws a run of text with the origin of its baseline at At.

The position is the baseline rather than the top of the line box, because that is what a text-drawing backend takes and because converting between them needs the face's metrics — which this stage has and the backend may not.

type Edges

type Edges struct {
	Top, Right, Bottom, Left style.Unit
}

Edges is a value per side, in the order CSS writes them.

func (Edges) Add

func (e Edges) Add(o Edges) Edges

func (Edges) Horizontal

func (e Edges) Horizontal() style.Unit

Horizontal and Vertical are the sums along each axis, which the box-model arithmetic asks for constantly.

func (Edges) String

func (e Edges) String() string

func (Edges) Vertical

func (e Edges) Vertical() style.Unit

type FallbackFontSet

type FallbackFontSet interface {
	FontSet
	// FaceFor returns a face that can set the whole of text, and whether one was
	// found. The bold and italic flags are the ones the box asked for; a set is
	// free to ignore them when the alternative is having no glyph at all.
	FaceFor(text string, bold, italic bool) (*shape.Face, bool)
}

FallbackFontSet is a FontSet that can also be asked for a face by what it needs to set rather than by name.

The two questions are different, and the second one cannot be asked through FontSet: "give me Helvetica" is answerable from the family alone, while "give me something that can set this Hebrew word" needs the text. A set that implements this is offering coverage the named family does not have.

It stays optional because it is a different promise. A FontSet answers what the document asked for; a FallbackFontSet answers what the document *needs*, which is a substitution and is reported as one. A set that made that substitution silently from inside Face would be a set that could hide it, and that is the thing this design is against — see the note on FontSet above.

The substitution is per box rather than per character. A box whose text mixes scripts that no single face covers still reports a missing glyph, and the remaining step is to cut a run into per-face pieces the way shape.Stack does, which reaches into measurement, line breaking and the content stream.

type FillRect

type FillRect struct {
	Rect  Rect
	Color style.RGBA
	// Overhang marks a fill whose position no layout decision accounted for: a
	// text decoration, and the background and border of an inline box.
	//
	// It exists for the overflow-page guardrail, which is about *boxes* leaving
	// the page and reads the display list to find them. Text is not checked by it
	// at all — a glyph whose ascender reaches above the page top produces a
	// DrawText, which the guard skips — so an overline over the same letters must
	// be skipped too. Without this, "line-height: 0.3" on the first line of a page
	// puts the overline a few pixels above the top edge, the guard fires at Error
	// severity, and no document is produced at all: an overhang of two pixels
	// turned into a refusal, from a rule whose whole purpose is to catch a wrong
	// scale calculation.
	//
	// An inline box's decoration is the same case and reaches it by the same
	// route. §10.6.1 gives the box a content area the height of its *font* rather
	// than of the line it sits on, and §8.4 and §8.5 keep its vertical border and
	// padding out of layout entirely — so a "line-height: 0.5" span, or one with
	// ten pixels of padding, puts ink above the first line of a page that nothing
	// in the flow ever measured. The scale-to-fit calculation cannot have
	// accounted for it, so a guard checking that calculation must not read it.
	Overhang bool
}

FillRect paints a rectangle in a solid colour.

type Finding

type Finding struct {
	// Rule is which guardrail fired.
	Rule Rule
	// Severity is what it did, after the caller's policy was applied.
	Severity Severity
	// Message says what happened, in terms of the author's input.
	Message string
	// Source is where in the input it happened.
	Source Source

	// Path is the DOM path of the element concerned, such as
	// "html > body > div > p", or empty. It is what makes a finding actionable
	// when the offset points at a stylesheet shared by many elements.
	Path string
	// Selector is the selector responsible, or empty.
	Selector string
	// Property is the declaration responsible, or empty.
	Property string
}

Finding is one guardrail firing.

It satisfies pdf0's Violation interface — error, RuleID and ObjectNum — so findings from a render collect into one slice alongside those from ValidatePDFA and ValidatePDFUA, which is the whole point of that interface. The interface is satisfied structurally and is not imported here, so this package does not depend on the one that documents it.

ObjectNum is always 0, which the interface already documents as "not tied to a specific object": a layout finding is about a paragraph in the source, not about an object in the file it became.

func (Finding) Error

func (f Finding) Error() string

Error renders the finding for a person, leading with the rule so that a list of them can be read down.

func (Finding) ObjectNum

func (f Finding) ObjectNum() int

ObjectNum is 0: a layout finding is not tied to a PDF object.

func (Finding) RuleID

func (f Finding) RuleID() string

RuleID is the identifier of the violated rule, for pdf0.Violation.

func (Finding) Unsupported

func (f Finding) Unsupported() bool

Unsupported reports whether the finding is about something this engine does not implement.

type FloatSide

type FloatSide uint8

FloatSide is the value of the float property.

const (
	// FloatNone leaves the box in the normal flow.
	FloatNone FloatSide = iota
	FloatLeft
	FloatRight
)

func (FloatSide) String

func (f FloatSide) String() string

type FontSet

type FontSet interface {
	// Face returns the face for a family in a weight and style, and whether the
	// set has it. The family is matched case-insensitively, as CSS matches one.
	Face(family string, bold, italic bool) (*shape.Face, bool)
}

FontSet supplies the faces a document is set in.

A set is asked for a family by name and answers whether it has one. It is not asked to do the fallback itself: which families to try, in what order, and what to do when none of them is there are decisions the engine makes and reports on, and a set that made them silently would be a set that could hide a substitution.

func StandardFonts

func StandardFonts() FontSet

StandardFonts is a FontSet of the fourteen faces every PDF reader has built in.

They cost nothing to use — a standard font is named in the file rather than embedded — and they cover Latin, which makes them the right default for a document that has not said what it wants. They cover nothing else, and a document that needs more will be told, which is the point of reporting a missing glyph rather than drawing a box.

type Fragment

type Fragment struct {
	// Box is what generated this fragment.
	Box *Box

	// BorderRect is the border box in absolute page coordinates: the area a
	// background paints and a border draws on.
	BorderRect Rect

	// Margin, Border and Padding are the resolved edges. They are kept rather
	// than folded into rectangles because painting needs them separately — a
	// border is drawn on its own width — and because the guardrails ask about
	// them by name.
	Margin, Border, Padding Edges

	Children []*Fragment

	// Lines is the inline content of a block container, in the same coordinates
	// as its children.
	//
	// A fragment has children or lines, with one exception: a float is out of
	// flow, so a block whose in-flow content is entirely inline can still have
	// floated children beside those lines. The anonymous box rule deliberately
	// does not wrap a float, because wrapping it would put it in a different
	// formatting context from the text meant to run around it.
	Lines []LineFragment

	// Marker is the bullet or number a list item generates, nil otherwise. It
	// is on the fragment rather than in the box tree because its text depends
	// on the item's position among its siblings, which is not a property of the
	// box.
	Marker *Marker

	// Offset is CSS 2.1 §9.4.3's relative displacement: how far the box is drawn
	// from where the flow put it.
	//
	// It is carried rather than folded into BorderRect because folding it in
	// during layout would be the classic way to get relative positioning wrong.
	// A relatively positioned box still *occupies* its original space, so every
	// question layout asks afterwards — where the next sibling goes, how tall
	// the parent is, which band a line box has — must be answered against the
	// unoffset position. Applying the offset in absolutise, which visits each
	// fragment once and already translates each subtree by its parent's origin,
	// moves the box and everything inside it and moves nothing else.
	Offset Point
	// contains filtered or unexported fields
}

Fragment is a box with a resolved geometry.

A box produces one fragment here. It will produce more than one when content can break — a paragraph across two columns, an inline across two lines — which is why this is a fragment rather than simply a laid-out box, and why the type is named for the thing that can be plural.

func Layout

func Layout(root *Box, avail Size, set FontSet, rec *Recorder) *Fragment

Layout positions a box tree inside an available width, returning the root fragment.

avail is the content box of the page: what §11 calls the page box minus its margins. Only the width constrains layout — the height is what comes out, and whether it fits is §5's scale-to-fit question, asked afterwards.

set supplies the faces; a nil one uses the fourteen standard faces, which need no embedding and cover Latin.

func (*Fragment) ContentRect

func (f *Fragment) ContentRect() Rect

ContentRect is the area the children were laid out in.

func (*Fragment) MarginRect

func (f *Fragment) MarginRect() Rect

MarginRect is the border box plus its margin, which is the space the box actually occupies in its parent's flow.

func (*Fragment) PaddingRect

func (f *Fragment) PaddingRect() Rect

PaddingRect is the border box minus its border.

type Inner

type Inner uint8

Inner is what formatting context a box establishes for its children.

const (
	// InnerFlow is ordinary block-and-inline layout.
	InnerFlow Inner = iota
	// InnerFlowRoot is the same, but as an independent formatting context —
	// what an inline-block establishes, and what stops margins collapsing
	// through it.
	InnerFlowRoot
	// InnerFlex, InnerTable and the table-internal contexts are named here and
	// laid out later; naming them now is what lets the box tree be built once.
	InnerFlex
	InnerTable
	InnerTableRowGroup
	InnerTableRow
	InnerTableCell
	InnerTableCaption
	InnerTableColumnGroup
	InnerTableColumn
	// InnerText is a run of text, which has no children and no context.
	InnerText
)

func (Inner) String

func (i Inner) String() string

type Input

type Input struct {
	// HTML is the document source.
	HTML string
	// CSS is the author's stylesheets, in the order they apply.
	CSS []Stylesheet
	// Policy chooses what each rule does. A nil policy uses the defaults.
	Policy Policy
	// UserCSS is a stylesheet applied on the reader's behalf, which sits
	// between the engine's defaults and the author's. It is separate from CSS
	// because its origin is different, and origin is the strongest term in the
	// cascade.
	UserCSS string

	// Resources supplies the bytes of the files the document refers to — the
	// images an <img> or a background-image names, and the stylesheets a
	// <link rel=stylesheet> does.
	//
	// A nil resolver loads nothing, which is the deliberate default: a document
	// is untrusted input, and "src" and "href" are strings in it. See
	// resource.go for what a resolver may and may not do, and NewDirResolver
	// for the contained filesystem one.
	Resources ResourceResolver

	// Fonts is the caller's font library: the faces a document may name and
	// have, before it brings any of its own. A nil set is the fourteen standard
	// PDF faces.
	//
	// It is on the input rather than on the render options because an
	// @font-face is part of the *document*, and the set a document is laid out
	// in is the caller's library with the document's own faces over it. Build
	// puts the two together and returns the result as Built.Fonts, which is
	// what a caller calling Layout directly must pass on.
	Fonts FontSet
}

Input is a document and the stylesheets to apply to it.

type LineFragment

type LineFragment struct {
	// Rect is the line box in the same coordinates as the fragment holding it.
	Rect Rect
	// Baseline is the distance from the top of the line box to the baseline the
	// text sits on. Painting needs it, and it is not derivable afterwards —
	// half-leading is split above and below the text.
	Baseline style.Unit
	// Runs are the pieces of text on the line, in *reading* order — the order
	// they were written, which on a line that mixes directions is not the order
	// they are drawn in. Where each one goes is its own X.
	//
	// The two are kept apart on purpose. The order here is the order the runs
	// reach the content stream, and so the order a reader extracting text from
	// the finished page gets them in; a right-to-left paragraph has to be drawn
	// the way it reads and copied out the way it was written, and only the X
	// decides the first.
	Runs []TextRun
	// Boxes are the fragments of the inline boxes that have a background or a
	// border on this line, in tree order — so an inner box's decoration is
	// painted over the box it is inside.
	//
	// They are fragments rather than a shape of their own because everything that
	// paints a background already works on one, and they hang from the line
	// rather than from the block's children for two reasons: Appendix E paints
	// them with the line's own content and not with the block backgrounds, and
	// one inline box produces one of them *per line* — see inlinepaint.go for
	// §8.6's slice model, which is the whole of why this is plural.
	//
	// A box with nothing to draw has none. The overwhelming majority of the
	// inline boxes in a document are an <em> or an <a> with no background and no
	// border, and a rectangle for each of them on each line would be work in
	// proportion to the document that nothing would ever read.
	Boxes []*Fragment
}

LineFragment is a line box: one row of text within a block.

type Marker

type Marker struct {
	Text string
	Face *shape.Face
	Size style.Unit
	// At is the origin of the marker's baseline, relative to the fragment's
	// border box.
	At Point
	// Color is the item's own text colour: a marker takes the colour of the
	// text it belongs to, which is why an author never sets it separately.
	Color style.RGBA
}

Marker is the bullet or number drawn beside a list item.

type Op

type Op interface {
	// contains filtered or unexported methods
}

Op is one primitive of the display list.

The set is deliberately small. Anything a backend cannot draw directly is something this stage should have decomposed — a border is four filled bands rather than a "border" primitive, because a backend that had to understand border-collapse would be a second layout engine.

func Paint

func Paint(root *Fragment) []Op

Paint turns a fragment tree into a display list, in painting order.

The order is CSS 2.1 Appendix E. It used to be tree order, with a note saying that this would stop being true the moment positioning arrived, and it has: tree order is painting order only while nothing is out of flow and nothing asks to be painted somewhere else in the stack. Both are now possible, so what is here is the real algorithm.

What a stacking context is, and what it is not

§9.9 and Appendix E divide the tree into *stacking contexts*, each of which is painted as an atomic unit in the eight steps of §E.2. The root element makes one. So does a positioned box with a z-index that is not auto — and only that, which is the distinction the whole scheme turns on and the one that reads as a technicality until it bites. A positioned box with "z-index: auto" is painted as a unit, at the same level as one with "z-index: 0", but it does *not* make a stacking context: its own positioned descendants are hoisted out and sorted against its siblings rather than against each other. So a descendant with "z-index: -1" paints behind an ancestor whose z-index is auto and in front of one whose z-index is 0, from the same markup. Collapsing auto onto 0 gives a page where that descendant is simply not visible, which looks like a missing box rather than like a stacking bug.

What each step of Appendix E is for

The eight steps exist to make three guarantees that tree order alone does not. Backgrounds of every ordinary block in a subtree are painted before any of the text in it, so a later sibling's background cannot cover an earlier one's words. Floats are a layer of their own between the two, so a floated image sits over the block backgrounds it overlaps and under the text that runs around it. And everything positioned is painted after everything that is not, which is what makes "position: relative" with no offsets at all a way to lift a box above its neighbours — a fact that looks like an accident of the specification and is relied on constantly.

What is not done

Opacity and transforms also create stacking contexts and are not implemented, so neither appears here, and step 2 is a background image, which nothing draws yet. Every other step of §E.2 is present, reduced to the primitives this engine emits.

type Options

type Options struct {
	// Page is the sheet. The zero value is A4 with a 20mm margin.
	//
	// The faces are not here. They are on Input, because a document brings its
	// own with @font-face and the set it is laid out in is the caller's library
	// with the document's faces over it — see Input.Fonts and Built.Fonts.
	Page PageSize
	// MinScale is the floor §6.1 puts under scale-to-fit. A document that had
	// to be shrunk past it is refused rather than produced illegibly. Zero uses
	// the default of 0.5.
	MinScale float64
	// MinFontSizePt is the floor under an effective font size, in points. Zero
	// uses the default of 6.
	MinFontSizePt float64
	// AllowScaleUp lets an underfull page be enlarged to fill the sheet. It is
	// off by default because it is surprising and it degrades images.
	AllowScaleUp bool
}

Options is what a caller can say about the sheet and about how hard the engine may try to make a document fit on it.

type Outer

type Outer uint8

Outer is how a box participates in its parent's formatting context: as a block, as something in a line, or not at all.

const (
	// OuterNone is "display: none". The element and everything inside it
	// produce no boxes, which is different from producing an invisible one:
	// nothing inside is laid out, measured or painted.
	OuterNone Outer = iota
	// OuterBlock takes a line of its own.
	OuterBlock
	// OuterInline sits in a line with its siblings.
	OuterInline
)

func (Outer) String

func (o Outer) String() string

type PageSize

type PageSize struct {
	// Width and Height are the whole sheet.
	Width, Height style.Unit
	// Margin is the space left around the content.
	Margin Edges
}

PageSize is the sheet a document is laid out onto.

func PageSizePt

func PageSizePt(w, h float64) PageSize

PageSizePt builds a page size from a width and height in points, which is how paper is conventionally measured.

func (PageSize) Content

func (p PageSize) Content() Size

Content is the area a document is laid out in: the sheet minus its margins.

func (PageSize) WithMarginPt

func (p PageSize) WithMarginPt(m float64) PageSize

WithMarginPt returns the page with a uniform margin in points.

type Point

type Point = paragraph.Point

Geometry and the item type itself.

type Policy

type Policy map[Rule]Severity

Policy is a caller's choice of severity per rule. A rule absent from it keeps its default.

type PositionScheme

type PositionScheme uint8

PositionScheme is the value of the position property.

It is named for the scheme rather than simply "Position" because a box also has a position in the geometric sense, and a field called Position holding "relative" beside a field called BorderRect holding a rectangle is a sentence nobody reads correctly twice.

const (
	// PositionStatic is the normal flow: the box goes where the flow puts it and
	// the four offset properties do not apply to it at all.
	PositionStatic PositionScheme = iota
	// PositionRelative lays the box out in the normal flow and then offsets it
	// visually, leaving the space it occupied behind.
	PositionRelative
	// PositionAbsolute takes the box out of the flow entirely and resolves it
	// against the padding box of its nearest positioned ancestor.
	PositionAbsolute
	// PositionFixed is PositionAbsolute against the page box. In a medium with a
	// viewport that can scroll the two differ; in a paged one the viewport is
	// the page, and they do not.
	PositionFixed
)

func (PositionScheme) String

func (p PositionScheme) String() string

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder collects findings under a policy.

It applies the policy at the point of recording rather than at the end, so a rule set to Ignore costs nothing to raise — which matters because the callers are the inner loops of layout, and a guardrail that is expensive to check is one that gets checked less often than it should.

func NewRecorder

func NewRecorder(p Policy) *Recorder

NewRecorder prepares to collect findings under a policy. A nil policy uses the defaults.

func (*Recorder) Count

func (r *Recorder) Count(rule Rule) int

Count returns how many times a rule fired, including occurrences that were deduplicated or dropped past the bound.

This is what lets a report say "flex-wrap was dropped 412 times" while showing the finding once, which is more useful than either the one or the four hundred on their own.

func (*Recorder) Failed

func (r *Recorder) Failed() bool

Failed reports whether anything fired at Error severity.

func (*Recorder) Findings

func (r *Recorder) Findings() []Finding

Findings returns what was recorded, in a deterministic order.

The order is by rule, then by where in the input the finding came from, then by message — the same shape internal/finding.Sort gives every validator, because two runs over the same document must produce the same slice and several of the stages above range over maps.

func (*Recorder) Report

func (r *Recorder) Report(rule Rule, src Source, message string) bool

Report records a finding, applying the policy.

It reports whether the finding was at Error severity, which is what a caller in a position to stop early wants to know.

func (*Recorder) ReportDetail

func (r *Recorder) ReportDetail(f Finding) bool

ReportDetail records a finding that carries more than a message.

The Severity field of f is ignored — it is filled in from the policy, because a caller raising a finding should not be able to decide how serious it is. That decision belongs to whoever is rendering.

func (*Recorder) Truncated

func (r *Recorder) Truncated() bool

Truncated reports whether the bound stopped findings being recorded, so a caller never presents a cut list as a complete one.

type Rect

type Rect struct {
	X, Y, W, H style.Unit
}

Rect is a rectangle: a position and an extent.

It is stored as position-and-size rather than as two corners because layout computes it that way — a box is placed and then given a width — and because a rectangle with a negative extent is then representable, which is worth being able to see rather than normalising away.

func (Rect) Bottom

func (r Rect) Bottom() style.Unit

func (Rect) Contains

func (r Rect) Contains(inner Rect) bool

Contains reports whether inner lies entirely within r.

It is what the overflow guardrails ask, so its edge behaviour matters: a box exactly filling its container is contained, since a border that lands on the boundary is not overflow.

func (Rect) Empty

func (r Rect) Empty() bool

Empty reports whether the rectangle encloses nothing.

func (Rect) Inset

func (r Rect) Inset(e Edges) Rect

Inset shrinks a rectangle by an edge on each side, which is what stepping in from a border box to a padding box to a content box does.

The result is clamped to a non-negative extent. A padding wider than the box it is inside produces an empty content box rather than an inside-out one, and an inside-out rectangle is the shape that makes every later comparison give a plausible wrong answer.

func (Rect) Intersect

func (r Rect) Intersect(o Rect) Rect

Intersect is the rectangle two rectangles have in common.

Two rectangles that do not overlap produce one with a negative extent, which Empty reports and every consumer treats as nothing. It is not normalised to zero, for the reason Rect itself is not: a value that says "less than nothing" is worth being able to see.

The arithmetic cannot wrap. Every operation on a Unit saturates at the ends of its range, so the worst an intersection of two extreme rectangles can produce is a saturated extent — never a negative width that reads as a very large one, which is the failure that would turn a clip into an amplifier.

func (Rect) Origin

func (r Rect) Origin() Point

func (Rect) Outset

func (r Rect) Outset(e Edges) Rect

Outset grows a rectangle by an edge on each side, which is what stepping out to a margin box does. It is not clamped: a negative margin legitimately produces a margin box smaller than the border box.

func (Rect) Right

func (r Rect) Right() style.Unit

func (Rect) Size

func (r Rect) Size() Size

func (Rect) String

func (r Rect) String() string

type ReplacedContent

type ReplacedContent struct {
	// Image is the decoded picture.
	Image image.Image

	// Width and Height are the intrinsic dimensions: the image's own pixel
	// size, one image pixel to one CSS pixel.
	Width, Height style.Unit

	// Ratio is the intrinsic ratio, width divided by height, and is zero when
	// there is none. It is kept as a number rather than recomputed from the two
	// dimensions because CSS 2.1 §10.3.2 distinguishes an element that has a
	// ratio from one that has dimensions, and a format can supply either
	// without the other.
	Ratio float64

	// Key identifies the source bytes, so that a document naming one file
	// twenty times embeds one image. It is a hash of the bytes rather than the
	// reference, which also collapses the same picture reached by two names.
	Key string

	// Pixels is the image's pixel count, which is what the document budget was
	// charged.
	Pixels int64
}

ReplacedContent is what makes an element replaced: content with dimensions of its own that layout sizes rather than lays out.

It is a value on the box rather than a subclass of it because "replaced" is a property of the element's content and not of its box type — a replaced element is still inline or block, still floats, still positions, and every rule about those applies unchanged. The one thing that differs is where its width and height come from, which is what this carries.

type ResourceResolver

type ResourceResolver interface {
	// Resolve returns the bytes of the resource a document referred to.
	Resolve(ref string) ([]byte, error)
}

ResourceResolver turns a reference written in a document into bytes.

It is deliberately not an io.Reader factory or a URL fetcher. A resolver is handed the reference exactly as the document wrote it, with no scheme and no leading slash — those are refused before it is called — and returns the whole resource or an error. Returning an error is normal: a missing image is a finding, not a failure of the render.

A resolver must bound what it returns. The engine caps what it will decode, but it cannot cap what a resolver allocates before returning, so a resolver reading from anywhere unbounded has to impose its own limit. DirResolver does.

type Rule

type Rule string

Rule identifies a guardrail.

The identifiers are the ones §6 names, and they are strings rather than an enumeration because they travel: they are what a caller matches on, what a configuration file names, and what a report is grouped by. An integer would be none of those.

const (
	// RuleUnsupportedProperty is a declaration parsed and then not applied.
	// §6.3 argues this is the highest-value guardrail for the least cost, and
	// it is: an engine implementing a subset *will* ignore declarations, and a
	// page where flex-wrap was dropped is plausible and wrong.
	RuleUnsupportedProperty Rule = "unsupported-property"
	// RuleUnsupportedElement is an element the engine does not lay out.
	RuleUnsupportedElement Rule = "unsupported-element"
	// RuleUnsupportedSelector is a selector outside the implemented subset, so
	// the rule using it never applied.
	RuleUnsupportedSelector Rule = "unsupported-selector"
	// RuleUnsupportedAtRule is an at-rule the engine does not act on.
	RuleUnsupportedAtRule Rule = "unsupported-at-rule"
	// RuleUnsupportedValue is a value that is correct CSS the engine cannot
	// resolve — a unit needing font metrics, a colour in a space needing
	// conversion.
	RuleUnsupportedValue Rule = "unsupported-value"

	// RuleInvalidMarkup and RuleInvalidCSS are input the engine refused. They
	// are not the same as the unsupported rules and must not be reported as
	// them: one says the author wrote something wrong, the other says the
	// engine does not do something. An author sent to the wrong one of those
	// looks for the wrong thing.
	RuleInvalidMarkup Rule = "invalid-markup"
	RuleInvalidCSS    Rule = "invalid-css"

	// RuleFontFallback is a requested family that was not available, so the
	// text was set in something else. The metrics and the line breaks differ,
	// and nothing about the page says so.
	RuleFontFallback Rule = "font-fallback"
	// RuleUnsupportedScript is text this engine cannot break or order
	// correctly. §6.3 makes it an error by default, and is right to: unbroken or
	// unordered text still looks like text, so the failure mode looks like
	// success.
	RuleUnsupportedScript Rule = "unsupported-script"
	// RuleGlyphMissing is a character no available face has a glyph for. Tofu is
	// the purest form of silent garbage — a box where a letter should be, which
	// a reader blames on their PDF viewer.
	RuleGlyphMissing Rule = "glyph-missing"

	// The size thresholds of §6.1, which are checkable exactly because §5's
	// scaling is geometric: the effective size of an element is its natural size
	// times one number, so a threshold is a multiplication rather than an
	// iteration.
	//
	// RuleMinScale is the blunt one and probably the most useful: if the content
	// had to be shrunk past half to fit, the document is wrong, and no
	// per-element threshold is needed to say so.
	RuleMinScale Rule = "min-scale"
	// RuleMinFontSize is text that would be set below a legible size.
	RuleMinFontSize Rule = "min-font-size"

	// The layout-integrity rules of §6.2, which are about the geometry rather
	// than about a size.
	//
	// RuleUnbreakableOverflow is atomic content wider than the box holding it: a
	// long URL, a nowrap run, an oversized image. §6.2 calls it the classic
	// silent clip, and it is — the text is there, the box is there, and the part
	// past the edge is simply not drawn.
	RuleUnbreakableOverflow Rule = "unbreakable-overflow"
	// RuleTableColumnUnderflow is a table column narrower than the content in
	// it, so the content is cut off at the column edge.
	//
	// It is the table-shaped form of the silent clip §6.2 is named after, and it
	// has its own identifier because it has its own cause and its own fix: the
	// fixed table layout of §17.5.2.1 deliberately ignores what is in the cells,
	// so a column can end up narrower than its content and the specification
	// says so. That is a trade an author may want and may not know they made —
	// the table looks tidy and a word is missing from it.
	RuleTableColumnUnderflow Rule = "table-column-underflow"

	// RulePositionApproximated is a positioned box this engine placed by a
	// weaker rule than the one that applies to it.
	//
	// It is its own rule rather than an unsupported-value because the value *was*
	// supported: "position: absolute" was honoured, the box was taken out of the
	// flow and given offsets, and only the rectangle those offsets were measured
	// from is not the one §10.1 names. That produces the most deceptive shape of
	// wrongness this engine has — a box that is manifestly positioned, in a place
	// that looks deliberate, some tens of pixels from where the author put it.
	// Telling an author their declaration was ignored would send them looking for
	// a feature that is there.
	RulePositionApproximated Rule = "position-approximated"

	// RuleControlApproximated is a form control laid out as the static box a
	// printed page has, where that box is visibly not the widget a browser
	// would draw.
	//
	// It is its own rule rather than an unsupported-element because the element
	// *was* laid out: it has its size, its chrome and whatever text the markup
	// gave it, and it takes part in the flow like any other box. What is missing
	// is a widget — a slider's thumb at a position, the mark inside a checked
	// box, the options a drop-down is not showing — and every one of those is a
	// piece of information the document carries and the page does not.
	//
	// It fires only where the difference shows. A text field with a border round
	// its value is what a browser prints, so a text field says nothing; a rule
	// that fired on every control would be one nobody reads.
	RuleControlApproximated Rule = "control-approximated"

	// RuleOverflowPage is content outside the page box after scaling.
	//
	// It should be unreachable given §5: the scale is computed so that
	// everything fits. So it is a self-check as much as a guardrail — if it
	// fires, the scale computation is wrong, which is worth hearing about far
	// more than the overflow itself.
	RuleOverflowPage Rule = "overflow-page"

	// RuleResourceBlocked is a file the document referred to and this engine did
	// not load: there was no resolver, the reference named a URL scheme, it
	// pointed outside the directory the resolver was rooted at, or it could not
	// be read.
	//
	// It is its own rule rather than an unsupported-element, because the element
	// *was* laid out — an <img> whose image did not arrive is still a box, still
	// takes part in the line, and still shows its alt text. What is missing is
	// the picture, and a page with a rectangle of nothing where a chart belongs
	// is the silent failure §6 is named after.
	RuleResourceBlocked Rule = "resource-blocked"
	// RuleImageUndecodable is a resource that was loaded and did not become an
	// image: a format this engine has no decoder for, bytes that do not parse,
	// or a picture larger than it will decode.
	//
	// The last of those is the one worth having a rule for. A ten-kilobyte PNG
	// may declare sixty thousand pixels on a side, and refusing it is the only
	// safe answer — but the refusal has to be visible, or a document that a
	// caller believes contains a photograph contains a gap instead.
	RuleImageUndecodable Rule = "image-undecodable"
	// RuleFontUndecodable is a font an @font-face named, that arrived, and that
	// did not become a face: a container this engine does not unwrap, a format
	// hint naming one, or bytes that are not a font program.
	//
	// It is separate from RuleResourceBlocked for the same reason
	// image-undecodable is: "the file did not arrive" and "the file arrived and
	// was not usable" send an author to different places, and a font is the
	// case where the second is most likely — the web serves woff2 to everything
	// and this engine reads sfnt.
	RuleFontUndecodable Rule = "font-undecodable"

	// RuleLimit is a resource guard that tripped, or a run that was cancelled.
	//
	// It is spelled the same as internal/finding.LimitRule, and deliberately so:
	// every other part of pdf0 already reports "we stopped short" under that
	// identifier, and a caller that distinguishes "the input is bad" from
	// "pdf0 could not finish" should not have to learn a second spelling for
	// the second one.
	RuleLimit Rule = "limit"
)

The rules this engine can currently report.

This is deliberately *not* the whole catalogue of §6. The geometry rules — min-font-size, unbreakable-overflow, overflow-page and the rest — arrive with the layout that can violate them, each together with the test that plants a violation and watches it fire. §6.5 asks for exactly that, and declaring a rule before anything can raise it would produce the decoration it warns against: an identifier in a catalogue that has never been seen to fire proves nothing at all. TestEveryRuleIsReachable holds that line.

func AllRules

func AllRules() []Rule

AllRules returns every rule this engine can report, in a fixed order.

It exists so that a caller can enumerate what it might be told, and so that the tests can require each one to have been seen to fire.

type Severity

type Severity uint8

Severity is what a rule does when it fires.

const (
	// Ignore drops the finding entirely. It is not the default for anything —
	// a caller has to ask for silence.
	Ignore Severity = iota
	// Warn records the finding and lets the render finish.
	Warn
	// Error records the finding and makes the render fail, so no document is
	// returned. This is for the cases where a produced document would be worse
	// than none: one that looks finished and is not.
	Error
)

func (Severity) String

func (s Severity) String() string

type Size

type Size struct{ W, H style.Unit }

Size is an extent.

type Source

type Source struct {
	// HTMLOffset is a byte offset into the HTML, or -1.
	HTMLOffset int
	// CSSOffset is a byte offset into the stylesheet, or -1.
	CSSOffset int
	// Sheet names which stylesheet CSSOffset is in, when there is more than
	// one. It is empty for the document's own.
	Sheet string
}

Source says where in the input a finding came from.

Both offsets are byte offsets into the document the caller supplied, and both are -1 when they do not apply. That is what lets a caller point an author at the markup or the stylesheet rather than at a description of it, and it cannot be recovered afterwards — which is why the html, css and style packages carry offsets at all.

func AtCSS

func AtCSS(offset int) Source

func AtHTML

func AtHTML(offset int) Source

AtHTML and AtCSS build the two common cases.

type Stylesheet

type Stylesheet struct {
	// Name identifies the sheet in a finding — a filename, usually. It is empty
	// for the document's own <style> content.
	Name string
	// Source is the CSS.
	Source string
}

Stylesheet is one stylesheet with a name to report against.

type TextRun

type TextRun struct {
	Text string
	// Face is what it is set in, and Size the font size.
	Face *shape.Face
	Size style.Unit
	// X is the offset from the left of the line box, and Width the advance.
	X, Width style.Unit
	// Box is the inline box the text came from, which carries the colour and
	// the decoration painting will need.
	Box *Box
	// Decorations are the lines ruled across this run: CSS 2.1 §16.3.1's
	// underline, overline and line-through. They are on the run rather than
	// derived from Box at paint time because a decoration belongs to whichever
	// *ancestor* declared it, and that box's colour is the line's colour — see
	// textdecoration.go, where the difference between propagating and inheriting
	// is worked through.
	Decorations []textDecoration
	// RTL says the run reads right to left, so its glyphs are drawn from the
	// right edge of its box towards the left and its brackets are mirrored.
	//
	// It is on the run rather than derived from the text because it is not a
	// property of the text: a run of punctuation between two Hebrew words is
	// right-to-left and has nothing in it that says so. The algorithm decided it
	// from the neighbours, which are other runs by the time anything paints this
	// one.
	RTL bool
	// LetterSpacing is what letter-spacing added after each character of this
	// run. It is carried into painting as well as into the width because the two
	// have to agree: the width decided where the next run starts, and glyphs
	// drawn without the same spacing would leave a gap the size of the whole
	// run's spacing before it.
	LetterSpacing style.Unit
	// Offset is §9.4.3's relative displacement, accumulated over the inline
	// boxes this run sits inside.
	//
	// It is on the run rather than on a single fragment because a <span> that
	// spans a line break has one fragment per line: a line box holds runs, and
	// the box's own background and border are a Boxes entry on each of the lines
	// it reaches. Both are moved by the same displacement and are given it
	// separately — this one at paint time, the fragment's in absolutise, because
	// a background image is placed against the rectangle its box is drawn at.
	Offset Point
	// Shift is how far this run's own baseline sits below the line box's,
	// which is §10.8.1's vertical-align applied to the inline box the run came
	// from. It is negative for a run that is raised.
	//
	// It is a length on the run rather than a line of its own because a line
	// box has exactly one baseline — §10.8's alignment is *against* that
	// baseline — and every run on the line is placed relative to it. Folding it
	// into Offset would have been shorter and is wrong: Offset is §9.4.3's
	// relative positioning, which moves a box after layout without changing
	// anything about the line, whereas this displacement is part of what
	// decided the line's height.
	Shift style.Unit
}

TextRun is a piece of text on a line, set in one face at one size.

type TileImage

type TileImage struct {
	// Clip is the area painted. Nothing is drawn outside it, including the part
	// of a tile that reaches past it.
	Clip Rect
	// Tile is the first tile: its position and its size.
	Tile Rect
	// StepX and StepY are the distance to the next tile on each axis, and are
	// always greater than zero. An axis that does not repeat has a step of the
	// tile's own size and a Clip no wider than one tile, so the neighbouring
	// cells fall outside — which is what keeps a backend from needing a case for
	// "does not repeat".
	StepX, StepY style.Unit

	Image image.Image
	// Key identifies the source bytes, so a backend embeds one picture once.
	Key string
}

TileImage paints a picture repeatedly across a rectangle.

It is one operation for a whole tiling rather than one per tile, and that is a decision about safety rather than about tidiness. The number of tiles is (area / tile size), and a stylesheet chooses both: "background-size: 0.001px" with "repeat" over an A4 page is four hundred billion placements. An engine that emitted one operation each would allocate until it died on a document an attacker wrote, and no cap on the *document* bounds it, because nothing in the document says the number.

So the tiling leaves layout as a description — where the first tile is, how far apart they are, and what area they may be drawn into — and the count appears nowhere. A backend expands it with the mechanism it has: PDF has tiling patterns, which are exactly this value. The count is still checked against maxBackgroundTiles before this is built, because whatever expands it is entitled not to be handed four hundred billion cells.

func (TileImage) Tiles

func (t TileImage) Tiles() (cols, rows int)

Tiles is how many tiles touch the clip on each axis, which is what a consumer that expands them needs before it starts.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL