@quario/layout 0.1.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/lib/fonts.js ADDED
@@ -0,0 +1,409 @@
1
+ /**
2
+ * The faces a layout can measure with — the one measurer (docs/adr/0039).
3
+ *
4
+ * Nothing here can write a document. The base-14 families measure against the
5
+ * AFM metrics `@pdf-lib/standard-fonts` carries, over WinAnsi, which is what
6
+ * pdf-lib measures against when it writes the same face — so a line breaks
7
+ * where the PDF breaks it. A host-supplied TrueType family measures against
8
+ * fontkit's shaping of the same bytes the PDF target embeds. Every face is
9
+ * loaded before the walk starts, so resolving a style to one during layout
10
+ * stays synchronous.
11
+ *
12
+ * A face answers three questions: how wide a string is at a size, how far a
13
+ * line's top sits above its baseline, and which code points it can draw. A
14
+ * base-14 face answers a fourth — what each code point's advance is — which
15
+ * the list carries so a painter can correct for the screen face standing in
16
+ * for it. A host's own face is nobody's stand-in and answers only the three.
17
+ */
18
+ import { Encodings, Font, FontNames } from "@pdf-lib/standard-fonts";
19
+
20
+ // The three families the vocabulary names, four variants each, indexed by
21
+ // `bold + 2 * italic` — the same ordering the schema's `bold`/`italic` flags
22
+ // produce. Unknown family names fall back to sans, as SCHEMA.md specifies.
23
+ let BASE = {
24
+ sans: [
25
+ FontNames.Helvetica,
26
+ FontNames.HelveticaBold,
27
+ FontNames.HelveticaOblique,
28
+ FontNames.HelveticaBoldOblique,
29
+ ],
30
+ serif: [
31
+ FontNames.TimesRoman,
32
+ FontNames.TimesRomanBold,
33
+ FontNames.TimesRomanItalic,
34
+ FontNames.TimesRomanBoldItalic,
35
+ ],
36
+ mono: [
37
+ FontNames.Courier,
38
+ FontNames.CourierBold,
39
+ FontNames.CourierOblique,
40
+ FontNames.CourierBoldOblique,
41
+ ],
42
+ };
43
+
44
+ /**
45
+ * Which face a piece of the list is set in: the family it resolved to, the
46
+ * variant index, and whether the bytes behind it came from the host. A painter
47
+ * maps the three to its own face — pdf-lib's embedded font, a CSS font string.
48
+ *
49
+ * `embedded` is the fact a painter cannot work out for itself. A host may map
50
+ * its own family onto `sans`, `serif` or `mono` (SCHEMA.md), so the family name
51
+ * does not say which kind of face this is, and the two kinds are drawn
52
+ * differently: a stand-in has to be corrected glyph by glyph, a host's own face
53
+ * must not be. See `paint.js`.
54
+ * @typedef {{ family: string, variant: number, embedded: boolean }} FaceRef
55
+ */
56
+ /**
57
+ * `shape` belongs to the base-14 faces alone: it exists so a painter can
58
+ * correct a [stand-in face](../../../CONTEXT.md#stand-in-face), and nothing
59
+ * stands in for bytes the host supplied. So **a face has `shape` exactly when
60
+ * its ref says `embedded: false`** — one fact, and the two constructors below
61
+ * each settle both halves of it, because a face whose ref disagreed with its
62
+ * own `shape` would have the list promise a painter advances it never carries
63
+ * (or carry advances no painter will place).
64
+ * @typedef {{ ref: FaceRef,
65
+ * widthOfTextAtSize: (text: string, size: number) => number,
66
+ * heightAtSize: (size: number) => number,
67
+ * getCharacterSet?: () => number[],
68
+ * shape?: (text: string, size: number) => { advances: number[] } }} Face
69
+ */
70
+ /** @typedef {{ families: Record<string, Face[]> }} Fonts */
71
+
72
+ /** @type {(text: string) => string[]} */
73
+ let codePoints = (text) => Array.from(text);
74
+
75
+ // A base-14 face: AFM widths and kerning over WinAnsi. The width of a string
76
+ // is the sum of its glyph widths plus the kerning between neighbours — the
77
+ // same arithmetic pdf-lib's standard-font embedder does, kept in step so the
78
+ // two measure alike. A glyph without a width is 250, as it is there.
79
+ //
80
+ // One glyph per code point, always: the text arrives WinAnsi-encodable because
81
+ // `printable` substituted everything else, the encoding is a one-to-one table,
82
+ // and there is no shaping to fold a pair into a ligature. That is what lets a
83
+ // painter place these advances against the characters of the source string.
84
+ /** @type {(family: string, variant: number, name: string) => Face} */
85
+ let standard = (family, variant, name) => {
86
+ let ref = { family, variant, embedded: false };
87
+ let font = Font.load(/** @type {any} */ (name));
88
+ let encoding = Encodings.WinAnsi;
89
+ /** @type {(text: string) => { code: number, name: string }[]} */
90
+ let glyphs = (text) =>
91
+ codePoints(text).map((char) => encoding.encodeUnicodeCodePoint(char.codePointAt(0) ?? 0));
92
+ /** @type {(glyph: { name: string }, next?: { name: string }) => number} */
93
+ let advance = (glyph, next) =>
94
+ (font.getWidthOfGlyph(glyph.name) || 250) +
95
+ (next ? font.getXAxisKerningForPair(glyph.name, next.name) || 0 : 0);
96
+ let ascender = font.Ascender || font.FontBBox[3];
97
+ return {
98
+ ref,
99
+ widthOfTextAtSize: (text, size) => {
100
+ let list = glyphs(text);
101
+ let total = 0;
102
+ for (let [i, glyph] of list.entries()) total += advance(glyph, list[i + 1]);
103
+ return (total * size) / 1000;
104
+ },
105
+ heightAtSize: (size) => (ascender * size) / 1000,
106
+ getCharacterSet: () => encoding.supportedCodePoints,
107
+ shape: (text, size) => {
108
+ let list = glyphs(text);
109
+ return { advances: list.map((glyph, i) => (advance(glyph, list[i + 1]) * size) / 1000) };
110
+ },
111
+ };
112
+ };
113
+
114
+ // A TrueType face: fontkit's shaping of the bytes, measured in the font's own
115
+ // units. pdf-lib shapes the same bytes with the same fontkit when it embeds
116
+ // them and advances by the same widths, so the two agree.
117
+ //
118
+ // No `shape`. Shaping is not one glyph per code point here — a ligature folds
119
+ // two characters into one glyph, an RTL run comes back in visual order, a
120
+ // combining mark advances by nothing — so an advance array has no character to
121
+ // belong to. A painter with these bytes draws the run whole and lets its own
122
+ // shaper place the glyphs, which is what pdf-lib does too.
123
+ /** @type {(family: string, variant: number, font: any) => Face} */
124
+ let embedded = (family, variant, font) => {
125
+ let ref = { family, variant, embedded: true };
126
+ let scale = 1000 / font.unitsPerEm;
127
+ /** @type {(text: string) => any[]} */
128
+ let glyphs = (text) => font.layout(text).glyphs;
129
+ let ascender = (font.ascent || font.bbox.maxY) * scale;
130
+ return {
131
+ ref,
132
+ widthOfTextAtSize: (text, size) => {
133
+ let total = 0;
134
+ for (let glyph of glyphs(text)) total += glyph.advanceWidth * scale;
135
+ return (total * size) / 1000;
136
+ },
137
+ heightAtSize: (size) => (ascender * size) / 1000,
138
+ getCharacterSet: () => font.characterSet,
139
+ };
140
+ };
141
+
142
+ // The base-14 metrics are immutable, so the twelve AFM parses happen once per
143
+ // process rather than once per render; each render still gets its own record,
144
+ // since a host's families are added to it.
145
+ /** @type {Record<string, Face[]> | null} */
146
+ let base = null;
147
+ /** @type {() => Record<string, Face[]>} */
148
+ let baseFamilies = () =>
149
+ (base ??= Object.fromEntries(
150
+ Object.entries(BASE).map(([family, names]) => [
151
+ family,
152
+ names.map((name, variant) => standard(family, variant, name)),
153
+ ]),
154
+ ));
155
+
156
+ // A render's own copy, so adding a host's families cannot reach the memo.
157
+ /** @type {() => Record<string, Face[]>} */
158
+ let loadBase = () => ({ ...baseFamilies() });
159
+
160
+ /**
161
+ * The base-14 sans regular, whatever a host mapped `sans` to. Reached only by
162
+ * the marking, which is the one thing on a page a host's font mapping must not
163
+ * reach — see `stamp` in canvas.js.
164
+ */
165
+ /** @type {() => Face} */
166
+ export let baseSans = () => baseFamilies().sans[0];
167
+
168
+ // Host-supplied TrueType families need a font parser, which is an optional
169
+ // peer: base-14 reports install nothing extra. Loaded only when `fonts` is
170
+ // passed, and a missing package is reported as what it is.
171
+ /** @type {() => Promise<any>} */
172
+ let useFontkit = async () => {
173
+ try {
174
+ return (await import("@pdf-lib/fontkit")).default;
175
+ } catch {
176
+ throw Error("options.fonts: install @pdf-lib/fontkit to measure TrueType families");
177
+ }
178
+ };
179
+
180
+ /** @type {(name: string, def: any) => string} */
181
+ let asFamily = (name, def) => {
182
+ let path = "options.fonts." + name;
183
+ if (!def || typeof def !== "object")
184
+ throw Error(path + ": expected { regular, bold?, italic?, boldItalic? }");
185
+ if (def.regular == null) throw Error(path + ".regular: required");
186
+ return path;
187
+ };
188
+
189
+ /** The bytes a host handed over, whichever of the two shapes it used. */
190
+ /** @type {(source: any) => Uint8Array} */
191
+ export let bytesOf = (source) => (source instanceof Uint8Array ? source : new Uint8Array(source));
192
+
193
+ /** @type {(fontkit: any, src: any, path: string, variant: string) => any} */
194
+ let parseOne = (fontkit, src, path, variant) => {
195
+ try {
196
+ return fontkit.create(bytesOf(src[variant]));
197
+ } catch (e) {
198
+ throw Error(path + "." + variant + ": " + /** @type {Error} */ (e).message);
199
+ }
200
+ };
201
+
202
+ /**
203
+ * The four variants of a family in the order `FaceRef.variant` indexes them —
204
+ * `bold + 2 * italic` — which is the one fact every painter's registry has to
205
+ * share with the measurer's.
206
+ */
207
+ export let VARIANTS = /** @type {const} */ (["regular", "bold", "italic", "boldItalic"]);
208
+
209
+ // Every face built from a host's bytes so far, so a family passed on every
210
+ // render is parsed once. The identity rule is `paint.js`'s `FACES`, to the
211
+ // letter — change one and change the other, or the measurer and the painter
212
+ // disagree about what a face is:
213
+ //
214
+ // • keyed on the host's own source object, never on `bytesOf(source)`, since
215
+ // a host may hand over an `ArrayBuffer` and `bytesOf` allocates a fresh
216
+ // view every call, so a bytes-keyed entry would never be found twice;
217
+ // • the inner key carries family and variant, because a `Face` closes over
218
+ // its `ref`: one file aliased across two families, or given as both a
219
+ // family's regular and its bold, is two faces. A variant falling back to
220
+ // the family's regular is keyed here too, under the regular's source — so
221
+ // all four faces of a `regular`-only family are stable across renders, and
222
+ // `CHARSETS` below keeps that font's cmap rather than rebuilding the set
223
+ // every render.
224
+ //
225
+ // The cache is only as good as the host's object stability, which is what
226
+ // `LayoutOptions.fonts` asks for. No promise caching, unlike `paint.js`:
227
+ // parsing is synchronous, so the first caller past the `useFontkit` await
228
+ // runs a whole family to completion before another resumes.
229
+ /** @type {WeakMap<object, Map<string, Face>>} */
230
+ let FACES = new WeakMap();
231
+
232
+ /** @type {(source: object) => Map<string, Face>} */
233
+ let registryOf = (source) => {
234
+ let registry = FACES.get(source);
235
+ if (!registry) FACES.set(source, (registry = new Map()));
236
+ return registry;
237
+ };
238
+
239
+ /** @type {(source: any, key: string, make: () => Face) => Face} */
240
+ let remembered = (source, key, make) => {
241
+ // A source that is not an object cannot be a `WeakMap` key. `asFamily` only
242
+ // proves `regular` is present, so a host may still hand over a path string
243
+ // or a number; those go straight to `make`, which reports them as the
244
+ // located `options.fonts.<name>.<variant>` mistake they are rather than as
245
+ // a raw "invalid value used as weak map key".
246
+ if (!source || typeof source !== "object") return make();
247
+ let registry = registryOf(source);
248
+ let face = registry.get(key);
249
+ // The entry is written after `make`, never before: a parse that throws is a
250
+ // host mistake that fails the render, so there is no hot path to protect and
251
+ // no rejection to reason about on a later one.
252
+ if (!face) registry.set(key, (face = make()));
253
+ return face;
254
+ };
255
+
256
+ // Missing variants fall back to the family's regular, so a face is always
257
+ // resolvable once the family is named.
258
+ /** @type {(fontkit: any, name: string, def: any) => Face[]} */
259
+ let familyOf = (fontkit, name, def) => {
260
+ let path = asFamily(name, def);
261
+ let family = name.toLowerCase();
262
+ /** @type {Face[]} */
263
+ let faces = [];
264
+ for (let [variant, key] of VARIANTS.entries())
265
+ faces.push(
266
+ remembered(def[key] ?? def.regular, family + "/" + variant, () =>
267
+ def[key] == null
268
+ ? { ...faces[0], ref: { ...faces[0].ref, variant } }
269
+ : embedded(family, variant, parseOne(fontkit, def, path, key)),
270
+ ),
271
+ );
272
+ return faces;
273
+ };
274
+
275
+ /** @type {(custom: any, families: Record<string, Face[]>) => Promise<void>} */
276
+ let loadCustom = async (custom, families) => {
277
+ if (!(custom && Object.keys(custom).length)) return;
278
+ let fontkit = await useFontkit();
279
+ for (let [name, def] of Object.entries(custom))
280
+ families[name.toLowerCase()] = familyOf(fontkit, name, def);
281
+ };
282
+
283
+ /**
284
+ * Load every face a layout can reach and return the registry `face()` reads.
285
+ *
286
+ * @param {any} custom `options.fonts`, or null.
287
+ * @returns {Promise<Fonts>}
288
+ */
289
+ export async function loadFonts(custom) {
290
+ let families = loadBase();
291
+ await loadCustom(custom, families);
292
+ return { families };
293
+ }
294
+
295
+ /**
296
+ * Check the shape of a host's font mapping without loading a parser: the
297
+ * factory call is where a malformed option is reported.
298
+ *
299
+ * @param {any} custom `options.fonts`, or null.
300
+ */
301
+ export let checkFonts = (custom) => {
302
+ if (custom == null) return;
303
+ if (typeof custom !== "object") throw Error("options.fonts: expected a record of families");
304
+ for (let [name, def] of Object.entries(custom)) asFamily(name, def);
305
+ };
306
+
307
+ // What a declared `family` normalises to before it is looked up: lower case,
308
+ // and nothing at all when the value is not a name. Exported because the report
309
+ // default is normalised once per render rather than once per cell, and two
310
+ // spellings of this would drift.
311
+ /** @type {(style: any) => string} */
312
+ export let familyName = (style) =>
313
+ typeof style.family === "string" ? style.family.toLowerCase() : "";
314
+
315
+ // Own-key lookup: the family name is render data, so `constructor` must
316
+ // fall back to sans rather than resolve an inherited member.
317
+ /** @type {(fonts: Fonts, name: string | null) => Face[]} */
318
+ let variantsOf = (fonts, name) =>
319
+ name && Object.hasOwn(fonts.families, name) ? fonts.families[name] : fonts.families.sans;
320
+
321
+ /** @type {(style: any) => number} */
322
+ let faceIndex = (style) => (style.bold ? 1 : 0) | (style.italic ? 2 : 0);
323
+
324
+ /**
325
+ * Resolve a style block to one face.
326
+ *
327
+ * `fallback` is the report default's family, which the canvas carries for the
328
+ * whole render (docs/adr/0033) — so text declaring no family is set in the
329
+ * document's.
330
+ *
331
+ * The fallback turns on whether a family was *declared*, never on whether the
332
+ * declared one resolves: a literal `family` is a non-empty string by the time
333
+ * the engine passes it, but an `=` expression is resolved at render and
334
+ * reaches here unchecked, so an item can declare `""`, a number or `null`.
335
+ * Those are the item's own declaration and fall to sans like any other name
336
+ * this layout cannot resolve (SCHEMA.md) — the document's family is for text
337
+ * that declared none. Own-key, because the block is render data.
338
+ *
339
+ * @type {(fonts: Fonts, style: any, fallback: string | null) => Face}
340
+ */
341
+ export function face(fonts, style, fallback) {
342
+ let declared = Object.hasOwn(style, "family");
343
+ return variantsOf(fonts, declared ? familyName(style) : fallback)[faceIndex(style)];
344
+ }
345
+
346
+ // The distance from a line's top to its baseline: the face's ascender.
347
+ /** @type {(font: Face, size: number) => number} */
348
+ export let ascOf = (font, size) => font.heightAtSize(size);
349
+
350
+ // Text measurement, against the face's own metrics.
351
+ /** @type {(font: Face, text: string, size: number) => number} */
352
+ export let width = (font, text, size) => font.widthOfTextAtSize(text, size);
353
+
354
+ // Printable ASCII, which every text face encodes and which almost all report
355
+ // text is. Asking the cmap instead costs a set walk per atom.
356
+ let ASCII = /^[\x20-\x7E]*$/;
357
+
358
+ // A face's character set is its cmap: WinAnsi code points on the base-14,
359
+ // fontkit's characterSet on an embedded TrueType. A face with no cmap (the
360
+ // layout suite's measuring doubles) does not substitute: width is its whole
361
+ // contract.
362
+ let CHARSETS = new WeakMap();
363
+ let OPEN = { has: () => true };
364
+
365
+ /** @typedef {{ has: (code: number) => boolean }} Cmap */
366
+
367
+ /** @type {(font: any) => Cmap} */
368
+ let charsetOf = (font) => {
369
+ let set = CHARSETS.get(font);
370
+ if (set) return set;
371
+ set = typeof font.getCharacterSet === "function" ? new Set(font.getCharacterSet()) : OPEN;
372
+ CHARSETS.set(font, set);
373
+ return set;
374
+ };
375
+
376
+ /**
377
+ * Replace characters a face cannot draw with `?`.
378
+ *
379
+ * Cell text is untrusted data, so one stray character must not fail the
380
+ * render: SCHEMA.md promises `?` for any face — WinAnsi on the base-14, the
381
+ * cmap on an embedded TrueType. A face that also lacks `?` omits the
382
+ * character rather than drawing .notdef.
383
+ *
384
+ * The ASCII guard carries the common case; only text with a character
385
+ * outside it pays for the cmap walk. Printable ASCII is a subset of WinAnsi
386
+ * and of every text face this layout measures.
387
+ *
388
+ * @type {(font: any, text: string) => string}
389
+ */
390
+ export let printable = (font, text) => {
391
+ if (!text || ASCII.test(text) || encodable(font, text)) return text;
392
+ return replaceUndrawable(charsetOf(font), text);
393
+ };
394
+
395
+ /** @type {(set: Cmap, text: string) => string} */
396
+ let replaceUndrawable = (set, text) => {
397
+ let sub = set.has(0x3f) ? "?" : "";
398
+ // Encodedness is a per-code-point question, so that is the unit to ask it
399
+ // in — the grapheme clusters the rule protects are undrawable anyway.
400
+ return codePoints(text)
401
+ .map((char) => (set.has(char.codePointAt(0) ?? -1) ? char : sub))
402
+ .join("");
403
+ };
404
+
405
+ /** @type {(font: any, text: string) => boolean} */
406
+ let encodable = (font, text) => {
407
+ let set = charsetOf(font);
408
+ return codePoints(text).every((char) => set.has(char.codePointAt(0) ?? -1));
409
+ };
package/lib/image.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * An image's own size, read from its header. Sizing is a fact about the bytes
3
+ * rather than anything a page can answer, so it sits here and not on the
4
+ * drawing surface: the band flow asks for it while measuring, where no
5
+ * document is in reach at all, and again while drawing, where one is.
6
+ *
7
+ * Read, never decoded — a PNG's IHDR and a JPEG's frame header carry the two
8
+ * numbers the layout needs, and nothing else here looks at a pixel.
9
+ *
10
+ * The XLSX target reads the same two headers for its own placement, in pixels.
11
+ * Whether that fact belongs on the engine's event instead — it already reads
12
+ * the magic numbers to name the format — is bead `quario-b0h`.
13
+ */
14
+
15
+ // Points, at the conventional 96 dpi the schema names: 72/96 of a pixel.
16
+ let PER_PX = 72 / 96;
17
+
18
+ /** @type {(bytes: Uint8Array, at: number) => number} */
19
+ let word = (bytes, at) => (bytes[at] << 8) | bytes[at + 1];
20
+
21
+ /** @type {(code: number) => boolean} */
22
+ let inSof = (code) => code >= 0xc0 && code <= 0xcf;
23
+
24
+ /** @type {(code: number) => boolean} */
25
+ let notTable = (code) => code !== 0xc4 && code !== 0xc8 && code !== 0xcc;
26
+
27
+ // The frame header carries the dimensions, and it is the first SOFn marker:
28
+ // every code in C0..CF except the three in that range that are not frames --
29
+ // DHT, the JPG extension, and DAC.
30
+ /** @type {(code: number) => boolean} */
31
+ let isFrame = (code) => inSof(code) && notTable(code);
32
+
33
+ /** @type {(bytes: Uint8Array) => { w: number, h: number }} */
34
+ let jpegSize = (bytes) => {
35
+ for (let at = 2; at + 9 < bytes.length; at += 2 + word(bytes, at + 2)) {
36
+ if (bytes[at] !== 0xff) break;
37
+ if (isFrame(bytes[at + 1])) return { w: word(bytes, at + 7), h: word(bytes, at + 5) };
38
+ }
39
+ return { w: 0, h: 0 };
40
+ };
41
+
42
+ /**
43
+ * The image's intrinsic size in points. The format is the engine's sniff,
44
+ * riding on the event, so nothing here decides it a second time.
45
+ *
46
+ * @param {Uint8Array} bytes The image file.
47
+ * @param {string} format The engine's sniff: `"png"` or `"jpeg"`.
48
+ * @returns {{ w: number, h: number }} The size, in points.
49
+ */
50
+ export let intrinsic = (bytes, format) => {
51
+ // A PNG carries the two numbers in its IHDR at a fixed offset.
52
+ let { w, h } = format === "png" ? { w: word(bytes, 18), h: word(bytes, 22) } : jpegSize(bytes);
53
+ // The engine vouched for the magic numbers, not for the rest of the file:
54
+ // a truncated header reaches here as a zero, and failing loudly beats
55
+ // drawing an image with no size (SCHEMA.md, "Image item").
56
+ if (!(w > 0 && h > 0)) throw Error("image: could not read the image's size from its bytes");
57
+ return { w: w * PER_PX, h: h * PER_PX };
58
+ };
package/lib/index.d.ts ADDED
@@ -0,0 +1,220 @@
1
+ import type { Target } from "quario";
2
+
3
+ /** Page geometry, in PostScript points. Host configuration, never schema. */
4
+ export interface LayoutPage {
5
+ /** Named size or `[width, height]` in points. Default `'A4'`. */
6
+ size?: "A4" | "letter" | [number, number];
7
+ /** Margin on all four sides, in points. Default `54` (0.75 in). */
8
+ margin?: number;
9
+ }
10
+
11
+ /** One TrueType family; missing variants fall back to `regular`. */
12
+ export interface LayoutFontFamily {
13
+ regular: Uint8Array | ArrayBuffer;
14
+ bold?: Uint8Array | ArrayBuffer;
15
+ italic?: Uint8Array | ArrayBuffer;
16
+ boldItalic?: Uint8Array | ArrayBuffer;
17
+ }
18
+
19
+ /** The font mapping: a declared `family` name to the bytes it measures against. */
20
+ export type LayoutFonts = Record<string, LayoutFontFamily>;
21
+
22
+ /** Host controls, taken and validated at the factory call. */
23
+ export interface LayoutOptions {
24
+ page?: LayoutPage;
25
+ /**
26
+ * TrueType families to measure against, selected from styles by
27
+ * `family: '<name>'`. Pass the same record to `pdf({ fonts })` and to
28
+ * `paint()`, so preview and document break their lines in the same places
29
+ * and draw in the same faces. Requires the optional peer `@pdf-lib/fontkit`;
30
+ * the base-14 families need nothing extra.
31
+ *
32
+ * Hold the record and the buffers in it across renders rather than building
33
+ * them anew each time: they are the identity a face is remembered by, so a
34
+ * host that hands over a fresh buffer parses the file again to measure
35
+ * against it, and registers a fresh face to draw it with.
36
+ */
37
+ fonts?: LayoutFonts;
38
+ }
39
+
40
+ /** A colour on the list: three channels in `0..1`. */
41
+ export interface Color {
42
+ r: number;
43
+ g: number;
44
+ b: number;
45
+ }
46
+
47
+ /**
48
+ * Which face a run is set in: the family it resolved to (`sans`, `serif`,
49
+ * `mono`, or a lower-cased name from the font mapping), the variant index
50
+ * `bold + 2 * italic`, and whether the face is bytes the host supplied.
51
+ *
52
+ * `embedded` is not derivable from the family name — a host may map its own
53
+ * family onto `sans`, `serif` or `mono` — and it decides how a painter draws
54
+ * the run. See `TextOp.advances`.
55
+ */
56
+ export interface FaceRef {
57
+ family: string;
58
+ variant: number;
59
+ embedded: boolean;
60
+ }
61
+
62
+ /** A filled rectangle. */
63
+ export interface RectOp {
64
+ kind: "rect";
65
+ x: number;
66
+ y: number;
67
+ w: number;
68
+ h: number;
69
+ color: Color;
70
+ }
71
+
72
+ /** A stroked segment: a border side, a rule, a text decoration. */
73
+ export interface LineOp {
74
+ kind: "line";
75
+ x1: number;
76
+ y1: number;
77
+ x2: number;
78
+ y2: number;
79
+ width: number;
80
+ color: Color;
81
+ dash: number[] | null;
82
+ }
83
+
84
+ /** An image, placed. `bytes` is the array the source expression yielded. */
85
+ export interface ImageOp {
86
+ kind: "image";
87
+ bytes: Uint8Array;
88
+ format: "png" | "jpeg";
89
+ x: number;
90
+ y: number;
91
+ w: number;
92
+ h: number;
93
+ }
94
+
95
+ /**
96
+ * One run of text: `y` is the line's top and `asc` how far below it the
97
+ * baseline sits, and `text` is the string to draw.
98
+ *
99
+ * `advances` rides only where a painter can use it — one advance in points per
100
+ * code point of `text`, present exactly when `font.embedded` is false. A
101
+ * base-14 family is drawn with them, character by character, so the face
102
+ * standing in for it on screen cannot drift across the run. A host's own
103
+ * family carries none: the painter has the same bytes the layout measured, so
104
+ * it draws the run whole and its own shaping is the document's.
105
+ */
106
+ export interface TextOp {
107
+ kind: "text";
108
+ x: number;
109
+ y: number;
110
+ w: number;
111
+ h: number;
112
+ asc: number;
113
+ size: number;
114
+ font: FaceRef;
115
+ color: Color;
116
+ text: string;
117
+ advances?: number[];
118
+ }
119
+
120
+ /**
121
+ * The unlicensed marking (LICENSE section 6), one per page, drawn last:
122
+ * `x`/`y` start its baseline and `angle` turns it counter-clockwise, in
123
+ * degrees.
124
+ */
125
+ export interface MarkOp {
126
+ kind: "mark";
127
+ x: number;
128
+ y: number;
129
+ angle: number;
130
+ size: number;
131
+ font: FaceRef;
132
+ text: string;
133
+ }
134
+
135
+ export type Op = RectOp | LineOp | ImageOp | TextOp | MarkOp;
136
+
137
+ /**
138
+ * The rectangle one schema node was drawn in, named by its path. One node
139
+ * draws many boxes — a column's cell on every row, a header on every page it
140
+ * repeats on — and every one of them means that node.
141
+ */
142
+ export interface Box {
143
+ path: string;
144
+ x: number;
145
+ y: number;
146
+ w: number;
147
+ h: number;
148
+ }
149
+
150
+ /**
151
+ * One page of the list. Coordinates are points from the page's top-left
152
+ * corner, `y` descending; `number` and `total` are what `page.number` and
153
+ * `page.total` read on it.
154
+ */
155
+ export interface Page {
156
+ number: number;
157
+ total: number;
158
+ width: number;
159
+ height: number;
160
+ ops: Op[];
161
+ boxes: Box[];
162
+ }
163
+
164
+ /** An outline entry: a group instance, and where its content begins. */
165
+ export interface Mark {
166
+ title: string;
167
+ depth: number;
168
+ page: number;
169
+ x: number;
170
+ y: number;
171
+ }
172
+
173
+ /** The display list: every page of the document, laid out. */
174
+ export interface Layout {
175
+ width: number;
176
+ height: number;
177
+ pages: Page[];
178
+ /** The group tree, in document order — the PDF target's bookmarks. */
179
+ marks: Mark[];
180
+ }
181
+
182
+ /**
183
+ * The layout target:
184
+ * `quario().report(schema).render(layout({ page }), data)` resolves the
185
+ * display list the PDF target writes and the viewer paints
186
+ * (docs/adr/0039).
187
+ */
188
+ export function layout(options?: LayoutOptions): Target<"layout", Promise<Layout>>;
189
+
190
+ /**
191
+ * CSS pixels per point at 100%: 96 dpi over PostScript's 72. Every surface
192
+ * that shows a page shows it at this size; a zoom is a factor on top of it.
193
+ */
194
+ export const PX_PER_POINT: number;
195
+
196
+ /**
197
+ * Paint one page onto a Canvas 2D context: a white page, then every op in
198
+ * order. `scale` is device pixels per point. `fonts` is the same record
199
+ * given to `layout()`, so a TrueType family draws in its own face.
200
+ */
201
+ export function paint(
202
+ ctx: CanvasRenderingContext2D,
203
+ page: Page,
204
+ options?: { scale?: number; fonts?: LayoutFonts },
205
+ ): Promise<void>;
206
+
207
+ /**
208
+ * The schema node drawn at a point on a page — the smallest box containing
209
+ * it — or `null` where nothing was drawn. Points from the top-left corner.
210
+ */
211
+ export function hit(page: Page, x: number, y: number): Box | null;
212
+
213
+ /**
214
+ * The page box a `page` option describes, validated: width, height and
215
+ * margin in points. `at` prefixes a failure with the option's name.
216
+ */
217
+ export function pageBox(
218
+ page?: LayoutPage,
219
+ at?: string,
220
+ ): { width: number; height: number; margin: number };