@quario/pdf 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/pdf",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "The browserless, paginated PDF render target for quario — in the makings, not yet released",
5
5
  "homepage": "https://getquario.com",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -36,19 +36,20 @@
36
36
  "postpack": "node -e \"require('fs').rmSync('LICENSE',{force:true})\""
37
37
  },
38
38
  "dependencies": {
39
+ "@quario/layout": "^0.1.0",
39
40
  "pdf-lib": "^1.17.1"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@arethetypeswrong/cli": "^0.18.3",
43
44
  "@pdf-lib/fontkit": "^1.1.1",
44
45
  "@size-limit/preset-small-lib": "^13.0.3",
45
- "quario": "^0.3.0",
46
+ "quario": "^0.4.0",
46
47
  "size-limit": "^13.0.3",
47
48
  "typescript": "^7.0.2"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "@pdf-lib/fontkit": "^1.1.1",
51
- "quario": "^0.3.0"
52
+ "quario": "^0.4.0"
52
53
  },
53
54
  "peerDependenciesMeta": {
54
55
  "@pdf-lib/fontkit": {
@@ -60,9 +61,10 @@
60
61
  "path": "lib/index.js",
61
62
  "ignore": [
62
63
  "quario",
64
+ "@quario/layout",
63
65
  "pdf-lib"
64
66
  ],
65
- "limit": "10.5 kB"
67
+ "limit": "6 kB"
66
68
  }
67
69
  ],
68
70
  "engines": {
package/lib/balance.js DELETED
@@ -1,90 +0,0 @@
1
- // The one number a balanced region decides: how tall each of its strips is
2
- // (ADR 0027 — balancing is a strip height, not a pass). Pure arithmetic over
3
- // what the buffer holds, so nothing here reaches a canvas or the band flow.
4
- //
5
- // Balancing by height alone under-fills, because most of what a region holds
6
- // does not divide: a group instance, a table row and an image each move whole.
7
- // A share that is not a whole number of them holds one fewer than it should,
8
- // and the page-bounded last strip absorbs the remainder (beads `quario-s5c`,
9
- // `quario-cgk`). So the height is not `measured / count` but the shortest
10
- // whole-line height at which placing the units in order lands them all in
11
- // `count` strips.
12
- //
13
- // The candidates are line multiples, for the reason the average was rounded to
14
- // one before this: a floor cuts between lines, so a height that is not a whole
15
- // number of them leaves every strip a fraction short. They are scanned from the
16
- // shortest upward rather than searched, because the range is bounded by the
17
- // lines a page holds and a scan owes nothing to an argument about the fill
18
- // being monotone in the height. The average is not the starting point and not
19
- // a lower bound: an instance drops its opening gap at a strip head, so `count`
20
- // of those gaps are measured that no strip ever pays.
21
-
22
- // A **unit** is one thing a strip places: `h` is what it costs mid-strip and
23
- // `head` what it costs when it is the first thing in a strip — which can be
24
- // less (a group instance drops its opening gap there) or more (a table row
25
- // pays for the column headings restated above it). `whole` units move
26
- // undivided and are the only ones `head` is read for; the rest flow, and
27
- // nothing that flows costs a different amount for opening a strip. What fills each of these in is the caller's, in
28
- // `layout.js`, which is also where the model's stated limits are.
29
- /**
30
- * @typedef {{ whole: boolean, h: number, head: number }} Unit
31
- */
32
- // How far one trial has filled: the strips it has opened and the height taken
33
- // out of the one it is in.
34
- /**
35
- * @typedef {{ strips: number, used: number }} Fill
36
- */
37
-
38
- // The height, or `null` when no candidate packs the units into `count` strips
39
- // and the region should fill instead. `room` is what one strip could hold at
40
- // most — the page — and `line` the height a floor cuts on.
41
- /** @type {(units: Unit[], count: number, room: number, line: number) => number | null} */
42
- export let balance = (units, count, room, line) => {
43
- for (let lines = 1; lines * line <= room; lines++) {
44
- let height = lines * line;
45
- if (stripsFor(units, height) <= count) return height;
46
- }
47
- return null;
48
- };
49
-
50
- // How many strips this height needs. `Infinity` when a unit is too tall for a
51
- // strip of it at all — the scan answers that by looking higher, and the region
52
- // fills once even the page is too short.
53
- // The only way a height fails outright is a whole unit taller than a strip of
54
- // it, so that case is answered here rather than through a result both helpers
55
- // would have to carry.
56
- /** @type {(units: Unit[], height: number) => number} */
57
- let stripsFor = (units, height) => {
58
- /** @type {Fill} */
59
- let fill = { strips: 1, used: 0 };
60
- for (let unit of units) {
61
- if (!unit.whole) placeFlow(fill, unit, height);
62
- else if (unit.head > height) return Infinity;
63
- else placeWhole(fill, unit, height);
64
- }
65
- return fill.strips;
66
- };
67
-
68
- // A whole unit that would cross the floor opens the next strip instead, where
69
- // its opening gap is dropped — the rule `breaksFor` applies during the replay.
70
- /** @type {(fill: Fill, unit: Unit, height: number) => void} */
71
- let placeWhole = (fill, unit, height) => {
72
- if (!fill.used) fill.used = unit.head;
73
- else if (fill.used + unit.h <= height) fill.used += unit.h;
74
- else {
75
- fill.strips++;
76
- fill.used = unit.head;
77
- }
78
- };
79
-
80
- // Breakable content fills to the floor and continues in the next strip, so it
81
- // spills across as many as it takes rather than moving whole. No height is too
82
- // short for it: content that splits always goes somewhere.
83
- /** @type {(fill: Fill, unit: Unit, height: number) => void} */
84
- let placeFlow = (fill, unit, height) => {
85
- fill.used += unit.h;
86
- while (fill.used > height) {
87
- fill.strips++;
88
- fill.used -= height;
89
- }
90
- };
package/lib/box.js DELETED
@@ -1,111 +0,0 @@
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 DELETED
@@ -1,389 +0,0 @@
1
- /**
2
- * The drawing surface. Every mark on a page goes through here, so nothing above
3
- * this module talks to pdf-lib page primitives.
4
- *
5
- * Two adapters satisfy one interface. `drawing` puts marks on a document;
6
- * `measuring` moves the cursor and marks nothing, which is how `probe` reserves
7
- * a band's height without emitting it. Measuring is therefore a choice of
8
- * adapter rather than a mode every primitive has to remember to check — and
9
- * since a measuring canvas is built from seven numbers and a font registry,
10
- * there is no parameter through which a document could reach it. Pagination is
11
- * the one thing measuring must never do: a page turn would reset the cursor
12
- * mid-measure and return a silently wrong height, so `newPage` throws there.
13
- */
14
- import { degrees, drawImage, drawText, rgb } from "pdf-lib";
15
- import { BLACK, dressed, shift } from "./style.js";
16
-
17
- // The operator builder wants explicit rotation and skew; report text has none.
18
- let NO_TURN = degrees(0);
19
-
20
- /** @typedef {import('./text.js').Line} Line */
21
- /** @typedef {import('./text.js').Metrics} Metrics */
22
- /** @typedef {import('pdf-lib').PDFPage} PDFPage */
23
- /** @typedef {import('pdf-lib').PDFFont} PDFFont */
24
- /** @typedef {import('pdf-lib').PDFName} PDFName */
25
-
26
- // The page box and the content box. Settled at `report-start` and fixed
27
- // thereafter: the geometry is the factory's and never moves, while `base` and
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
- // height off `top`/`bottom` through `adopt` below, while the first page is
31
- // still untouched.
32
- /**
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
38
- * declares none.
39
- *
40
- * @typedef {{ width: number, height: number, margin: number, base: number,
41
- * content: number, top: number, bottom: number, family: string | null,
42
- * locale?: string, currency?: string, timeZone?: string }} Frame
43
- */
44
-
45
- // A frame from the page box and the base size: how `content`/`top`/`bottom`
46
- // fall out of a page and a margin is derived here, once, so no caller and no
47
- // suite has to restate it and drift from what a real render uses.
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) => ({
53
- width,
54
- height,
55
- margin,
56
- base,
57
- family,
58
- content: width - 2 * margin,
59
- top: height - margin,
60
- bottom: margin,
61
- });
62
-
63
- // `y` is the cursor on the open page and `fresh` says nothing has been drawn on
64
- // it yet, which is what makes a break legal. `count` is how many pages exist.
65
- /**
66
- * @typedef {Frame & Metrics & { y: number, fresh: boolean, count: number,
67
- * newPage: () => void,
68
- * rect: (color: any, x: number, y: number, w: number, h: number) => void,
69
- * stroke: (x1: number, y1: number, x2: number, y2: number, thickness: number,
70
- * color: any, dash: number[] | null) => void,
71
- * picture: (bytes: Uint8Array, format: string, x: number, y: number,
72
- * w: number, h: number) => void,
73
- * drawLine: (line: Line, x: number, yTop: number, avail: number,
74
- * align: any) => void }} Canvas
75
- */
76
-
77
- // The page passes on top of the interface: re-visiting finished pages, handing
78
- // out their refs for the outline, and stamping them. Deliberately not on
79
- // `Canvas` — the layout may not re-target a page mid-walk, and a measurement
80
- // has no business doing any of it.
81
- /**
82
- * @typedef {Canvas & { select: (i: number) => void, refs: any[],
83
- * pictures: () => Promise<void>, watermark: (mark: any) => void }} Drawing
84
- */
85
-
86
- // What every canvas starts as, whichever adapter it is: its frame and faces,
87
- // and a cursor that has drawn nothing yet. Shared because the parity test
88
- // compares member names, not their values — two hand-written copies of this
89
- // could drift in what they start from and nothing would notice.
90
- /**
91
- * @type {(box: Frame, fonts: import('./fonts.js').Fonts) =>
92
- * Frame & Metrics & { y: number, fresh: boolean }}
93
- */
94
- let blank = (box, fonts) => ({ ...box, fonts, y: 0, fresh: true });
95
-
96
- let GREY = rgb(0.5, 0.5, 0.5);
97
-
98
- // Text decoration in the text colour. Thickness and offset come from the
99
- // line's ascender (the face metric already measured for baseline placement).
100
- // Empty lines (no width) draw nothing. Shared by the drawing adapter and the
101
- // layout recorder so both exercise the same path.
102
- /** @type {(line: Line) => any} */
103
- let ink = (line) => (line.pieces[0] && line.pieces[0].color) || BLACK;
104
-
105
- /** @type {(line: Line) => boolean} */
106
- let wantsDeco = (line) => !!line.w && dressed(line);
107
-
108
- /**
109
- * @type {(stroke: (x1: number, x2: number, y: number, thickness: number, color: any) => void,
110
- * line: Line, left: number, baseline: number) => void}
111
- */
112
- let decorateLine = (stroke, line, left, baseline) => {
113
- if (!wantsDeco(line)) return;
114
- let thickness = Math.max(line.asc / 12, 0.5);
115
- let right = left + line.w;
116
- let color = ink(line);
117
- if (line.underline) stroke(left, right, baseline - line.asc * 0.12, thickness, color);
118
- if (line.strikethrough) stroke(left, right, baseline + line.asc * 0.35, thickness, color);
119
- };
120
-
121
- /**
122
- * The adapter that draws: a canvas over a real document.
123
- *
124
- * @type {(doc: import('pdf-lib').PDFDocument, box: Frame,
125
- * fonts: import('./fonts.js').Fonts) => Drawing}
126
- */
127
- let drawing = (doc, box, fonts) => {
128
- /** @type {PDFPage} */
129
- let page;
130
- /** @type {PDFPage[]} */
131
- let pages = [];
132
- /**
133
- * The resource name a page refers to a face or an image by, minted once per
134
- * resource per page.
135
- *
136
- * pdf-lib's own page-level state cannot do this: `setFont` mints a fresh
137
- * random-suffixed name on every call, and `drawText({ font })` calls it twice
138
- * — once to select the face and once to restore the previous one. A report
139
- * that alternates bold and regular therefore grows its font dictionary with
140
- * its text rather than with its faces, and measured 4.8x larger for the same
141
- * content. `drawImage` on the page has the same shape, so one logo down a
142
- * thousand rows would put a thousand entries in one XObject dictionary,
143
- * every one of them pointing at the same stream.
144
- *
145
- * Keyed by page and kept for the canvas's whole life, never reset per page,
146
- * because the page-band pass returns to pages the body already drew on and
147
- * must reuse what they registered.
148
- *
149
- * @type {(mint: (on: PDFPage, resource: any) => PDFName) =>
150
- * (on: PDFPage, resource: any) => PDFName}
151
- */
152
- let perPage = (mint) => {
153
- /** @type {WeakMap<PDFPage, Map<any, PDFName>>} */
154
- let keys = new WeakMap();
155
- return (on, resource) => {
156
- let minted = keys.get(on);
157
- if (!minted) keys.set(on, (minted = new Map()));
158
- let key = minted.get(resource);
159
- if (!key) minted.set(resource, (key = mint(on, resource)));
160
- return key;
161
- };
162
- };
163
- let faceKey = perPage((on, font) => on.node.newFontDictionary(font.name, font.ref));
164
- let imageKey = perPage((on, image) => on.node.newXObject("Image", image.ref));
165
- /** @type {(font: PDFFont) => PDFName} */
166
- let fontKey = (font) => faceKey(page, font);
167
-
168
- // Open a fresh page and put the cursor at its top.
169
- let newPage = () => {
170
- page = doc.addPage([canvas.width, canvas.height]);
171
- pages.push(page);
172
- canvas.y = canvas.top;
173
- canvas.fresh = true;
174
- };
175
-
176
- /** @type {Canvas['rect']} */
177
- let rect = (color, x, y, w, h) => page.drawRectangle({ x, y, width: w, height: h, color });
178
-
179
- // Where every image goes, recorded as the walk passes it and drawn once the
180
- // walk is over. Embedding is asynchronous and a walk handler is not, so the
181
- // placement is what the flow makes and the bytes are turned into a document
182
- // resource afterwards -- which is also what lets one logo repeated on every
183
- // page be embedded once and referenced, keyed by the array the source
184
- // expression yielded.
185
- /** @type {{ page: PDFPage, bytes: Uint8Array, format: string, x: number,
186
- * y: number, w: number, h: number }[]} */
187
- let placed = [];
188
- // Drawn once, at the end of the render, so nothing clears this: the canvas
189
- // and the document it marks end together.
190
- /** @type {Canvas['picture']} */
191
- let picture = (bytes, format, x, y, w, h) => {
192
- placed.push({ page, bytes, format, x, y, w, h });
193
- };
194
- /** @type {Drawing['pictures']} */
195
- let pictures = async () => {
196
- // Keyed by the array the source expression yielded, so one logo reused
197
- // across pages is embedded once and every placement references it.
198
- /** @type {Map<Uint8Array, any>} */
199
- let embedded = new Map();
200
- for (let spot of placed) {
201
- let image = embedded.get(spot.bytes);
202
- if (!image) {
203
- image = await (spot.format === "png" ? doc.embedPng(spot.bytes) : doc.embedJpg(spot.bytes));
204
- embedded.set(spot.bytes, image);
205
- }
206
- spot.page.pushOperators(
207
- ...drawImage(imageKey(spot.page, image), {
208
- x: spot.x,
209
- y: spot.y,
210
- width: spot.w,
211
- height: spot.h,
212
- rotate: NO_TURN,
213
- xSkew: NO_TURN,
214
- ySkew: NO_TURN,
215
- }),
216
- );
217
- }
218
- };
219
-
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
- });
229
-
230
- /** @type {(piece: Line['pieces'][number], x: number, y: number) => void} */
231
- let writePiece = (piece, x, y) => {
232
- if (!piece.text) return;
233
- page.pushOperators(
234
- ...drawText(piece.font.encodeText(piece.text), {
235
- font: fontKey(piece.font),
236
- size: piece.size,
237
- color: piece.color || BLACK,
238
- x,
239
- y,
240
- rotate: NO_TURN,
241
- xSkew: NO_TURN,
242
- ySkew: NO_TURN,
243
- }),
244
- );
245
- };
246
-
247
- /** @type {(left: number, right: number, y: number, thickness: number, color: any) => void} */
248
- let strokeAt = (left, right, y, thickness, color) =>
249
- page.drawLine({
250
- start: { x: left, y },
251
- end: { x: right, y },
252
- thickness,
253
- color,
254
- });
255
-
256
- // Draw one wrapped line with its baseline the line's ascender under `yTop`,
257
- // horizontally placed by `align` within `avail` starting at `x`.
258
- /** @type {Canvas['drawLine']} */
259
- let drawLine = (line, x, yTop, avail, align) => {
260
- let left = x + shift(align, avail - line.w);
261
- let baseline = yTop - line.asc;
262
- let cursor = left;
263
- for (let piece of line.pieces) {
264
- writePiece(piece, cursor, baseline);
265
- cursor += piece.w;
266
- }
267
- decorateLine(strokeAt, line, left, baseline);
268
- };
269
-
270
- // Draw one page's marking through the same operator path as body text, so the
271
- // face reuses the page's `fontKey` resource instead of growing the dictionary
272
- // per draw.
273
- /** @type {Drawing['watermark']} */
274
- let watermark = (mark) =>
275
- page.pushOperators(
276
- ...drawText(mark.line, {
277
- font: fontKey(mark.font),
278
- size: mark.size,
279
- color: GREY,
280
- x: mark.x,
281
- y: mark.y,
282
- rotate: mark.rotate,
283
- xSkew: NO_TURN,
284
- ySkew: NO_TURN,
285
- // Opacity needs a named ExtGState resource on the page, and pdf-lib
286
- // declares the only method that mints one private, with no public
287
- // equivalent. The cast is deliberate and stays as narrow as the call.
288
- graphicsState: /** @type {any} */ (page).maybeEmbedGraphicsState({ opacity: 0.15 }),
289
- }),
290
- );
291
-
292
- /** @type {Drawing} */
293
- let canvas = {
294
- ...blank(box, fonts),
295
- get count() {
296
- return pages.length;
297
- },
298
- newPage,
299
- rect,
300
- stroke,
301
- picture,
302
- pictures,
303
- drawLine,
304
- // Re-open a finished page, for the passes that run over them all.
305
- select: (i) => {
306
- page = pages[i];
307
- },
308
- get refs() {
309
- return pages.map((each) => each.ref);
310
- },
311
- watermark,
312
- };
313
- return canvas;
314
- };
315
-
316
- // Nothing at all — what every primitive does when the canvas only measures.
317
- let MARKS_NOTHING = () => {};
318
-
319
- /**
320
- * The adapter that only measures: the same cursor arithmetic with every mark
321
- * discarded. It holds no document and no page, so a measurement cannot write.
322
- *
323
- * @type {(box: Frame, fonts: import('./fonts.js').Fonts) => Canvas}
324
- */
325
- let measuring = (box, fonts) => ({
326
- ...blank(box, fonts),
327
- count: 0,
328
- newPage: () => {
329
- throw new Error("probe reached newPage: the measuring path must not paginate");
330
- },
331
- rect: MARKS_NOTHING,
332
- stroke: MARKS_NOTHING,
333
- picture: MARKS_NOTHING,
334
- drawLine: MARKS_NOTHING,
335
- });
336
-
337
- /**
338
- * Take on a content box someone else worked out — how much page furniture
339
- * needs is layout's policy (`reserve` there), while which of a frame's members
340
- * may move at all is this module's invariant. `top` and `bottom` are the two,
341
- * and the only two read here: the page box and the base size are fixed for the
342
- * whole document, so the rest of the frame that arrives is this canvas's own
343
- * and is left alone. Layout reads the bounds live, so every page the body then
344
- * flows through sees the narrowed box.
345
- *
346
- * Legal only while the render has not committed to a page — the band flow
347
- * adopts from its `report-start` handler, with its own first page open but
348
- * empty. `fresh` alone would not say that: a page turn makes it true again on
349
- * page five, where narrowing would silently mix two geometries in one document.
350
- *
351
- * @param {Canvas} canvas The canvas to narrow.
352
- * @param {Frame} box The frame it takes its content box from.
353
- */
354
- let adopt = (canvas, box) => {
355
- if (canvas.count > 1 || !canvas.fresh) throw Error("adopt: the content box is fixed");
356
- canvas.top = box.top;
357
- canvas.bottom = box.bottom;
358
- // The open page has drawn nothing, so its cursor moves with the box — the
359
- // same statement `newPage` makes, and nothing on the page can be lost by it.
360
- canvas.y = canvas.top;
361
- };
362
-
363
- // The unlicensed-output marking (LICENSE section 6): one translucent line
364
- // drawn corner-to-corner across the finished page — over the content, not
365
- // under it, so no filled table header or background rectangle can cover it.
366
- // The wording comes from the engine, on `report-start`; this target owns the
367
- // geometry, which depends only on the page size and never on key contents,
368
- // so document bytes stay deterministic in both licensed states.
369
-
370
- // The per-render half of the marking, computed once: page geometry, the face
371
- // and the wording are all fixed for a whole render, so the trig and the
372
- // glyph-width walk never repeat per page.
373
- /** @type {(canvas: Canvas, text: string) => any} */
374
- let stamp = (canvas, text) => {
375
- let font = canvas.fonts.families.sans[0];
376
- let angle = Math.atan2(canvas.height, canvas.width);
377
- // Scale the line to three quarters of the page diagonal, whatever the size.
378
- let span = Math.hypot(canvas.width, canvas.height) * 0.75;
379
- return {
380
- font,
381
- line: font.encodeText(text),
382
- size: (54 * span) / font.widthOfTextAtSize(text, 54),
383
- x: canvas.width / 2 - (span / 2) * Math.cos(angle),
384
- y: canvas.height / 2 - (span / 2) * Math.sin(angle),
385
- rotate: degrees((angle * 180) / Math.PI),
386
- };
387
- };
388
-
389
- export { adopt, decorateLine, drawing, frame, measuring, stamp };