Skip to content
API

Display structure

The display structure is what the engine produces. Painters consume it and never re-derive layout, so a preview and an export cannot disagree.

Coordinates are points, with the origin at the top left of the page.

LayoutOutput

pub struct LayoutOutput {
pub pages: Vec<Page>,
pub fonts: Vec<FontRefEntry>,
pub assets: Vec<Asset>,
pub warnings: Vec<Warning>,
pub navigation: Navigation,
}

fonts is the table font_id indexes. Both painters and the PDF writer resolve ids through it, so a run’s face is the same face on both sides. An entry names the family, the face and style names, the slope and weight it answers for, and variations: where on its file’s axes it sits, in user space. A variable file’s named styles are one file at several locations, and the location is what distinguishes them.

assets is the table that Image.asset and Background.asset index. An entry is the url an image was named by, in the content tree or in the stylesheet, and what its header reported: pixel dimensions and resolution. That is all layout read.

warnings is the whole run’s, style compilation included. See diagnostics.

navigation holds the outline of the headings. The PDF writer reads it. The wire does not carry it. The links are on the pages.

pub struct Navigation {
pub outline: Vec<OutlineEntry>,
}
pub struct OutlineEntry {
pub title: String,
pub level: u8,
pub place: PageBox,
pub children: Vec<OutlineEntry>,
}

A PageBox is a box on one page. page is the index of the page in the book, from 0. x, y, width, and height are in points.

outline has the headings of the book in reading order, nested by level. level is from 1 to 6. children has the headings after the entry that have a higher level, up to the next heading whose level is the same or less. title is the text of the heading on one line, and place is the box of the heading on its page. A heading in a quotation, a list, or a table is not in the outline.

Page

pub struct Page {
pub number: u32,
pub side: Side,
pub width: f32,
pub height: f32,
pub sections: Vec<NodeId>,
pub items: Vec<DrawItem>,
pub links: Vec<Link>,
}

number is the page number, counting from 1. side is recto or verso; books open on a right-hand page, so odd numbers are recto. width and height are the page size, in points, per page, because a book may change page size at a named page.

sections names the content-tree sections that appear on the page, in the order their content appears on it. A chapter that ends mid-page is followed there by the next one opening, so the page names both. A blank page names none.

Layout decides which page a chapter starts on, so a host builds a contents page or a page range beside a chapter out of this. Session::folios answers the same question for a handful of nodes without handing back a page, and answers it in both numbers: see sessions.

items is in paint order: by layer, and inside one layer in the order the blocks are written. A painter walks the list from the start and paints each item over the ones before it. A painter does not sort.

Every item names its own layer. z-index on a block sets that layer, and the CSS subset describes it under Layers. Two layers are the engine’s own. DrawItem::PAGE_BACKGROUND is the background of a page, under every layer a stylesheet can name. DrawItem::PAGE_FURNITURE is a page number or a running head, over every one of them. The TypeScript reader exports the same two numbers as PAGE_BACKGROUND and PAGE_FURNITURE.

pub struct Link {
pub areas: Vec<PageBox>,
pub to: LinkTo,
}
pub enum LinkTo {
Place { node: NodeId, place: PageBox },
Uri(String),
}

links on a Page has one Link for each link on that page, in the order of the draw items. A page with no link has an empty links. A link that continues on the next page has a Link on each of the two pages.

areas has one box for each line of the link on the page. A box runs from the first glyph to the end of the last glyph, and from the ascent to the descent of the font. The text of ::before and ::after on the link is in the box. So a link with no text of its own has a box around the text of its ::after. The space between two lines is in no box. The PDF writer writes each box as one link annotation, so the preview and the PDF have the same links.

to is where the link goes. Place is a place in the book. node is the element that the link names. place is the box of that element on the page where the element starts, and the box is as wide as the column. place.page is the index of that page in the book, so a host can fetch the page from the link alone. Uri is a url outside the book, such as https://example.com.

A link that names nothing in the book is on no page, and the run warns once for each url.

DrawItem

Five variants. A painter that handles all five can draw any page the engine produces.

Text

Text {
x: f32,
y: f32,
font_id: u16,
size: f32,
width: f32,
text: String,
source: String,
source_map: Vec<u32>,
origin: Option<SourceRange>,
pseudo_element: Option<NodeId>,
features: Features,
color: Color,
glyphs: Vec<Glyph>,
layer: i32,
}

One run of shaped glyphs sharing a font, a size and a baseline. y is the baseline, not the top of the line box.

width is how far the glyphs of the run advance, in points. The run ends at x + width. A painter that puts selectable text over the glyphs needs it to find where the last glyph ends.

"My dear Mr. Bennet," replied his wife, "how can you be so tiresome!You must know that I am thinking of his marrying one of them.""Is that his design in settling here?""Design! Nonsense, how can you talk so! But it is very likely that he mayfall in love with one of them, and therefore you must visit him as soon ashe comes.""I see no occasion for that. You and the girls may go, or you may sendthem by themselves, which perhaps will be still better, for as you are ashandsome as any of them, Mr. Bingley may like you the best of the party.""My dear, you flatter me. I certainly have had my share of beauty, but Ido not pretend to be anything extraordinary now. When a woman has fivegrown-up daughters, she ought to give over thinking of her own beauty.""In such cases, a woman has not often much beauty to think of.""But, my dear, you must indeed go and see Mr. Bingley when he comesinto the neighbourhood.""It is more than I engage for, I assure you.""But consider your daughters. Only think what an establishment itwould be for one of them. Sir William and Lady Lucas are determinedto go, merely on that account, for in general, you know, they visit no new-comers. Indeed you must go, for it will be impossible for us to visit him ifyou do not.""You are over-scrupulous, surely. I dare say Mr. Bingley will be very gladto see you; and I will send a few lines by you to assure him of my heartyconsent to his marrying whichever he chooses of the girls; though I mustthrow in a good word for my little Lizzy.""I desire you will do no such thing. Lizzy is not a bit better than theothers; and I am sure she is not half so handsome as Jane, nor half so good-humoured as Lydia. But you are always giving her the preference.""They have none of them much to recommend them," replied he; "theyare all silly and ignorant like other girls; but Lizzy has something more ofquickness than her sisters."Pride and Prejudice2
page 2
Every text run of a page, with a rule drawn on the y the display structure gave it. The rules sit on the letters rather than above them, which is what a baseline is. Edit the manuscript and the rules move with the runs.

text is the string the glyphs were shaped from, and the glyphs’ ranges index it. It travels with the run because only the shaper knew which glyph came from which character. A painter that draws characters rather than glyphs draws these.

source is what the author wrote, where text-transform or small capitals made that differ from what was shaped, and empty where the two are the same. Extraction, selection and copy and paste return it, so a chapter title set in capitals is read back in the case it was written in. source_map records the offset in source of every byte boundary of text, and is empty alongside it: a glyph’s range taken through the map is the source that glyph stands for.

origin is where the run was written: the content node it was shaped from, and the bytes of that node’s own text it stands for. The runs that name one node tile it, so a byte of the manuscript falls in exactly one of them, and a paragraph broken over a page turn gives two ranges that meet at the break. A hyphen a line break drew stands for nothing anybody wrote, so its range is empty. It is None on text that the engine adds to the book:

  • A page number or a running head.
  • A scene break’s ornament.
  • A table’s header row where it repeats on a later page.
  • The marker of a list item.

The text of ::before and ::after has an origin. See ids of pseudo-elements.

pseudo_element is the id of the pseudo-element that the run belongs to. The run is then a drop cap, part of the first line of a paragraph, or the text of ::before or ::after. It is None on every other run. The origin of a drop cap or of a first line names the manuscript text that the run stands for.

color is what the run is filled with: three eight-bit channels and an eight-bit alpha, where 255 is opaque. Where the stylesheet gives no color, the run is opaque black. The alpha includes the opacity of the blocks that the run is in.

features is what the run was shaped with beyond the features the shaper turns on for every run. A painter that draws characters asks the face for the same ones, as it pins the face’s variations, or the browser picks its own glyphs and sets them at positions measured for others. A painter that draws glyphs has the answer already.

Features

pub struct Features(Option<Arc<FeatureSet>>);
pub struct FeatureSet {
pub small_caps: bool,
pub settings: Vec<FeatureSetting>,
}
pub struct FeatureSetting {
pub tag: [u8; 4],
pub value: u32,
}

The shaper turns a default set of features on for every run. The fonts page lists them. A run that adds nothing to those defaults carries no FeatureSet, which is most of the text in a book. Features::small_caps and Features::settings answer for such a run too, so a painter reads them without testing for a set first. A book uses a handful of sets across hundreds of thousands of runs, so the runs share the sets between them.

small_caps is the face’s own small capitals, which font-variant-caps: small-caps asks for. The tag is smcp.

settings is what the stylesheet named, in the order the shaper reads them. tag is a four-character OpenType feature tag. value is the setting: 0 is off, 1 is on, and a higher number selects the alternate of that number. A tag that appears twice takes the value of the last one. The CSS subset says which properties a stylesheet writes these with.

Glyph

pub struct Glyph {
pub id: u32,
pub x: f32,
pub range: Range<u32>,
}

id is a glyph id in the run’s font, not a character. x is absolute, per glyph. Kerning and justification mean no two glyphs are uniformly spaced, so there is no advance for a painter to accumulate.

range is the byte range in the run’s text that this glyph stands for. A ligature spans several characters; a decomposed cluster puts several glyphs on one range. A painter that treats the range as a mapping rather than a bijection handles both.

SourceRange

pub struct SourceRange {
pub node: NodeId,
pub range: Range<u32>,
}

node is a content-tree node id. range indexes that node’s own text as the frontend read it, before text-transform or a synthesized small capital changed what was shaped, so a run’s origin and its source cover the same bytes.

A host maps a glyph back to the manuscript in two steps: the glyph’s range through the run’s source_map, where there is one, then that offset plus origin.range.start. A cursor in the manuscript goes the other way: find the run whose node and range contain the byte, and the glyph on it whose range does.

A node id is the engine’s own name for a place in the book, and a host that handed over markdown has a file and a byte of it instead. The content tree turns one into the other: the node one byte of a source was read into, and the source one node was read from. A cursor becomes a node, and the runs that name it are on the page the cursor is set on; a run under the pointer becomes a place in the manuscript.

Ids of pseudo-elements

::first-letter, ::first-line, ::before, and ::after style parts of a page that are not nodes of the content tree. The CSS subset describes them under Pseudo-elements. Each pseudo-element of an element has an id of its own.

No content node has the id of a pseudo-element. The id of a content node is less than 536870912 (2^29), and the id of a pseudo-element is 2147483648 (2^31) or more. The id comes from the element and the pseudo-element. The same book and stylesheets give the same id. A stylesheet edit that adds or removes a pseudo-element does not change the id of a content node.

The runs of a pseudo-element have its id in pseudo_element:

pseudo-elementrunsorigin
::first-letterThe drop cap.The letter in the manuscript.
::first-lineThe first line of the paragraph, after the drop cap.The text in the manuscript.
::before, ::afterThe text that the pseudo-element adds. On a block, the text is in a box.The id of the pseudo-element, and a range in the text that it adds.

A host maps a glyph of a drop cap or a first line to the manuscript in the same way as a glyph of any other run.

The following methods take the id of a pseudo-element:

methodresult
NodeId::pseudo_elementThe element and the pseudo-element.
NodeId::elementThe element that the pseudo-element belongs to.
Session::inspectThe rules that match the pseudo-element, its computed style, and its boxes.
Book::source_ofNothing, because no source contains the pseudo-element.

Session::hit returns the id of a pseudo-element for a point on its text or in its box. An inline element on a first line is inside ::first-line. A point on the text of that inline element returns the inline element.

The inspection of a pseudo-element names the element in element and the pseudo-element in pseudo_element, as ::first-letter. The boxes of ::before and ::after on a block are the boxes of the block that they add. The boxes of the other pseudo-elements are the area that their text covers on each page. For a drop cap, that area is the rectangle of the letter.

The inspection also carries element_node, the id of that element. A host that lists a pseudo-element beside its element moves from one to the other with it. Session::inspect takes that id and answers for the element. The id of the element does not follow from the id of the pseudo-element, so a host reads it from the inspection.

Rect

Rect { x: f32, y: f32, w: f32, h: f32, color: Color, layer: i32 }

A filled rectangle: rules, borders, backgrounds. color is the fill, and its alpha is how opaque the fill is.

Image

Image { x: f32, y: f32, w: f32, h: f32, asset: u32, alpha: u8, layer: i32 }

A placed image. asset indexes LayoutOutput::assets, and the pixels are the host’s. Layout never decoded them, so the placement comes from a header probe and the painter decodes the file.

w and h are what layout decided, which is the intrinsic size at the header’s own resolution unless the page had no room for it. A painter scales the image into that box, and does not read the header again.

alpha is how opaque the image is, from 0 to 255. It is the opacity of the image and of the blocks around it. At 255 the image is opaque.

Background

Background {
x: f32,
y: f32,
w: f32,
h: f32,
radii: Corners,
tile_x: f32,
tile_y: f32,
tile_w: f32,
tile_h: f32,
repeat: bool,
asset: u32,
alpha: u8,
layer: i32,
}

An image painted behind a box: the page’s own box, or a block’s border box. asset indexes LayoutOutput::assets, the same table Image indexes.

x, y, w, and h are the box, and radii rounds its corners. A painter clips the image to the rounded box and draws nothing outside it. alpha is how opaque the image is, as on Image.

tile_x, tile_y, tile_w, and tile_h are where one copy of the image goes, which can reach outside the box. Layout resolved background-size and background-position into them.

Where repeat is true, that copy tiles. A painter steps the tile by tile_w across and tile_h down, in both directions, until the box is covered.

Rounded

Rounded {
x: f32,
y: f32,
w: f32,
h: f32,
radii: Corners,
ring: Edges,
color: Color,
layer: i32,
}

A filled box with rounded corners: the background or the border of a block that has a border-radius. A block with square corners draws Rect items instead.

The outline is the box x, y, w, h, with each corner rounded by its radius in radii. Where ring is zero on all four edges, the whole outline is filled. That is a background.

Otherwise the fill is the band between the outline and a second outline inside it. That is a border. The second outline is the box ring.top in from the top edge, ring.right in from the right edge, and so on. Corners::inside gives its corners: each radius loses the width of the edge it runs along, and no radius goes below zero.

Where the edges of a border have one color, the border is one item. Where they have different colors, each edge is an item of its own, with a ring that is zero on the other three edges.

Corners

pub struct Corners {
pub top_left: Radius,
pub top_right: Radius,
pub bottom_right: Radius,
pub bottom_left: Radius,
}
pub struct Radius {
pub x: f32,
pub y: f32,
}

A corner is a quarter ellipse. x is its radius along the top or bottom edge, and y is its radius along the left or right edge, both in points. A corner with a radius of zero is square. Layout resolved the percentages, and the radii of two corners on one edge never add up to more than the length of that edge.

Writing a painter

For each page, set up a coordinate system in points with the origin top-left, then walk items in order. The list is already in paint order. Do not sort by layer.

For text, resolve font_id through LayoutOutput::fonts, pin the entry’s variations, set the size, fill with the run’s color, and place each glyph by id at its absolute x on the run’s baseline y. Do not shape or kern. The engine has done both, and a painter that re-shapes will disagree with the export.

A painter that cannot place glyphs by id, and draws characters instead, draws text and turns the run’s features on, which is how it reaches the glyphs the engine measured. It reads source back rather than text wherever the reader gets the words: selection, extraction and copy and paste.

For an image, resolve asset through LayoutOutput::assets and draw the file that url stands for into the box the item gives. A url nothing can supply still takes up its box on the page.

For a background, resolve asset the same way, clip to the box with its corners rounded by radii, and draw the file into the tile. Where repeat is true, step the tile across and down until the box is covered. For a url nothing can supply, draw nothing. A placeholder behind the text reads as part of the page.

Fill every color at its alpha, and draw every image at its alpha. At 255 the fill or the image is opaque, and at 0 it does not show.

For a rounded box, draw the outline with each corner as a quarter ellipse. Where ring is not zero, add the second outline to the same path and fill the path by the even-odd rule. The even-odd rule leaves the inside of the second outline empty.

The SVG painter in fleuron is a worked example. See the preview.