@quario/pdf 0.2.0 → 0.3.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
@@ -7,6 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-09-02
11
+
12
+ ### Added
13
+
14
+ - **`format` stringifies at the edge from the instance locale.** A PDF
15
+ without a host locale still uses `en-US` / UTC, so the bytes stay
16
+ reproducible (`docs/adr/0041`, `docs/adr/0025`).
17
+
18
+ - **A height-declared report header pins from the page top.** Leftover sits
19
+ under the items; the next band starts at the pin. The half-line group gap
20
+ drops after it. `page.margin` on the document is the inset when the host
21
+ omits it; both is a render error.
22
+
23
+ - **`spaceBefore` / `spaceAfter` skip the cursor.** Adjacent gaps add.
24
+ `spaceBefore` drops at a fresh body page or strip top; page-band items
25
+ keep it.
26
+
27
+ - **The box is honoured in layout.** Padding and border inset wrap width; a
28
+ named padding `0` beats the cell omakase (`PADX` 6 / `PADY` 2) on that
29
+ side. Incomplete sides draw nothing.
30
+
31
+ ### Changed
32
+
33
+ - **The last data row keeps the whole emitted total block.** A stack taller
34
+ than a page degrades like a tall row.
35
+
36
+ - **An unstyled table has no rules.** This target used to draw two 0.5 pt
37
+ rules, under the header and above the total, that no declaration could
38
+ ask for or refuse. Those strokes are gone. The strokes an author wants
39
+ are the box they declared. Cell padding omakase stays on undeclared cell
40
+ sides.
41
+
42
+ - **A visible text item occupies a line at its own `size`, empty or not.** An
43
+ empty or whitespace-only value used to sit at the report's base leading
44
+ (~14 pt at the 10 pt baseline). It now occupies `1.4 ×` the item's size, the
45
+ same as a glyph line. A literal newline is a line break; the blank line
46
+ among `"a\n\nb"` is that size too. A table cell that is empty or only
47
+ horizontal whitespace still has no content height.
48
+
49
+ ### Fixed
50
+
51
+ - **A missing TrueType glyph draws as `?`, not `.notdef`.** Characters
52
+ outside WinAnsi already substituted `?`; an embedded face's cmap holes
53
+ drew a box instead, and text extraction hid it. Same rule for every
54
+ face: a character the face cannot draw becomes `?`.
55
+
56
+ - **CR, LF, and CRLF are one hard line break.** Cell text already broke
57
+ on LF; a CRLF left a CR on the previous line, and a lone CR did not
58
+ break. All three are now one break, matching `SCHEMA.md` Cell values.
59
+ Wrapping still applies within each line.
60
+
10
61
  ## [0.2.0] - 2026-09-01
11
62
 
12
63
  ### Added
package/README.md CHANGED
@@ -135,8 +135,9 @@ without authored margins.
135
135
 
136
136
  The base-14 Helvetica, Times, and Courier families carry the default output (`family: "sans"`,
137
137
  `"serif"`, `"mono"`, each with regular, bold, italic, and bold-italic faces), using WinAnsi
138
- encoding. Characters outside WinAnsi render as `?` rather than failing the report, because cell
139
- text is untrusted data and one stray character must not take a document down.
138
+ encoding. Characters a face cannot draw render as `?` rather than failing the report, because cell
139
+ text is untrusted data and one stray character must not take a document down. WinAnsi is the
140
+ base-14 limit; a TrueType face's cmap is its own.
140
141
 
141
142
  For full Unicode, supply TrueType families and select them by name:
142
143
 
package/lib/box.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The box model this target honours: per-side padding and border, border-box,
3
+ * no collapse. Cell omakase (PADX / PADY) applies only where the author named
4
+ * no padding on that side; a named 0 wins. A border side contributes only when
5
+ * width, style and colour all resolve and width is positive — an incomplete
6
+ * result at render is nothing, not a solid black stroke.
7
+ */
8
+ import { PADX, PADY, col } from "./style.js";
9
+
10
+ let SIDES = ["Top", "Right", "Bottom", "Left"];
11
+ /** @type {Record<string, number[] | null>} */
12
+ let DASH = { solid: null, dashed: [3, 2], dotted: [1, 1.5] };
13
+
14
+ /** @typedef {{ t: number, r: number, b: number, l: number }} Inset */
15
+
16
+ /** @type {Inset} */
17
+ let CELL_PAD = { t: PADY, r: PADX, b: PADY, l: PADX };
18
+ /** @type {Inset} */
19
+ let NO_PAD = { t: 0, r: 0, b: 0, l: 0 };
20
+
21
+ /** @type {(name: string) => boolean} */
22
+ let isBox = (name) => name.startsWith("padding") || name.startsWith("border");
23
+
24
+ /** @type {(style: any) => any} */
25
+ let unbox = (style) => {
26
+ if (!style) return style;
27
+ let names = Object.keys(style).filter((name) => !isBox(name));
28
+ if (!names.length) return null;
29
+ return Object.fromEntries(names.map((name) => [name, style[name]]));
30
+ };
31
+
32
+ /** @type {(style: any, side: string, fallback: number) => number} */
33
+ let padOf = (style, side, fallback) => {
34
+ let value = style?.["padding" + side];
35
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
36
+ };
37
+
38
+ /** @type {(width: any) => boolean} */
39
+ let isStroke = (width) => Number.isFinite(width) && width > 0;
40
+ /** @type {(line: any) => boolean} */
41
+ let isLine = (line) => typeof line === "string" && Object.hasOwn(DASH, line);
42
+
43
+ /**
44
+ * @type {(width: any, line: any, color: any) =>
45
+ * { width: number, dash: number[] | null, color: any } | null}
46
+ */
47
+ let strokeOf = (width, line, color) => {
48
+ if (!isStroke(width) || !isLine(line) || !color) return null;
49
+ return { width, dash: DASH[line], color };
50
+ };
51
+
52
+ /**
53
+ * @type {(style: any, side: string) =>
54
+ * { width: number, dash: number[] | null, color: any } | null}
55
+ */
56
+ let edgeOf = (style, side) => {
57
+ if (!style) return null;
58
+ return strokeOf(
59
+ style["border" + side + "Width"],
60
+ style["border" + side + "Style"],
61
+ col(style["border" + side + "Color"]),
62
+ );
63
+ };
64
+
65
+ /** @type {(style: any, side: string) => number} */
66
+ let thick = (style, side) => {
67
+ let edge = edgeOf(style, side);
68
+ return edge ? edge.width : 0;
69
+ };
70
+
71
+ /** @type {(style: any, side: string, fallback: number) => number} */
72
+ let inset = (style, side, fallback) => padOf(style, side, fallback) + thick(style, side);
73
+
74
+ /** @type {(style: any, omakase: Inset) => Inset} */
75
+ let insetOf = (style, omakase) => ({
76
+ t: inset(style, "Top", omakase.t),
77
+ r: inset(style, "Right", omakase.r),
78
+ b: inset(style, "Bottom", omakase.b),
79
+ l: inset(style, "Left", omakase.l),
80
+ });
81
+
82
+ /** Endpoints of one inner-centred edge, as stroke() takes them. */
83
+ /** @type {Record<string, (x: number, yTop: number, w: number, h: number, half: number) => number[]>} */
84
+ let SPAN = {
85
+ Top: (x, yTop, w, _h, half) => [x, yTop - half, x + w, yTop - half],
86
+ Bottom: (x, yTop, w, h, half) => [x, yTop - h + half, x + w, yTop - h + half],
87
+ Left: (x, yTop, _w, h, half) => [x + half, yTop, x + half, yTop - h],
88
+ Right: (x, yTop, w, h, half) => [x + w - half, yTop, x + w - half, yTop - h],
89
+ };
90
+
91
+ /** @type {(canvas: { stroke: Function }, x: number, yTop: number, w: number, h: number, style: any, side: string) => void} */
92
+ let paintEdge = (canvas, x, yTop, w, h, style, side) => {
93
+ let edge = edgeOf(style, side);
94
+ if (!edge) return;
95
+ canvas.stroke(...SPAN[side](x, yTop, w, h, edge.width / 2), edge.width, edge.color, edge.dash);
96
+ };
97
+
98
+ /**
99
+ * Fill and stroke one border-box. Background is the whole rect; each edge
100
+ * sits inside it, centred on its own width, so a stroke does not spill past
101
+ * the box the caller measured.
102
+ *
103
+ * @type {(canvas: { rect: Function, stroke: Function }, x: number, yTop: number,
104
+ * w: number, h: number, style: any, bg: any) => void}
105
+ */
106
+ let paintBox = (canvas, x, yTop, w, h, style, bg) => {
107
+ if (bg) canvas.rect(bg, x, yTop - h, w, h);
108
+ if (style) for (let side of SIDES) paintEdge(canvas, x, yTop, w, h, style, side);
109
+ };
110
+
111
+ export { CELL_PAD, NO_PAD, insetOf, paintBox, unbox };
package/lib/canvas.js CHANGED
@@ -25,30 +25,36 @@ let NO_TURN = degrees(0);
25
25
 
26
26
  // The page box and the content box. Settled at `report-start` and fixed
27
27
  // thereafter: the geometry is the factory's and never moves, while `base` and
28
- // `style` take the report default one event into the render, before anything is
29
- // measured. The page bands are measured against the page box and take their
28
+ // `family` take the report default one event into the render, before anything
29
+ // is measured. The page bands are measured against the page box and take their
30
30
  // height off `top`/`bottom` through `adopt` below, while the first page is
31
31
  // still untouched.
32
32
  /**
33
- * `style` is the report default the outermost style layer, resolved once per
34
- * render from `report-start` and carried here so a measuring canvas built off
35
- * this frame reads the same one the drawing canvas does. Null when the report
33
+ * `family` is the report default's typeface, normalised, and `base` its size
34
+ * the two declarations that default is narrowed to. They ride the frame so a
35
+ * measuring canvas built off it reads the same pair the drawing canvas does,
36
+ * and so the default reaches a node as a fallback rather than a merged layer;
37
+ * `layout.js`'s `adoptDefault` carries why. `family` is null when the report
36
38
  * declares none.
37
39
  *
38
40
  * @typedef {{ width: number, height: number, margin: number, base: number,
39
- * content: number, top: number, bottom: number, style: any }} Frame
41
+ * content: number, top: number, bottom: number, family: string | null,
42
+ * locale?: string, currency?: string, timeZone?: string }} Frame
40
43
  */
41
44
 
42
45
  // A frame from the page box and the base size: how `content`/`top`/`bottom`
43
46
  // fall out of a page and a margin is derived here, once, so no caller and no
44
47
  // suite has to restate it and drift from what a real render uses.
45
- /** @type {(width: number, height: number, margin: number, base: number, style?: any) => Frame} */
46
- let frame = (width, height, margin, base, style = null) => ({
48
+ /**
49
+ * @type {(width: number, height: number, margin: number, base: number,
50
+ * family?: string | null) => Frame}
51
+ */
52
+ let frame = (width, height, margin, base, family = null) => ({
47
53
  width,
48
54
  height,
49
55
  margin,
50
56
  base,
51
- style,
57
+ family,
52
58
  content: width - 2 * margin,
53
59
  top: height - margin,
54
60
  bottom: margin,
@@ -60,7 +66,8 @@ let frame = (width, height, margin, base, style = null) => ({
60
66
  * @typedef {Frame & Metrics & { y: number, fresh: boolean, count: number,
61
67
  * newPage: () => void,
62
68
  * rect: (color: any, x: number, y: number, w: number, h: number) => void,
63
- * rule: (x1: number, x2: number, y: number) => void,
69
+ * stroke: (x1: number, y1: number, x2: number, y2: number, thickness: number,
70
+ * color: any, dash: number[] | null) => void,
64
71
  * picture: (bytes: Uint8Array, format: string, x: number, y: number,
65
72
  * w: number, h: number) => void,
66
73
  * drawLine: (line: Line, x: number, yTop: number, avail: number,
@@ -210,9 +217,15 @@ let drawing = (doc, box, fonts) => {
210
217
  }
211
218
  };
212
219
 
213
- /** @type {Canvas['rule']} */
214
- let rule = (x1, x2, y) =>
215
- page.drawLine({ start: { x: x1, y }, end: { x: x2, y }, thickness: 0.5, color: BLACK });
220
+ /** @type {Canvas['stroke']} */
221
+ let stroke = (x1, y1, x2, y2, thickness, color, dash) =>
222
+ page.drawLine({
223
+ start: { x: x1, y: y1 },
224
+ end: { x: x2, y: y2 },
225
+ thickness,
226
+ color,
227
+ ...(dash ? { dashArray: dash, dashPhase: 0 } : {}),
228
+ });
216
229
 
217
230
  /** @type {(piece: Line['pieces'][number], x: number, y: number) => void} */
218
231
  let writePiece = (piece, x, y) => {
@@ -284,7 +297,7 @@ let drawing = (doc, box, fonts) => {
284
297
  },
285
298
  newPage,
286
299
  rect,
287
- rule,
300
+ stroke,
288
301
  picture,
289
302
  pictures,
290
303
  drawLine,
@@ -316,7 +329,7 @@ let measuring = (box, fonts) => ({
316
329
  throw new Error("probe reached newPage: the measuring path must not paginate");
317
330
  },
318
331
  rect: MARKS_NOTHING,
319
- rule: MARKS_NOTHING,
332
+ stroke: MARKS_NOTHING,
320
333
  picture: MARKS_NOTHING,
321
334
  drawLine: MARKS_NOTHING,
322
335
  });
package/lib/fonts.js CHANGED
@@ -115,22 +115,43 @@ export async function embedFonts(doc, custom) {
115
115
  return { families };
116
116
  }
117
117
 
118
+ // What a declared `family` normalises to before it is looked up: lower case,
119
+ // and nothing at all when the value is not a name. Exported because the report
120
+ // default is normalised once per render rather than once per cell, and two
121
+ // spellings of this would drift.
118
122
  /** @type {(style: any) => string} */
119
- let familyName = (style) => (typeof style.family === "string" ? style.family.toLowerCase() : "");
123
+ export let familyName = (style) =>
124
+ typeof style.family === "string" ? style.family.toLowerCase() : "";
120
125
 
121
126
  // Own-key lookup: the family name is render data, so `constructor` must
122
127
  // fall back to sans rather than resolve an inherited member.
123
- /** @type {(fonts: Fonts, name: string) => any[]} */
128
+ /** @type {(fonts: Fonts, name: string | null) => any[]} */
124
129
  let variantsOf = (fonts, name) =>
125
- Object.hasOwn(fonts.families, name) ? fonts.families[name] : fonts.families.sans;
130
+ name && Object.hasOwn(fonts.families, name) ? fonts.families[name] : fonts.families.sans;
126
131
 
127
132
  /** @type {(style: any) => number} */
128
133
  let faceIndex = (style) => (style.bold ? 1 : 0) | (style.italic ? 2 : 0);
129
134
 
130
- // Resolve a style block to one embedded face.
131
- /** @type {(fonts: Fonts, style: any) => any} */
132
- export function face(fonts, style) {
133
- return variantsOf(fonts, familyName(style))[faceIndex(style)];
135
+ /**
136
+ * Resolve a style block to one embedded face.
137
+ *
138
+ * `fallback` is the report default's family, which the canvas carries for the
139
+ * whole render (docs/adr/0033) — so text declaring no family is set in the
140
+ * document's.
141
+ *
142
+ * The fallback turns on whether a family was *declared*, never on whether the
143
+ * declared one resolves: a literal `family` is a non-empty string by the time
144
+ * the engine passes it, but an `=` expression is resolved at render and
145
+ * reaches here unchecked, so an item can declare `""`, a number or `null`.
146
+ * Those are the item's own declaration and fall to sans like any other name
147
+ * this target cannot resolve (SCHEMA.md) — the document's family is for text
148
+ * that declared none. Own-key, because the block is render data.
149
+ *
150
+ * @type {(fonts: Fonts, style: any, fallback: string | null) => any}
151
+ */
152
+ export function face(fonts, style, fallback) {
153
+ let declared = Object.hasOwn(style, "family");
154
+ return variantsOf(fonts, declared ? familyName(style) : fallback)[faceIndex(style)];
134
155
  }
135
156
 
136
157
  // The distance from a line's top to its baseline: pdf-lib's ascender, which
@@ -143,38 +164,60 @@ export let ascOf = (font, size) => font.heightAtSize(size, { descender: false })
143
164
  /** @type {(font: any, text: string, size: number) => number} */
144
165
  export let width = (font, text, size) => font.widthOfTextAtSize(text, size);
145
166
 
146
- // Printable ASCII, which every face encodes and which almost all report text
147
- // is. Asking the face instead costs a full measurement per atom.
167
+ // Printable ASCII, which every text face encodes and which almost all report
168
+ // text is. Asking the cmap instead costs a set walk per atom.
148
169
  let ASCII = /^[\x20-\x7E]*$/;
149
170
 
171
+ // pdf-lib's getCharacterSet() is the cmap for both kinds of face: WinAnsi
172
+ // code points on the base-14, fontkit's characterSet on an embedded TTF.
173
+ // widthOfTextAtSize does not throw on a TrueType .notdef, so the old probe
174
+ // let cmap holes through as glyph id 0. A face with no cmap (the layout
175
+ // suite's measuring doubles) does not substitute: width is its whole contract.
176
+ let CHARSETS = new WeakMap();
177
+ let OPEN = { has: () => true };
178
+
179
+ /** @typedef {{ has: (code: number) => boolean }} Cmap */
180
+
181
+ /** @type {(font: any) => Cmap} */
182
+ let charsetOf = (font) => {
183
+ let set = CHARSETS.get(font);
184
+ if (set) return set;
185
+ set = typeof font.getCharacterSet === "function" ? new Set(font.getCharacterSet()) : OPEN;
186
+ CHARSETS.set(font, set);
187
+ return set;
188
+ };
189
+
150
190
  /**
151
- * Replace characters a face cannot encode with `?`.
191
+ * Replace characters a face cannot draw with `?`.
152
192
  *
153
- * The base-14 faces are WinAnsi and pdf-lib refuses anything outside it. Cell
154
- * text is untrusted data, so one stray character must not fail the render:
155
- * SCHEMA.md promises `?`, and full Unicode through `options.fonts`.
193
+ * Cell text is untrusted data, so one stray character must not fail the
194
+ * render: SCHEMA.md promises `?` for any face WinAnsi on the base-14, the
195
+ * cmap on an embedded TrueType. A face that also lacks `?` omits the
196
+ * character rather than drawing .notdef.
156
197
  *
157
- * There is no way to ask a pdf-lib face what it can encode without measuring
158
- * against it, so the ASCII guard carries the common case and only text with a
159
- * character outside it pays for the answer. WinAnsi is a superset of printable
160
- * ASCII, so the guard never lets an unencodable character through.
198
+ * The ASCII guard carries the common case; only text with a character
199
+ * outside it pays for the cmap walk. Printable ASCII is a subset of WinAnsi
200
+ * and of every text face this target embeds.
161
201
  *
162
202
  * @type {(font: any, text: string) => string}
163
203
  */
164
204
  export let printable = (font, text) => {
165
205
  if (!text || ASCII.test(text) || encodable(font, text)) return text;
166
- // WinAnsi encodability is a per-code-point question, so that is the unit to
167
- // ask it in — the grapheme clusters the rule protects are unencodable anyway.
206
+ return replaceUndrawable(charsetOf(font), text);
207
+ };
208
+
209
+ /** @type {(set: Cmap, text: string) => string} */
210
+ let replaceUndrawable = (set, text) => {
211
+ let sub = set.has(0x3f) ? "?" : "";
212
+ // Encodedness is a per-code-point question, so that is the unit to ask it
213
+ // in — the grapheme clusters the rule protects are undrawable anyway.
168
214
  // oxlint-disable-next-line typescript/no-misused-spread
169
- return [...text].map((char) => (encodable(font, char) ? char : "?")).join("");
215
+ return [...text].map((char) => (set.has(char.codePointAt(0) ?? -1) ? char : sub)).join("");
170
216
  };
171
217
 
172
218
  /** @type {(font: any, text: string) => boolean} */
173
219
  let encodable = (font, text) => {
174
- try {
175
- font.widthOfTextAtSize(text, 1);
176
- return true;
177
- } catch {
178
- return false;
179
- }
220
+ let set = charsetOf(font);
221
+ // oxlint-disable-next-line typescript/no-misused-spread
222
+ return [...text].every((char) => set.has(char.codePointAt(0) ?? -1));
180
223
  };
package/lib/index.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * headers, page bands, `break: "page"`, `reset: "page"` — while the writing is pdf-lib's, so
6
6
  * this target carries a typesetter rather than a PDF implementation. Output
7
7
  * is deterministic: the document carries no dates of its own, so the same
8
- * input renders the same bytes. Page geometry is target configuration
9
- * (`pdf({ page })`), never schema.
8
+ * input renders the same bytes. Page size is target configuration
9
+ * (`pdf({ page.size })`); `page.margin` may also be a document field.
10
10
  *
11
11
  * This file is options, fonts and geometry, the walk itself, and the passes
12
12
  * that follow it over the finished pages — page furniture and the marking,
@@ -57,11 +57,11 @@ let pair = (dimensions) => {
57
57
  return { width, height };
58
58
  };
59
59
 
60
- /** @type {(margin: any, width: number, height: number) => number} */
61
- let marginOf = (margin = 54, width, height) => {
60
+ /** @type {(margin: any, width: number, height: number, path?: string) => number} */
61
+ let marginOf = (margin = 54, width, height, path = "options.page.margin") => {
62
62
  let fits = Number.isFinite(margin) && margin >= 0 && 2 * margin < Math.min(width, height);
63
63
  // oxlint-disable-next-line no-unused-expressions
64
- fits || err("options.page.margin: expected a non-negative number smaller than half the page");
64
+ fits || err(path + ": expected a non-negative number smaller than half the page");
65
65
  return margin;
66
66
  };
67
67
 
@@ -71,16 +71,30 @@ let marginOf = (margin = 54, width, height) => {
71
71
  // carries the same 10 for the same reason (docs/adr/0014, docs/adr/0033).
72
72
  let BASE = 10;
73
73
 
74
- // The page box and the content box in one, settled here beside the validation
75
- // that produced them. This frame never moves at all: it serves every render the
76
- // target compiles, so a narrowing here would be one document's page bands
77
- // reserved out of the next one too. The one narrowing a render does make is the
78
- // band flow's, off that render's own canvas.
79
- /** @type {(options: any) => import('./canvas.js').Frame} */
80
- let geometry = (options) => {
74
+ // The page box and the content box in one. Size is settled at the factory;
75
+ // margin may come from the document's `report-start`, so a render peeks that
76
+ // event before opening a canvas.
77
+ /** @type {(page: any) => any} */
78
+ let hostMargin = (page) => (Object.hasOwn(page, "margin") ? page.margin : undefined);
79
+ /** @type {(opening: any) => any} */
80
+ let docMargin = (opening) => opening?.margin;
81
+ /** @type {(doc: any, host: any) => boolean} */
82
+ let bothMargins = (doc, host) => doc != null && host !== undefined;
83
+ /** @type {(fromDoc: boolean) => string} */
84
+ let marginPath = (fromDoc) => (fromDoc ? "page.margin" : "options.page.margin");
85
+ /** @type {(opening: any, page: any) => { value: any, path: string }} */
86
+ let marginChoice = (opening, page) => {
87
+ let host = hostMargin(page);
88
+ let doc = docMargin(opening);
89
+ if (bothMargins(doc, host)) err("page.margin: document and target both declare a margin");
90
+ return { value: doc ?? host, path: marginPath(doc != null) };
91
+ };
92
+ /** @type {(options: any, opening?: any) => import('./canvas.js').Frame} */
93
+ let geometry = (options, opening) => {
81
94
  let { page = {} } = options ?? {};
82
95
  let { width, height } = pair(named(page.size));
83
- return frame(width, height, marginOf(page.margin, width, height), BASE);
96
+ let chosen = marginChoice(opening, page);
97
+ return frame(width, height, marginOf(chosen.value, width, height, chosen.path), BASE);
84
98
  };
85
99
 
86
100
  // Optional document information. Never a date: pdf-lib stamps the current time
@@ -136,7 +150,7 @@ let markPages = async (canvas, marking) => {
136
150
  * The target (see SCHEMA.md, "Instances and targets").
137
151
  */
138
152
  export function pdf(options) {
139
- let geo = geometry(options);
153
+ geometry(options);
140
154
  let meta = options?.meta,
141
155
  custom = options?.fonts;
142
156
  /** @type {(stream: any) => (data?: any) => Promise<Uint8Array>} */
@@ -146,11 +160,18 @@ export function pdf(options) {
146
160
  // Every face is embedded before the walk, so resolving a style to a
147
161
  // font during layout stays synchronous.
148
162
  let fonts = await embedFonts(doc, custom);
163
+ let gen = stream(data);
164
+ let first = gen.next();
165
+ let geo = geometry(options, first.done ? null : first.value);
149
166
  let canvas = drawing(doc, geo, fonts);
150
167
  // The band flow owns the placement state and every handler over it, and
151
168
  // opens the first page as it is built; this file only hands it the stream.
152
169
  let { handlers, finish } = flow(canvas);
153
- await walk(stream(data), handlers);
170
+ function* events() {
171
+ if (!first.done) yield first.value;
172
+ yield* gen;
173
+ }
174
+ await walk(events(), handlers);
154
175
  // The flow settled the opening event on its way past — it reserves the
155
176
  // page bands off it — and hands it back with the marks. What is left on it
156
177
  // is what the passes below want: the band closures to render per page, and