Escape

@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?)

signature
function layout(options?: LayoutOptions): Target<'layout', Promise<Layout>>

A render target like any other, resolving the display list instead of bytes.

LayoutOptions

signature
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

signature
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

signature
type Op = RectOp | LineOp | ImageOp | TextOp | MarkOp

One 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

signature
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: number

paint 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:

painting a page
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.