@quario/pdf 0.8.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,5 +1,60 @@
1
1
  # @quario/pdf
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
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.
14
+
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.
18
+
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
+
3
58
  ## 0.8.0
4
59
 
5
60
  ### Minor Changes
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,19 +184,19 @@ 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
 
201
202
  The full contract is [The PDF target](https://getquario.com/docs/diving-deeper/pdf-target/),
@@ -211,10 +212,10 @@ specification of what a report may declare, and
211
212
 
212
213
  ## License
213
214
 
214
- Commercial software with readable source. Free, unlimited, watermarked evaluation; per-developer
215
- 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.
216
217
 
217
- 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:
218
219
 
219
220
  ```js
220
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/pdf",
3
- "version": "0.8.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.5.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.8.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.8.0"
53
+ "quario": "^0.9.0"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "fontkit": {