@quario/layout
The paged typesetting the PDF target writes and the viewer and editor paint, as a display list. Reach for it to build a paginating surface of your own.
layout(options?)
function layout(options?: LayoutOptions): Target<'layout', Promise<Layout>>A render target like any other, resolving the display list instead of bytes.
LayoutOptions
interface LayoutOptions {
page?: LayoutPage
fonts?: LayoutFonts
}
interface LayoutPage {
size?: 'A4' | 'letter' | [number, number]
margin?: number | { top: number; right: number; bottom: number; left: number }
}
interface LayoutFontFamily {
regular: Uint8Array | ArrayBuffer
bold?: Uint8Array | ArrayBuffer
italic?: Uint8Array | ArrayBuffer
boldItalic?: Uint8Array | ArrayBuffer
}
type LayoutFonts = Record<string, LayoutFontFamily>Pass the same fonts record here and to pdf({ fonts }) so a preview and the
document break their lines in the same places. Keep the record and its buffers
across renders, since they’re the identity the layout remembers a face by.
It takes two options: page and fonts. Any other key, or one of the wrong
type, throws a TypeError naming the path. An array in place of the fonts
record throws too, rather than reading as families named 0, 1 and 2.
Paper, Layout, Page, Box, Mark
interface Paper {
width: number
height: number
}
interface Layout extends Paper {
pages: [Page, ...Page[]]
marks: Mark[]
}
interface Page {
number: number
total: number
ops: Op[]
boxes: Box[]
lines: TextLine[]
tables: TextTable[]
}
interface TextLine {
path: string | undefined
from: number
to: number
heading?: number
join?: string
}
interface Box { path: string; x: number; y: number; w: number; h: number }
interface TextTable { rows: TextRow[] }
interface TextRow { head?: true; cells: TextCell[] }
interface TextCell { from: number; to: number; span?: number }
interface Mark { title: string; label?: string; depth: number; page: number; x: number; y: number }Paper is the page box the whole render takes. One report has one page size:
it’s a target option, not something a document declares. So the pair lives on
the list, and a page carries none of its own. Size a canvas from list.width
and list.height, never from a page.
pages is never empty: the band flow opens the body’s first page whether or
not anything renders on it, so pages[0] needs no guard.
boxes maps a schema path to the rectangle it drew, which is the selection
seam. marks is the outline the PDF target writes as bookmarks. A mark’s y
reads down from the page top, as an op’s does. The PDF target flips it for
each bookmark itself, so a host reading marks uses it as it stands. label
is the instance’s declared label, present only where it declared one. The
title falls back to header text, but only a label is a place an href
of # names.
lines is the page’s text in reading order. Each entry names the text ops
ops[from] up to ops[to] of one laid-out line, and the schema node it
belongs to. A table row’s cells are a line each, and a page band’s lines follow
the body’s. heading is the outline depth, on the first line of a group header
only. join continues this line into the next as one line of text. It holds
what a wrap dropped, or a space to the next cell or slot. It’s absent where the
next line starts a new one. The viewer reads this list to lay selectable text
over a page.
tables groups the page’s lines into the tables drawn there. A cell names
the range of lines it drew, and span the columns it covers where more than
one. An empty cell is an empty range, so later cells keep their column. A table
that breaks restates its headings, so each page’s part opens with a head
row. Every instance of one repeated table shares one set of column widths
across the render.
Op
type Op = RectOp | LineOp | ImageOp | TextOp | MarkOpOne page’s drawing instructions, in paint order. FaceRef on a text op names
the family, the variant index, and whether the face is host-supplied bytes.
That decides how a painter draws the run.
An image op carries the item’s rendered alt, absent where it declared none.
A text op also carries href beside its x, y, w and h, where the run
it draws linked. A run wrapping across two lines is two ops, which is two
rectangles. Two stretches pointing at different places never merge into one
phrase, even where they draw identically. A phrase is the rectangle a painter
attaches a link to.
Helpers
function paint(
ctx: CanvasRenderingContext2D,
page: Page,
options?: { scale?: number; fonts?: LayoutFonts },
): Promise<void>
function hit(page: Page, x: number, y: number): Box | null
function destinations(marks: readonly Mark[]): Map<string, Mark>
function destination(href: string, byLabel: Map<string, Mark>): string | Mark | null
function pageBox(
page?: LayoutPage,
at?: string,
): {
width: number
height: number
margin: number | { top: number; right: number; bottom: number; left: number }
}
function fontOf(font: FaceRef, size: number): string
function checkFonts(fonts?: LayoutFonts | null, at?: string): void
function loadFontkit(): Promise<unknown>
const PX_PER_POINT: numberpaint draws one page to a canvas, filling white over the whole canvas first.
A page’s ops sit in the page’s own coordinates, so size the canvas to the
paper before you call it. A canvas short of the paper clips them, whatever
the fill covers:
import { layout, paint } from '@quario/layout'
const list = await report.render(layout({ page: { size: 'A4' } }), data)
canvas.width = list.width * 2 // the paper's, never a page's
canvas.height = list.height * 2
await paint(ctx, list.pages[0], { scale: 2 })loadFontkit loads the optional font parser once and gives the same module to
every caller, which is how @quario/pdf embeds with the parser this package
measured with. A parser that won’t load reports options.fonts: the font parser did not load. Install fontkit to use TrueType families, carrying
whatever actually threw as cause. A failed dynamic import doesn’t prove the
package is absent.
fontOf returns the CSS font string paint draws a run in, for a host laying
its own text over the canvas at the same metrics.
hit returns the smallest box containing a point, measured from the top-left
corner, or null where the page has nothing there. pageBox validates a page
option and returns its geometry, with at prefixing a failure with the
option’s name.
destinations maps each declared label to the first mark carrying it.
destination says where one href goes: a URL, a mark, or null for a
label no instance declares. The PDF target and the viewer both use them.