@quario/layout 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,22 +1,94 @@
1
- # Changelog
2
-
3
- All notable changes to @quario/layout are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [0.3.0] - 2026-09-07
11
-
12
- ### Added
1
+ # @quario/layout
2
+
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **An image's box is now as wide as the container it sits in.** A
8
+ `background` or `border*` on an image item hugged the picture and now spans
9
+ the content width — or the slot's share inside a split — with `fit` sizing
10
+ the picture and `align` placing it inside, the way a text item's box has
11
+ always been drawn. The picture itself does not move. Breaking: a report that
12
+ relied on a border hugging a logo now draws that border across the full
13
+ width, and no declaration asks for the old shape.
14
+ - **A page of the display list no longer carries `width` and `height`.** Read
15
+ the list's own `width` and `height` instead: they are the paper every page of
16
+ the report is drawn on, and every page's pair was a copy of them. A report has
17
+ one page size for the whole render — that has always been so, and page size is
18
+ a target option rather than something a document declares — so a page is
19
+ identified and positioned, and asked nothing about its size. A page now
20
+ carries `{ number, total, ops, boxes }`.
21
+
22
+ If you consume the list yourself, the fix is one substitution:
23
+
24
+ ```js
25
+ canvas.width = list.width * 2; // was list.pages[i].width * 2
26
+ canvas.height = list.height * 2; // was list.pages[i].height * 2
27
+ ```
28
+
29
+ `paint()` is unchanged in shape — it still takes one page — and it now fills
30
+ the white over the whole canvas rather than over the page's own rectangle.
31
+ **Size the canvas to the page before you call it**, as the sizing example above
32
+ does: the ops are in the page's own coordinates, so a canvas short of the paper
33
+ clips them whatever the fill covers. That was always so; it is now written down
34
+ in the README and on `paint()` itself.
35
+
36
+ - **`Layout.pages` is typed as a non-empty list.** A render always lays out at
37
+ least one page — the first page opens whether or not anything renders on it —
38
+ so `pages[0]` no longer needs a check for a list that cannot exist. Runtime
39
+ behaviour is unchanged; this only states in the type what was already true.
40
+ - **`Paper`** — the page box a report is drawn on, `{ width, height }` in points,
41
+ and what a display list's own `width` and `height` are. `Layout` extends it, so
42
+ `list.width` is unchanged and the pair now has a name to refer to.
43
+
44
+ ### Patch Changes
45
+
46
+ - **A page's right and bottom edge could be left unpainted.** Where a caller
47
+ rounds the backing store up to a whole device pixel — A4 at 100% asks for
48
+ 793.71 and takes 794 — `paint()` filled its white over the page's exact
49
+ rectangle and left the fraction past it transparent. On a canvas with white
50
+ behind it that is invisible; on one without, a hairline of whatever is behind
51
+ the canvas showed along two edges. The fill now covers the whole canvas.
52
+ - **Every published README says where the documentation is.** Each package now
53
+ carries a Documentation section pointing at the reference, at the report schema
54
+ that normatively specifies what a report may declare, and at the package's own
55
+ API. The paragraphs that used to end on an unstated contract — the event
56
+ stream's field semantics, the style vocabulary, page columns, the Content
57
+ Security Policy a fragment with images needs, the formula mangling, and each
58
+ target's own contract — link the page that states it. Every link is an absolute
59
+ URL, so it resolves from the npm package page as readily as from an installed
60
+ copy.
61
+ - Updated dependencies
62
+ - quario@0.8.0
63
+
64
+ ## 0.4.0
65
+
66
+ ### Minor Changes
67
+
68
+ - **Per-run typography, decoration and highlights.** A styled run resolves its
69
+ own face, size and colour, and a line takes the largest size among its runs.
70
+ `underline` and `strikethrough` are drawn per run — over that run's width, in
71
+ that run's colour — where they used to stroke the whole line in the first
72
+ piece's colour. A run's `background` paints a highlight rectangle behind its
73
+ text; a cell's own background is still painted once, at cell scope.
74
+ - **`checkFonts` is public.** It checks the shape of a `fonts` mapping without
75
+ loading a parser, and takes an optional name to prefix a failure with, the
76
+ way `pageBox` does — so a surface validating its own `fonts` property can
77
+ report the mistake against that property rather than against
78
+ `options.fonts`.
79
+ - **An outline `Mark` carries only what it declares.** Every mark shipped an
80
+ extra `titled` boolean — bookkeeping for whether a header had claimed the
81
+ entry — which was never part of the `Mark` interface and which nothing
82
+ reads. It is the open group instance's own state now, and no longer travels
83
+ on the object a consumer receives.
84
+
85
+ ## 0.3.0
86
+
87
+ ### Minor Changes
13
88
 
14
89
  - The text join reads a cell's own `currency` code, ahead of the instance's
15
90
  default, so every target built on this package presents a per-cell
16
91
  denomination.
17
-
18
- ### Changed
19
-
20
92
  - **A group instance that renders nothing no longer takes up space.** A group
21
93
  whose header and footer items all resolve `visible: false`, with nothing
22
94
  visible under it either, used to open the same half-line gap as any other
@@ -27,7 +99,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
27
99
  its rows stay in the aggregates. A group that declares `break: "page"` or
28
100
  `reset: "page"` still starts its page either way. Every document with such a
29
101
  group renders slightly shorter than it did.
30
-
31
102
  - **Numbers presented through `format` now show a fixed two fraction digits,
32
103
  matching every other target** — `1,000.00` where `1,000` was rendered,
33
104
  `21.00%` where `21%` was, and a currency's own minor units in place of a
@@ -35,15 +106,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
35
106
  a formatted cell is now up to three characters wider than it was: a line
36
107
  that just fitted can wrap, which can move a page break in a PDF, the viewer,
37
108
  or an editor preview. Nothing else about wrapping changed.
38
-
39
- ### Fixed
40
-
41
109
  - **An image failure now names the item that asked for the bytes.** A file too
42
110
  short to carry a size failed saying only that the size could not be read,
43
111
  naming no item, so a report with two pictures gave no way to tell which one
44
112
  was bad. The message is now prefixed with the item's `source` path, as every
45
113
  other render error is.
46
-
47
114
  - **An image the browser cannot decode no longer costs the whole page.** A
48
115
  PNG or JPEG whose pixel data is corrupt past the size in its header lays out
49
116
  like any other — the size is all that is read of it — and used to throw out
@@ -52,9 +119,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
52
119
  drawn as nothing and the page is drawn around it, so what a bad image costs
53
120
  is the image.
54
121
 
55
- ## [0.2.0] - 2026-09-05
122
+ ## 0.2.0
56
123
 
57
- ### Added
124
+ ### Minor Changes
58
125
 
59
126
  - **A spanning cell is drawn as one box** across the columns it covers,
60
127
  starting where the first of them starts. It takes no part in allocating their
@@ -62,21 +129,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
62
129
  empty table still shows its geometry. A spanning row too tall for the page it
63
130
  is on slices like any other, each slice following the geometry of the page or
64
131
  page-column strip it lands in.
65
-
66
132
  - **`valign`** places a cell's lines, or a slot's lines or picture, in the slack
67
133
  its row or split leaves over its own content: middle halves it, bottom takes
68
134
  it. The box does not move, and a picture is not scaled. A row too tall for
69
135
  any page, sliced across pages, paints from the top.
70
-
71
- ### Changed
72
-
73
136
  - **A row's box is drawn by the row's cells.** A box declared on a table row
74
137
  used to be one rect across the summed column widths; it is now each covered
75
138
  cell's own, so a row's `borderBottom` still reads as one continuous edge
76
139
  while a row's `borderLeft` becomes an edge on every cell rather than one at
77
140
  the row's outer left. A row's border also occupies height now, as a cell's
78
141
  always has, so a bordered row is taller by its border's width.
79
-
80
142
  - **Measuring TrueType families now needs `fontkit`, not `@pdf-lib/fontkit`.**
81
143
  Install `fontkit` instead; nothing else about `options.fonts` changes. The
82
144
  old package's bundle crashed with a bare `ReferenceError` on any OpenType
@@ -85,9 +147,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
85
147
  besides. Those faces now measure where they used to throw. Which scripts a
86
148
  face supports remains the font's and the parser's to answer, not this
87
149
  package's.
88
-
89
- ### Fixed
90
-
91
150
  - **A group header no longer strands above a split.** A split cannot be broken
92
151
  across a page, so a header that introduces one has to keep the whole of it
93
152
  company — but the keep-together test measured a split by its first two lines
@@ -102,9 +161,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
102
161
  always has before content no page can hold: it degrades to plain paginated
103
162
  flow and repeats nothing.
104
163
 
105
- ## [0.1.0] - 2026-09-03
164
+ ## 0.1.0
106
165
 
107
- ### Added
166
+ ### Minor Changes
108
167
 
109
168
  - **The paged display list.** `layout({ page, fonts })` is a render target
110
169
  resolving every page of the report — ops in points from the top-left, one
@@ -112,12 +171,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
112
171
  marks, and on an unlicensed render the marking. The algorithm is the PDF
113
172
  target's typesetter, with its home moved here so a preview can paint the
114
173
  same pages the document has.
115
-
116
174
  - **One measurer.** Base-14 families measure against `@pdf-lib/standard-fonts`'
117
175
  AFM metrics over WinAnsi, TrueType families against `@pdf-lib/fontkit`'s
118
176
  shaping of the same bytes the PDF embeds, so a preview breaks its lines
119
177
  where the document breaks them.
120
-
121
178
  - **A box survives a page break.** An item or table row no page can hold
122
179
  whole is drawn as slices, and a slice carries the box sides the break left
123
180
  it: the top belongs to the first slice, the bottom to the last, and left and
@@ -126,7 +183,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
126
183
  short one draws. Each slice reserves the bottom padding it may yet owe, so
127
184
  that edge stays above the bottom margin, and each carries a hit box, so an
128
185
  item that breaks is selectable on every page it reaches.
129
-
130
186
  - **`paint` and `hit`.** A Canvas 2D painter for one page, and the hit-test
131
187
  that maps a point on a page back to the schema node drawn there.
132
188
 
@@ -145,13 +201,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
145
201
 
146
202
  - **`pageBox`.** The page-size table and its validation, in one home for the
147
203
  PDF target, the viewer and the editor.
148
-
149
204
  - **`PX_PER_POINT`.** How big a point is on screen — 96 dpi over PostScript's
150
205
  72 — so every surface that shows a page shows it at the same size, and a
151
206
  zoom is a factor on top of it.
152
-
153
- ### Changed
154
-
155
207
  - **A split slot's box now fills the split's height.** Before, a slot's
156
208
  background and border were exactly as tall as that slot's own content and
157
209
  padding asked for, so any slot shorter than the tallest one drew a box that
@@ -164,13 +216,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
164
216
  does, and a slot's hit box follows its painted one. A report that relied on
165
217
  the short box — a slot background used as a chip beside taller content —
166
218
  now draws it full height; give that content a narrower slot of its own.
167
-
168
219
  - **A date string under `format: "date"` now presents as a date.** The text
169
220
  join presents the kind through the engine's `format()` helper, which now
170
221
  revives the two read forms. See the `quario` changelog for the forms.
171
-
172
- ### Fixed
173
-
174
222
  - **A nested group's footer keeps the page columns.** Inside a page-column
175
223
  region, the first footer of a group nested in it ended the region: the
176
224
  strips collapsed and every band after it was laid out across the page, as
package/README.md CHANGED
@@ -38,8 +38,9 @@ list.pages[0].ops; // what to draw on the first page, in order
38
38
  list.pages[0].boxes; // which schema node was drawn where
39
39
  ```
40
40
 
41
- Every coordinate is in PostScript points from the page's top-left corner. A page carries its
42
- `{ number, total }`, its ops`rect`, `line`, `image`, `text` (the string to draw, the face
41
+ Every coordinate is in PostScript points from the page's top-left corner. `list.width` and
42
+ `list.height` are the paper every page is drawn on a report has one, so a page does not carry its
43
+ own size. A page carries its `{ number, total }`, its ops — `rect`, `line`, `image`, `text` (the string to draw, the face
43
44
  it is set in, and — for a built-in family only — the per-character advances the measurer laid it
44
45
  out with) and, on an unlicensed render, one `mark` — and its `boxes`, one per schema node drawn,
45
46
  named by the node's path.
@@ -61,14 +62,16 @@ import { hit, paint } from "@quario/layout";
61
62
 
62
63
  const canvas = document.querySelector("canvas");
63
64
  const page = list.pages[0];
64
- canvas.width = page.width * 2; // 2 device pixels per point
65
- canvas.height = page.height * 2;
65
+ canvas.width = list.width * 2; // 2 device pixels per point
66
+ canvas.height = list.height * 2;
66
67
  await paint(canvas.getContext("2d"), page, { scale: 2, fonts });
67
68
 
68
69
  hit(page, x, y); // -> { path, x, y, w, h } or null
69
70
  ```
70
71
 
71
- `paint` draws a white page and every op in order. Images are decoded from the bytes on the list
72
+ `paint` draws a white page and every op in order. Size the canvas to the page before you call it:
73
+ the ops are in the page's own coordinates, so a smaller canvas clips them, and the white is filled
74
+ over the whole canvas. Images are decoded from the bytes on the list —
72
75
  no `img-src` grant, no URLs. `hit` answers which schema node was drawn at a point: the smallest box
73
76
  containing it.
74
77
 
@@ -85,9 +88,16 @@ positioning, so a browser may kern by a fraction more. Where a line breaks, how
85
88
  and where a page ends are the layout's — one measurer, so preview and document agree.
86
89
 
87
90
  `PX_PER_POINT` is how big a point is on screen — 96 dpi over PostScript's 72 — so a surface showing
88
- a page at 100% sizes its canvas `page.width * PX_PER_POINT` CSS pixels wide and paints at
91
+ a page at 100% sizes its canvas `list.width * PX_PER_POINT` CSS pixels wide and paints at
89
92
  `PX_PER_POINT * devicePixelRatio`. Any zoom is a factor on top of that.
90
93
 
94
+ ## Documentation
95
+
96
+ [The quario documentation](https://getquario.com/docs/) is the reference.
97
+ The [report schema](https://getquario.com/docs/reference/report-schema/) is the normative
98
+ specification of what a report may declare, and
99
+ [`@quario/layout`](https://getquario.com/docs/reference/layout/) is this package's own API.
100
+
91
101
  ## License
92
102
 
93
103
  quario is commercial software; evaluation is free and fully featured, with output marked as
package/lib/canvas.js CHANGED
@@ -24,7 +24,7 @@
24
24
  * `y` descending, the way a screen reads. Points throughout.
25
25
  */
26
26
  import { baseSans } from "./fonts.js";
27
- import { BLACK, dressed, shift } from "./style.js";
27
+ import { BLACK, shift } from "./style.js";
28
28
 
29
29
  /** @typedef {import('./page.js').Frame} Frame */
30
30
  /** @typedef {import('./text.js').Line} Line */
@@ -71,27 +71,55 @@ import { BLACK, dressed, shift } from "./style.js";
71
71
  */
72
72
  let blank = (box, render) => ({ ...box, settings: render, y: 0, fresh: true });
73
73
 
74
- // Text decoration in the text colour. Thickness and offset come from the
75
- // line's ascender (the face metric already measured for baseline placement).
76
- // Empty lines (no width) draw nothing. Shared by the listing adapter and the
77
- // layout suite's recorder so both exercise the same path.
78
- /** @type {(line: Line) => Color} */
79
- let ink = (line) => (line.pieces[0] && line.pieces[0].color) || BLACK;
74
+ // A styled run's `background`, painted behind its text as a highlight over
75
+ // that run's width and the line's full height -- the same rectangle a cell's
76
+ // own background is, one stretch narrower. All of a line's highlights are
77
+ // painted before any of its glyphs, so a rectangle behind one run can never
78
+ // land on top of the run before it. Shared with the layout suite's recorder
79
+ // for the reason `decorateLine` is.
80
+ /**
81
+ * @type {(rect: (color: Color, x: number, y: number, w: number, h: number) => void,
82
+ * line: Line, left: number, yTop: number) => void}
83
+ */
84
+ let highlightLine = (rect, line, left, yTop) => {
85
+ let cursor = left;
86
+ for (let piece of line.pieces) {
87
+ if (piece.bg) rect(piece.bg, cursor, yTop - line.h, piece.w, line.h);
88
+ cursor += piece.w;
89
+ }
90
+ };
80
91
 
81
- /** @type {(line: Line) => boolean} */
82
- let wantsDeco = (line) => !!line.w && dressed(line);
92
+ // Text decoration, drawn per piece: over that piece's width, in that piece's
93
+ // colour. A rule across the whole line in the first piece's colour is what
94
+ // this used to be, and it is exactly what "underline a word" cannot mean
95
+ // (ADR 0061). Thickness and offset stay the line's, from the ascender already
96
+ // measured for baseline placement, so one line's rules sit at one height
97
+ // whatever sizes it mixes. Empty lines (no width) draw nothing. Shared by the
98
+ // listing adapter and the layout suite's recorder so both exercise the same
99
+ // path.
100
+ /**
101
+ * @type {(stroke: (x1: number, x2: number, y: number, thickness: number, color: Color) => void,
102
+ * piece: Line['pieces'][number], line: Line, left: number, baseline: number) => void}
103
+ */
104
+ let decoratePiece = (stroke, piece, line, left, baseline) => {
105
+ let thickness = Math.max(line.asc / 12, 0.5);
106
+ let right = left + piece.w;
107
+ let color = piece.color || BLACK;
108
+ if (piece.underline) stroke(left, right, baseline - line.asc * 0.12, thickness, color);
109
+ if (piece.strikethrough) stroke(left, right, baseline + line.asc * 0.35, thickness, color);
110
+ };
83
111
 
84
112
  /**
85
113
  * @type {(stroke: (x1: number, x2: number, y: number, thickness: number, color: Color) => void,
86
114
  * line: Line, left: number, baseline: number) => void}
87
115
  */
88
116
  let decorateLine = (stroke, line, left, baseline) => {
89
- if (!wantsDeco(line)) return;
90
- let thickness = Math.max(line.asc / 12, 0.5);
91
- let right = left + line.w;
92
- let color = ink(line);
93
- if (line.underline) stroke(left, right, baseline - line.asc * 0.12, thickness, color);
94
- if (line.strikethrough) stroke(left, right, baseline + line.asc * 0.35, thickness, color);
117
+ if (!line.w) return;
118
+ let cursor = left;
119
+ for (let piece of line.pieces) {
120
+ decoratePiece(stroke, piece, line, cursor, baseline);
121
+ cursor += piece.w;
122
+ }
95
123
  };
96
124
 
97
125
  /**
@@ -112,7 +140,10 @@ let listing = (box, render) => {
112
140
 
113
141
  // Open a fresh page and put the cursor at its top.
114
142
  let newPage = () => {
115
- page = { width: canvas.width, height: canvas.height, ops: [], boxes: [] };
143
+ // No geometry: the paper is the list's, written once from this canvas's
144
+ // own frame, because the page box is fixed for the whole document (ADR
145
+ // 0069). A page carries what is per-page.
146
+ page = { ops: [], boxes: [] };
116
147
  pages.push(page);
117
148
  canvas.y = canvas.top;
118
149
  canvas.fresh = true;
@@ -172,6 +203,7 @@ let listing = (box, render) => {
172
203
  /** @type {Canvas['drawLine']} */
173
204
  let drawLine = (line, x, yTop, avail, align) => {
174
205
  let left = x + shift(align, avail - line.w);
206
+ highlightLine(rect, line, left, yTop);
175
207
  let cursor = left;
176
208
  for (let piece of line.pieces) {
177
209
  writePiece(piece, line, cursor, yTop);
@@ -307,4 +339,4 @@ let stamp = (canvas, text) => {
307
339
  };
308
340
  };
309
341
 
310
- export { adopt, decorateLine, listing, measuring, stamp };
342
+ export { adopt, decorateLine, highlightLine, listing, measuring, stamp };
package/lib/fonts.js CHANGED
@@ -161,9 +161,9 @@ let useFontkit = async () => {
161
161
  }
162
162
  };
163
163
 
164
- /** @type {(name: string, def: any) => string} */
165
- let asFamily = (name, def) => {
166
- let path = "options.fonts." + name;
164
+ /** @type {(name: string, def: any, at: string) => string} */
165
+ let asFamily = (name, def, at) => {
166
+ let path = at + "." + name;
167
167
  if (!def || typeof def !== "object")
168
168
  throw Error(path + ": expected { regular, bold?, italic?, boldItalic? }");
169
169
  if (def.regular == null) throw Error(path + ".regular: required");
@@ -241,7 +241,9 @@ let remembered = (source, key, make) => {
241
241
  // resolvable once the family is named.
242
242
  /** @type {(fontkit: any, name: string, def: any) => Face[]} */
243
243
  let familyOf = (fontkit, name, def) => {
244
- let path = asFamily(name, def);
244
+ // Always the factory's own option here: loading happens behind `layout()`,
245
+ // and a surface that named it something else has already validated it.
246
+ let path = asFamily(name, def, "options.fonts");
245
247
  let family = name.toLowerCase();
246
248
  /** @type {Face[]} */
247
249
  let faces = [];
@@ -278,15 +280,23 @@ export async function loadFonts(custom) {
278
280
  }
279
281
 
280
282
  /**
281
- * Check the shape of a host's font mapping without loading a parser: the
282
- * factory call is where a malformed option is reported.
283
+ * Check the shape of a host's font mapping without loading a parser.
284
+ *
285
+ * `at` names the property in the message, the way `pageBox` takes one: the
286
+ * factory call reports `options.fonts`, but a surface element validating its
287
+ * own `fonts` property before any render has a different name for the same
288
+ * value, and a host mistake is named on the property that carries it.
289
+ *
290
+ * Only the shape. A face that will not parse, or a missing parser, is found
291
+ * while the report is measured and is a render failure there.
283
292
  *
284
293
  * @param {any} custom `options.fonts`, or null.
294
+ * @param {string} [at] What to call it in the message.
285
295
  */
286
- export let checkFonts = (custom) => {
296
+ export let checkFonts = (custom, at = "options.fonts") => {
287
297
  if (custom == null) return;
288
- if (typeof custom !== "object") throw Error("options.fonts: expected a record of families");
289
- for (let [name, def] of Object.entries(custom)) asFamily(name, def);
298
+ if (typeof custom !== "object") throw Error(at + ": expected a record of families");
299
+ for (let [name, def] of Object.entries(custom)) asFamily(name, def, at);
290
300
  };
291
301
 
292
302
  // What a declared `family` normalises to before it is looked up: lower case,
package/lib/index.d.ts CHANGED
@@ -159,12 +159,13 @@ export interface Box {
159
159
  * One page of the list. Coordinates are points from the page's top-left
160
160
  * corner, `y` descending; `number` and `total` are what `page.number` and
161
161
  * `page.total` read on it.
162
+ *
163
+ * A page carries what is per-page and nothing else: the `Paper` it is drawn on
164
+ * is the list's (ADR 0069).
162
165
  */
163
166
  export interface Page {
164
167
  number: number;
165
168
  total: number;
166
- width: number;
167
- height: number;
168
169
  ops: Op[];
169
170
  boxes: Box[];
170
171
  }
@@ -178,11 +179,23 @@ export interface Mark {
178
179
  y: number;
179
180
  }
180
181
 
181
- /** The display list: every page of the document, laid out. */
182
- export interface Layout {
182
+ /**
183
+ * The [paper](../../../CONTEXT.md#paper) a report is drawn on, in points: one
184
+ * width and one height for a whole render. The page box is fixed once the
185
+ * render opens, so this is the list's and a page carries none of its own (ADR
186
+ * 0069) — a consumer sizing a page reads it here.
187
+ */
188
+ export interface Paper {
183
189
  width: number;
184
190
  height: number;
185
- pages: Page[];
191
+ }
192
+
193
+ /** The display list: every page of the document, laid out, on one `Paper`. */
194
+ export interface Layout extends Paper {
195
+ /** Every page, and never none: the band flow opens the body's first page
196
+ * whether or not anything renders on it. Typed as a non-empty tuple so a
197
+ * consumer reading `pages[0]` needs no check for a list that cannot exist. */
198
+ pages: [Page, ...Page[]];
186
199
  /** The group tree, in document order — the PDF target's bookmarks. */
187
200
  marks: Mark[];
188
201
  }
@@ -206,11 +219,17 @@ export const PX_PER_POINT: number;
206
219
  * order. `scale` is device pixels per point. `fonts` is the same record
207
220
  * given to `layout()`, so a TrueType family draws in its own face.
208
221
  *
222
+ * **Size the canvas to the page first** — `Layout.width` and `Layout.height`
223
+ * times `scale`, the paper every page of the list is drawn on. The ops are in
224
+ * the page's own coordinates, so a canvas short of that clips them, and the
225
+ * white is filled over the whole canvas rather than over a rectangle a page
226
+ * carries (ADR 0069).
227
+ *
209
228
  * It awaits its faces and images before it draws, and it draws whatever
210
- * happened in between: a canvas re-sized under a call still in flight is
211
- * filled at that call's own `scale`, not the size it now has. A caller that
212
- * repaints one canvas at changing scales owns that, by not letting a
213
- * superseded call reach a canvas still on screen.
229
+ * happened in between: a canvas re-sized under a call still in flight has its
230
+ * ops placed at that call's own `scale` on a canvas of the size it now has. A
231
+ * caller that repaints one canvas at changing scales owns that, by not letting
232
+ * a superseded call reach a canvas still on screen.
214
233
  */
215
234
  export function paint(
216
235
  ctx: CanvasRenderingContext2D,
@@ -232,3 +251,13 @@ export function pageBox(
232
251
  page?: LayoutPage,
233
252
  at?: string,
234
253
  ): { width: number; height: number; margin: number };
254
+
255
+ /**
256
+ * Check the shape of a `fonts` mapping, throwing where it is malformed. `at`
257
+ * prefixes a failure with the option's name, so a surface validating its own
258
+ * `fonts` property names that rather than `options.fonts`.
259
+ *
260
+ * Only the shape: a face that will not parse, or a missing parser, is found
261
+ * while the report is measured and is a render failure there.
262
+ */
263
+ export function checkFonts(fonts?: LayoutFonts | null, at?: string): void;
package/lib/index.js CHANGED
@@ -29,6 +29,7 @@ import { settings } from "./settings.js";
29
29
 
30
30
  export { PX_PER_POINT, hit, paint } from "./paint.js";
31
31
  export { pageBox } from "./page.js";
32
+ export { checkFonts } from "./fonts.js";
32
33
 
33
34
  // The options are described once, in the hand-written public declarations, and
34
35
  // read back here — a second copy in JSDoc is a copy that drifts.