@quario/pdf 0.7.0 → 0.9.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,39 +1,106 @@
1
- # Changelog
1
+ # @quario/pdf
2
2
 
3
- All notable changes to @quario/pdf are documented in this file.
3
+ ## 0.9.0
4
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).
5
+ ### Minor Changes
7
6
 
8
- ## [Unreleased]
7
+ - **Every target factory now refuses an option it does not understand.** An unknown key, a key with
8
+ a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the
9
+ factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx`
10
+ already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read
11
+ the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no
12
+ signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding
13
+ one.
9
14
 
10
- ## [0.7.0] - 2026-09-07
15
+ The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and
16
+ `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A
17
+ fourth document property is one edit rather than three.
11
18
 
12
- ### Added
19
+ **What this changes for you.** One options object spread across several targets stops working if
20
+ any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`,
21
+ `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers
22
+ share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works.
23
+ TypeScript does not warn about this: excess-property checking fires on an object literal and not
24
+ on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.
25
+
26
+ Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and
27
+ `{ meta: { title: config.title } }` over a config that carries neither are both fine.
28
+ `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws
29
+ instead of turning path stamping on. A `fonts` mapping given as an array is refused by
30
+ `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.
31
+
32
+ Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests
33
+ `instanceof Error` is unaffected. One that compares the constructor is not.
34
+
35
+ `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null`
36
+ and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }`
37
+ is a property you did not write rather than one of the wrong type, and an inherited enumerable key
38
+ is no longer reported as an option you wrote.
39
+
40
+ `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the
41
+ object you passed no longer changes what a configured target writes.
42
+
43
+ ### Patch Changes
44
+
45
+ - The package now accepts only a version of `@cantoo/pdf-lib` it can import. The declared range is
46
+ `~2.9.1` rather than `^2.9.1`.
47
+
48
+ Earlier versions accepted `@cantoo/pdf-lib` 2.11.0, whose ESM build imports its font metric JSON
49
+ without an import attribute. A fresh install could resolve to it, and importing `@quario/pdf` then
50
+ failed on Node before rendering anything, with `TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]` naming a
51
+ `.compressed.json` file inside that package. Node 22 and Node 24 both report it. If you saw that
52
+ error, reinstall — nothing in your own code has to change.
53
+
54
+ - Updated dependencies
55
+ - quario@0.9.0
56
+ - @quario/layout@0.6.0
57
+
58
+ ## 0.8.0
59
+
60
+ ### Minor Changes
61
+
62
+ - **An image's box is now as wide as the container it sits in.** A
63
+ `background` or `border*` on an image item hugged the picture and now spans
64
+ the content width, or the slot's share inside a split; the picture itself
65
+ does not move. See the `@quario/layout` changelog for the rule and for what
66
+ it costs a report that relied on a border hugging a logo.
67
+
68
+ ### Patch Changes
69
+
70
+ - **Every published README says where the documentation is.** Each package now
71
+ carries a Documentation section pointing at the reference, at the report schema
72
+ that normatively specifies what a report may declare, and at the package's own
73
+ API. The paragraphs that used to end on an unstated contract — the event
74
+ stream's field semantics, the style vocabulary, page columns, the Content
75
+ Security Policy a fragment with images needs, the formula mangling, and each
76
+ target's own contract — link the page that states it. Every link is an absolute
77
+ URL, so it resolves from the npm package page as readily as from an installed
78
+ copy.
79
+ - Updated dependencies
80
+ - @quario/layout@0.5.0
81
+ - quario@0.8.0
82
+
83
+ ## 0.7.0
84
+
85
+ ### Minor Changes
13
86
 
14
87
  - **Styled runs reach the page.** A cell value written as a list of styled runs
15
88
  draws each run in its own face, size and colour, underlines and strikes
16
89
  through per run rather than across the line, and paints a run's `background`
17
90
  as a highlight behind its text.
18
-
19
- ### Changed
20
-
21
91
  - **A `format` inside a sentence now presents where the engine says it does.** A
22
92
  cell mixing literal text and one interpolation under a `format` draws the
23
93
  value plainly, as the spreadsheet and CSV targets already did; splitting the
24
94
  value into styled runs is how a value inside a sentence is formatted.
25
95
 
26
- ## [0.6.0] - 2026-09-07
96
+ ## 0.6.0
27
97
 
28
- ### Added
98
+ ### Minor Changes
29
99
 
30
100
  - **A cell's own `currency` code is honoured**, ahead of the instance's
31
101
  default, so a listing whose rows arrive in different currencies presents each
32
102
  in its own. A code the engine could not accept leaves the cell as plain
33
103
  display text rather than presenting it in the instance's currency.
34
-
35
- ### Changed
36
-
37
104
  - **Numbers presented through `format` now show a fixed two fraction digits,
38
105
  matching every other target.** `format: "number"` on `1000` renders
39
106
  `1,000.00` where it rendered `1,000`, and `0.12345` renders `0.12` where it
@@ -42,9 +109,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
42
109
  so a JPY amount loses the two decimals it never had. `date` is unchanged, as
43
110
  are grouping separators and symbol placement. The digits come from the
44
111
  engine, so a cell reads the same here as it does in a workbook.
45
-
46
- ### Fixed
47
-
48
112
  - **An image these bytes cannot embed now fails with the item's name on it.**
49
113
  A PNG whose pixel data is corrupt past the size in its header used to reject
50
114
  the render with `Invalid typed array length: 0` — the PDF writer's own words,
@@ -54,9 +118,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
54
118
  account after it and on `cause`. A JPEG is unchanged: its header is read but
55
119
  its pixel data never is, so a corrupt one still embeds without complaint.
56
120
 
57
- ## [0.5.0] - 2026-09-05
121
+ ## 0.5.0
58
122
 
59
- ### Added
123
+ ### Minor Changes
60
124
 
61
125
  - **A spanning cell draws as one box** across the columns it covers, through
62
126
  `@quario/layout`. It takes no part in measuring them, and a spanning row that
@@ -64,16 +128,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
64
128
  each slice lands in.
65
129
  - **`valign`** on table cells and split slots, through `@quario/layout`: middle
66
130
  and bottom place the content in the height its row or split leaves over it.
67
-
68
- ### Changed
69
-
70
131
  - **A row's box is drawn by the row's cells**, through `@quario/layout`. A box
71
132
  declared on a table row used to be one rect across the columns; each covered
72
133
  cell now draws its own. A row's `borderBottom` still reads as one continuous
73
134
  edge, a row's `borderLeft` becomes an edge on every cell rather than one at
74
135
  the row's outer left, and a bordered row is taller by its border's width,
75
136
  since a border occupies height as a cell's always has.
76
-
77
137
  - **Embedding TrueType fonts now needs `fontkit`, not `@pdf-lib/fontkit`.**
78
138
  Install `fontkit` instead; nothing else about `options.fonts` changes. The
79
139
  old package's bundle crashed with a bare `ReferenceError` on any OpenType
@@ -81,28 +141,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
81
141
  Tamil, Khmer or Myanmar webfont in practice, and some fifty further scripts
82
142
  besides. Those faces now measure, embed and draw. Which scripts a face
83
143
  supports remains the font's and the parser's to answer, not this package's.
84
-
85
144
  - **`pdf-lib` is now `@cantoo/pdf-lib`.** A maintained fork, and what
86
145
  `fontkit`'s subsetting requires. Rendered documents are unchanged in what
87
146
  they draw: every drawn string is identical and every filled area lands in
88
147
  the same place, but the file's bytes differ, so a host comparing digests
89
148
  against stored output will see them move once.
90
149
 
91
- ## [0.4.0] - 2026-09-03
150
+ ## 0.4.0
92
151
 
93
- ### Changed
152
+ ### Minor Changes
94
153
 
95
154
  - **A split slot's box now fills the split's height.** A slot's background and
96
155
  border stopped at that slot's own content and now take the split's height,
97
156
  the way a table cell's take their row's. See the `@quario/layout` changelog
98
157
  for the rule and for what it costs a report that relied on the short box.
99
-
100
158
  - **A date string under `format: "date"` now presents as a date.** The
101
159
  drawn text comes from the engine's `format()` helper, which revives the two
102
160
  read forms. See the `quario` changelog for the forms and the timezone rule.
103
-
104
- ### Changed
105
-
106
161
  - **The layout moved to `@quario/layout`; this target paints its list.**
107
162
  Measurement, wrapping, pagination, page furniture, the marking's geometry
108
163
  and the page-size table now live in the layout package, which this target
@@ -112,16 +167,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
112
167
  options are the layout's own, so the same object configures `pdf()`, the
113
168
  viewer and the editor. The font mapping's shape is now refused at the
114
169
  factory call rather than at render.
115
-
116
- ### Fixed
117
-
118
170
  - **A nested group's footer keeps the page columns.** Inside a page-column
119
171
  region, the first footer of a group nested in it ended the region: the
120
172
  strips collapsed and every band after it was laid out across the page, as
121
173
  if `columns` were absent. Only the node that declared `columns` closes its
122
174
  own region now — a nested group's header and footer are region content,
123
175
  laid out in the strips.
124
-
125
176
  - **A padded or bordered item or row that breaks across a page keeps its
126
177
  box.** Content too tall for one page is drawn as slices, and a slice now
127
178
  carries the box sides the break left it — the top on the first, the bottom
@@ -133,60 +184,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
133
184
  the horizontal padding they were wrapped for, rather than flush to the
134
185
  column. **This changes output** for reports where such content breaks.
135
186
 
136
- ## [0.3.0] - 2026-09-02
187
+ ## 0.3.0
137
188
 
138
- ### Added
189
+ ### Minor Changes
139
190
 
140
191
  - **`format` stringifies at the edge from the instance locale.** A PDF
141
192
  without a host locale still uses `en-US` / UTC, so the bytes stay
142
193
  reproducible.
143
-
144
194
  - **A height-declared report header pins from the page top.** Leftover sits
145
195
  under the items; the next band starts at the pin. The half-line group gap
146
196
  drops after it. `page.margin` on the document is the inset when the host
147
197
  omits it; both is a render error.
148
-
149
198
  - **`spaceBefore` / `spaceAfter` skip the cursor.** Adjacent gaps add.
150
199
  `spaceBefore` drops at a fresh body page or strip top; page-band items
151
200
  keep it.
152
-
153
201
  - **The box is honoured in layout.** Padding and border inset wrap width; a
154
202
  named padding `0` beats the cell omakase (`PADX` 6 / `PADY` 2) on that
155
203
  side. Incomplete sides draw nothing.
156
-
157
- ### Changed
158
-
159
204
  - **The last data row keeps the whole emitted total block.** A stack taller
160
205
  than a page degrades like a tall row.
161
-
162
206
  - **An unstyled table has no rules.** This target used to draw two 0.5 pt
163
207
  rules, under the header and above the total, that no declaration could
164
208
  ask for or refuse. Those strokes are gone. The strokes an author wants
165
209
  are the box they declared. Cell padding omakase stays on undeclared cell
166
210
  sides.
167
-
168
211
  - **A visible text item occupies a line at its own `size`, empty or not.** An
169
212
  empty or whitespace-only value used to sit at the report's base leading
170
213
  (~14 pt at the 10 pt baseline). It now occupies `1.4 ×` the item's size, the
171
214
  same as a glyph line. A literal newline is a line break; the blank line
172
215
  among `"a\n\nb"` is that size too. A table cell that is empty or only
173
216
  horizontal whitespace still has no content height.
174
-
175
- ### Fixed
176
-
177
217
  - **A missing TrueType glyph draws as `?`, not `.notdef`.** Characters
178
218
  outside WinAnsi already substituted `?`; an embedded face's cmap holes
179
219
  drew a box instead, and text extraction hid it. Same rule for every
180
220
  face: a character the face cannot draw becomes `?`.
181
-
182
221
  - **CR, LF, and CRLF are one hard line break.** Cell text already broke
183
222
  on LF; a CRLF left a CR on the previous line, and a lone CR did not
184
223
  break. All three are now one break, matching `SCHEMA.md` Cell values.
185
224
  Wrapping still applies within each line.
186
225
 
187
- ## [0.2.0] - 2026-09-01
226
+ ## 0.2.0
188
227
 
189
- ### Added
228
+ ### Minor Changes
190
229
 
191
230
  - **The report default sets the document's face and base size.** A report's
192
231
  top-level `style` replaces this target's own baseline, so its `size` scales
@@ -194,12 +233,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
194
233
  nothing is set in. It is the layer _under_ the band-role defaults: a report
195
234
  declaring `size: 12` still renders its report header at 14, and an item's own
196
235
  style wins over both.
197
-
198
236
  - **`uppercase` draws capitals.** With no text-transform to defer to, this
199
237
  target capitalises the string before measuring it, so wrapping and column
200
238
  widths are those of the text actually drawn. The mapping is Unicode default
201
239
  case, never the host's locale, so output stays byte-reproducible.
202
-
203
240
  - **Splits lay out across the content width.** Each slot is measured and
204
241
  wrapped inside its own share, all slots draw from a common top, and the
205
242
  split takes the height of its tallest slot so the band below clears them
@@ -208,27 +245,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
208
245
  any page renders in full past the bottom margin. The split's own style is
209
246
  the layer under each slot's, and its `background` fills the whole split
210
247
  behind them.
211
-
212
- ### Changed
213
-
214
248
  - **A bare `Date` draws as ISO 8601 UTC, the same on every machine.** Cell
215
249
  text and outline bookmark titles for `Date` values used `String(date)`,
216
250
  which bakes the host's timezone and locale into the document — at odds with
217
251
  this target's byte-reproducibility guarantee. Both now render through the
218
252
  engine's shared display rule, so a `Date` group key titles its bookmark
219
253
  with the same ISO text its cells draw.
220
-
221
- ### Removed
222
-
223
254
  - **`options.baseSize`.** A document's type size is the document's own, so it
224
255
  is the report's `style.size` — portable, travelling with the definition to
225
256
  every target — rather than a host option one target honoured. Text with
226
257
  nothing declared still renders at 10 points. Replace `pdf({ baseSize: 11 })`
227
258
  with `"style": { "size": 11 }` on the report.
228
259
 
229
- ## [0.1.0] - 2026-08-27
260
+ ## 0.1.0
230
261
 
231
- ### Added
262
+ ### Minor Changes
232
263
 
233
264
  - **Paginated PDFs from the same compiled report.** The band flow places
234
265
  headers, groups, and tables; pdf-lib writes the file. Page furniture and an
package/README.md CHANGED
@@ -4,8 +4,8 @@
4
4
  definition straight to a paginated PDF. No headless browser, no print CSS, deterministic bytes.
5
5
 
6
6
  quario does the typesetting — pagination, keep-together, tables, fonts, the document outline —
7
- through `@quario/layout`, the same layout the viewer paints on screen, so the preview and the
8
- document break their pages in the same places. [`@cantoo/pdf-lib`](https://github.com/Cantoo-Scribe/pdf-lib) writes the file. You get typesetting plus pdf-lib: no Chromium
7
+ through `@quario/layout`. That is the same layout the viewer paints on screen, so the preview and
8
+ the document break their pages in the same places. [`@cantoo/pdf-lib`](https://github.com/Cantoo-Scribe/pdf-lib) writes the file. You get typesetting plus pdf-lib: no Chromium
9
9
  in your container, no page-load race, no fonts-not-ready flake.
10
10
 
11
11
  ## Install
@@ -14,8 +14,8 @@ in your container, no page-load race, no fonts-not-ready flake.
14
14
  npm install quario @quario/pdf
15
15
  ```
16
16
 
17
- `@cantoo/pdf-lib` comes along as a dependency; the engine is a peer, installed beside it. ESM-only, Node 22+, and browser-ready through
18
- any standards-based ESM bundler. The renderer returns bytes, so the host decides where they go.
17
+ `@cantoo/pdf-lib` arrives as a dependency. The engine is a peer, installed beside it. ESM-only,
18
+ Node 22+, and browser-ready through any standards-based ESM bundler. The renderer returns bytes, so the host decides where they go.
19
19
 
20
20
  Embedding TrueType fonts also needs the optional peer:
21
21
 
@@ -58,8 +58,9 @@ const paged = {
58
58
  ```
59
59
 
60
60
  `visible: "=page.number > 1"` on a band is the cover-page recipe. Gate band _visibility_ on
61
- `page.number` only. Band heights are reserved before the real page count is known, probed as a
62
- two-page document, so `=page.total > 10` reserves only the 8 pt gap a declared band always costs.
61
+ `page.number` only. The layout reserves band heights before it knows the real page count, probing as a
62
+ two-page document. `=page.total > 10` therefore reserves only the 8 pt gap a declared band always
63
+ costs.
63
64
  `page.total` is fine in band _text_.
64
65
 
65
66
  ## API
@@ -67,7 +68,7 @@ two-page document, so `=page.total > 10` reserves only the 8 pt gap a declared b
67
68
  ### `pdf(options?)`
68
69
 
69
70
  The target factory takes this target's host options, validates them at the call, and returns
70
- the target you pass to `render`. `report()` compiles once and `report.render(pdf(options), data)`
71
+ the target you pass to `render`. The factory refuses an option it does not know. It throws a `TypeError` at the call for an unknown key. It throws one also for a key with a value of the wrong type. `report()` compiles once and `report.render(pdf(options), data)`
71
72
  resolves the document bytes. One compile serves any number of configurations (A4 and letter from
72
73
  the same report). Compile at startup and render per request. Definition problems throw at
73
74
  `report()`, at compile time.
@@ -81,14 +82,14 @@ The compiled report carries `stream` (the raw event generator), `names`, `functi
81
82
  `paths`, like every quario report. Engine-level options (`query` budgets, the license key)
82
83
  live on the instance, and `q.license` settles with the verification result.
83
84
 
84
- Rendering is asynchronous and hands the loop back between batches, so a large report never blocks the
85
+ Rendering is asynchronous and returns the loop between batches, so a large report never blocks the
85
86
  host. Render-time failures reject with located errors.
86
87
 
87
88
  ### Options
88
89
 
89
90
  Page size and fonts are target configuration. The margin is the target's unless the document
90
- declares `page.margin`, in which case the document's is used; a document and a target that both
91
- declare one reject the render.
91
+ declares `page.margin`, in which case the document's margin wins. A document and a target that
92
+ both declare one reject the render.
92
93
 
93
94
  ```js
94
95
  {
@@ -98,7 +99,7 @@ declare one reject the render.
98
99
  }
99
100
  ```
100
101
 
101
- `size` defaults to `A4` (595.28 × 841.89 pt); an array is a custom `[width, height]` in points. An
102
+ `size` defaults to `A4` (595.28 × 841.89 pt). An array is a custom `[width, height]` in points. An
102
103
  unknown size name is an error the factory throws. `margin` applies to all four sides and defaults to 54
103
104
  (0.75 in). Text renders at 10pt when nothing declares a size: a document's type size is the
104
105
  document's own, so it is the report's `style.size`, not a host option. Line leading is 1.4× a line's
@@ -108,8 +109,8 @@ largest font size.
108
109
 
109
110
  **Guaranteed** normative behavior a conforming target must produce:
110
111
 
111
- - Table header rows repeat after every page break. An unstyled table draws no rules; an
112
- authored border on a header row or a total row is drawn at the width written.
112
+ - Table header rows repeat after every page break. An unstyled table draws no rules. The target draws an
113
+ authored border on a header row or a total row at the width written.
113
114
  - A group header always travels with its first content unit: the first detail lines, or a table's
114
115
  header plus its first row. An instance's headers also repeat at the top of every page it
115
116
  continues onto (outermost first, then content) and stop when the instance ends.
@@ -117,25 +118,25 @@ largest font size.
117
118
  - A row that fits on a page is never split across one.
118
119
  - `break: "page"` on a group opens a fresh page per instance.
119
120
  - `reset: "page"` on a group does the same and restarts `page.number` / `page.total` for that instance's sequence.
120
- - Authored column `width` percentages are honoured; remaining columns are measured from
121
- content and scaled to fill the rest. A table whose authored shares leave the width-less columns
121
+ - The target honours authored column `width` percentages. It measures the remaining columns from
122
+ content and scales them to fill the rest. A table whose authored shares leave the width-less columns
122
123
  nothing is a definition error, so there is no over-commitment case.
123
124
  - Hidden cells keep their column slot.
124
125
  - Page bands render on every page, their heights reserved out of the body area.
125
- - A report header declaring `height` pins a box of that many points from the page top; its items
126
+ - A report header declaring `height` pins a box of that many points from the page top. Its items
126
127
  pack from the top and the next band starts where the box ends.
127
- - Every group instance opens with a half-line gap, dropped at a page top; authored `spaceBefore`
128
+ - Every group instance opens with a half-line gap, dropped at a page top. Authored `spaceBefore`
128
129
  drops at a fresh page or strip top the same way.
129
130
  - An item or row a page cannot hold whole still carries its box: each slice draws the sides the
130
131
  break left it, the top on the first and the bottom on the last.
131
- - A split lays out as one block at the height of its tallest slot; every slot's box is the split's
132
+ - A split lays out as one block at the height of its tallest slot. Every slot's box is the split's
132
133
  height, and a slot's `valign` places its content in that slack, as a table cell's does in its
133
134
  row's.
134
135
  - Page columns are strips: the flow fills one to its foot, moves to the next, and turns the page
135
- once the last is spent; a table restates its headings at a strip head, a group's headers only
136
+ once the last is spent. A table restates its headings at a strip head, and a group's headers only
136
137
  at a page head.
137
- - The band-role defaults (a bold, larger report header; bold group headers) sit under the
138
- author's own style, and `uppercase` capitalises the drawn string before it is measured.
138
+ - The band-role defaults (a bold, larger report header, and bold group headers) sit under the
139
+ author's own style. `uppercase` capitalises the drawn string before the target measures it.
139
140
  - An image draws at the size its `fit` dictates (`natural` at 96 dpi capped at the content
140
141
  width, `width` scaled to it, aspect ratio preserved) and is never split across a page break:
141
142
  one that does not fit the remaining height moves whole to a fresh page.
@@ -154,9 +155,9 @@ without authored margins.
154
155
 
155
156
  The base-14 Helvetica, Times, and Courier families carry the default output (`family: "sans"`,
156
157
  `"serif"`, `"mono"`, each with regular, bold, italic, and bold-italic faces), using WinAnsi
157
- encoding. Characters a face cannot draw render as `?` rather than failing the report, because cell
158
- text is untrusted data and one stray character must not take a document down. WinAnsi is the
159
- base-14 limit; a TrueType face's cmap is its own.
158
+ encoding. Characters a face cannot draw render as `?` rather than failing the report, because
159
+ quario does not trust cell text, and one stray character must not take a document down. WinAnsi
160
+ is the base-14 limit. A TrueType face's cmap is its own.
160
161
 
161
162
  For full Unicode, supply TrueType families and select them by name:
162
163
 
@@ -169,10 +170,10 @@ const target = pdf({
169
170
  // then, in the schema: style: { family: "Inter" }
170
171
  ```
171
172
 
172
- Missing variants fall back to the family's regular. Embedded text uses the font's own metrics, is
173
- subset to the glyphs the document uses, and carries a ToUnicode map so extraction and
174
- copy see the original text. Fonts are trusted host assets, like registered functions. A mapping
175
- without a `regular` is refused by the factory (`options.fonts.<name>.regular: required`); bytes
173
+ Missing variants fall back to the family's regular. Embedded text uses the font's own metrics.
174
+ The target subsets it to the glyphs the document uses and carries a ToUnicode map, so extraction
175
+ and copy see the original text. quario trusts fonts as host assets, like registered functions. The
176
+ factory refuses a mapping without a `regular` (`options.fonts.<name>.regular: required`). Bytes
176
177
  the parser cannot read reject the render, located at the same option path.
177
178
 
178
179
  ## Outline
@@ -183,27 +184,38 @@ when there is none. Ungrouped reports have no outline.
183
184
 
184
185
  ## Determinism
185
186
 
186
- The document's dates are pinned rather than stamped and `meta` is opt-in, so the same schema,
187
- data, and options produce **byte-identical** output. That makes report bytes cacheable, diffable,
187
+ This target pins the document's dates rather than stamping them, and `meta` is opt-in, so the
188
+ same schema, data, and options produce **byte-identical** output. That makes report bytes cacheable, diffable,
188
189
  and safe to compare in tests.
189
190
 
190
191
  ## Unlicensed marking
191
192
 
192
193
  An unlicensed render draws the wording from `report-start.marking` once per page, after
193
194
  everything else: a translucent grey line corner-to-corner over content and page furniture. The
194
- per-page presence is normative; exact geometry is best-effort. A licensed render draws nothing.
195
+ per-page presence is normative. Exact geometry is best-effort. A licensed render draws nothing.
195
196
 
196
197
  ## Page furniture
197
198
 
198
- Paper size and fonts are options, not schema; the margin is an option a document may declare in
199
+ Paper size and fonts are options, not schema. The margin is an option a document may declare in
199
200
  its stead. Authored watermarks and finer break control stay out of scope. Keep-together and continuation headers are by construction, above.
200
201
 
202
+ The full contract is [The PDF target](https://getquario.com/docs/diving-deeper/pdf-target/),
203
+ with each declaration's fate in the
204
+ [support matrix](https://getquario.com/docs/reference/support-matrix/).
205
+
206
+ ## Documentation
207
+
208
+ [The quario documentation](https://getquario.com/docs/) is the reference.
209
+ The [report schema](https://getquario.com/docs/reference/report-schema/) is the normative
210
+ specification of what a report may declare, and
211
+ [`@quario/pdf`](https://getquario.com/docs/reference/pdf/) is this package's own API.
212
+
201
213
  ## License
202
214
 
203
- Commercial software with readable source. Free, unlimited, watermarked evaluation; per-developer
204
- licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
215
+ Commercial software with readable source. Evaluation is free, unlimited, and watermarked.
216
+ Per-developer licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
205
217
 
206
- Pass your license key once, on the instance; it is verified offline:
218
+ Pass your license key once, on the instance. quario verifies it offline:
207
219
 
208
220
  ```js
209
221
  const q = quario({ license: "quario_..." });
package/lib/index.d.ts CHANGED
@@ -14,7 +14,11 @@ export interface PdfMeta {
14
14
  /** One embeddable TrueType family: the layout's own, measured and embedded from the same bytes. */
15
15
  export type PdfFontFamily = LayoutFontFamily;
16
16
 
17
- /** Host controls, taken and validated at the factory call. */
17
+ /**
18
+ * Host controls, taken and validated at the factory call: an option this
19
+ * target does not understand, or one of the wrong type, throws a `TypeError`
20
+ * there rather than costing the host the option in silence.
21
+ */
18
22
  export interface PdfOptions {
19
23
  page?: PdfPage;
20
24
  meta?: PdfMeta;
package/lib/index.js CHANGED
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { PDFDocument } from "@cantoo/pdf-lib";
17
17
  import { layout } from "@quario/layout";
18
+ import { hostMeta, hostOptions } from "quario";
18
19
  import { embedFonts } from "./embed.js";
19
20
  import { outline } from "./outline.js";
20
21
  import { paint } from "./painter.js";
@@ -26,13 +27,16 @@ import { paint } from "./painter.js";
26
27
  // Optional document information. Never a date: pdf-lib stamps the current time
27
28
  // unless told otherwise, and a timestamp would make the same input render
28
29
  // different bytes on every run. Each key names the pdf-lib setter it feeds,
29
- // here rather than in a table, and a value of any other shape is left off.
30
+ // here rather than in a table, and that mapping is all this target owns of
31
+ // `meta` -- the contract itself is the engine's `hostMeta`, checked at the
32
+ // factory call, so a present property is a string and a nullish one is a
33
+ // property the host did not write.
30
34
  /** @type {(doc: any, meta: any) => void} */
31
35
  let describe = (doc, meta) => {
32
36
  doc.setCreationDate(new Date(0));
33
37
  doc.setModificationDate(new Date(0));
34
38
  let field = (/** @type {string} */ method, /** @type {any} */ value) => {
35
- if (typeof value === "string") doc[method](value);
39
+ if (value != null) doc[method](value);
36
40
  };
37
41
  if (meta) {
38
42
  field("setTitle", meta.title);
@@ -61,7 +65,9 @@ let open = async (meta, custom) => {
61
65
  * The target (see SCHEMA.md, "Instances and targets").
62
66
  */
63
67
  export function pdf(options) {
64
- let { page, fonts: custom, meta } = options ?? {};
68
+ hostOptions(options, ["page", "meta", "fonts"], "options");
69
+ let { page, fonts: custom, meta: written } = options ?? {};
70
+ let meta = hostMeta(written, "options.meta");
65
71
  let paged = layout({ page, fonts: custom });
66
72
  /** @type {(stream: any) => (data?: any) => Promise<Uint8Array>} */
67
73
  let compile = (stream) => {
package/lib/painter.js CHANGED
@@ -80,6 +80,7 @@ let paintLine = ({ page, height }, op) =>
80
80
  });
81
81
 
82
82
  /** @type {(painter: Painter, op: any) => void} */
83
+ // fallow-ignore-next-line code-duplication -- one operator path, so a face is embedded once per page; ink, baseline, rotation and opacity all differ, and the marking's draw stays its own
83
84
  let paintText = ({ page, height, fonts, faceKey }, op) => {
84
85
  let font = fontFor(fonts, op.font);
85
86
  page.pushOperators(
@@ -100,6 +101,7 @@ let paintText = ({ page, height, fonts, faceKey }, op) => {
100
101
  // face reuses the page's `faceKey` resource instead of growing the dictionary
101
102
  // per draw.
102
103
  /** @type {(painter: Painter, op: any) => void} */
104
+ // fallow-ignore-next-line code-duplication -- one operator path, so a face is embedded once per page; ink, baseline, rotation and opacity all differ, and the marking's draw stays its own
103
105
  let paintMark = ({ page, height, fonts, faceKey }, op) => {
104
106
  let font = fontFor(fonts, op.font);
105
107
  page.pushOperators(
@@ -126,7 +128,7 @@ let paintMark = ({ page, height, fonts, faceKey }, op) => {
126
128
  // The embedders parse the bytes, and what they throw when those bytes are bad
127
129
  // says nothing an author could act on: a corrupt PNG used to reach a host as a
128
130
  // bare `RangeError: Invalid typed array length: 0`, naming no image and no
129
- // item (quario-e0bz). The engine vouched for the magic numbers and the layout
131
+ // item (quario-e0bz). The engine vouched for the magic numbers and
130
132
  // read the size out of the header, so a file corrupt past that point arrives
131
133
  // here looking like any other and this is the first place that knows. The
132
134
  // embedder's own class and words survive behind the path, which is the shape
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/pdf",
3
- "version": "0.7.0",
3
+ "version": "0.9.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,21 +36,21 @@
36
36
  "postpack": "node -e \"require('fs').rmSync('LICENSE',{force:true})\""
37
37
  },
38
38
  "dependencies": {
39
- "@cantoo/pdf-lib": "^2.9.1",
40
- "@quario/layout": "^0.4.0"
39
+ "@cantoo/pdf-lib": "~2.9.1",
40
+ "@quario/layout": "^0.6.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@arethetypeswrong/cli": "^0.18.3",
44
44
  "@size-limit/preset-small-lib": "^13.0.3",
45
45
  "@types/fontkit": "^2.0.9",
46
46
  "fontkit": "^2.0.4",
47
- "quario": "^0.7.0",
47
+ "quario": "^0.9.0",
48
48
  "size-limit": "^13.0.3",
49
49
  "typescript": "^7.0.2"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "fontkit": "^2.0.4",
53
- "quario": "^0.7.0"
53
+ "quario": "^0.9.0"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "fontkit": {