@quario/pdf 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to @quario/pdf are documented in this file.
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).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-08-27
11
+
12
+ ### Added
13
+
14
+ - **Paginated PDFs from the same compiled report.** The band flow places
15
+ headers, groups, and tables; pdf-lib writes the file. Page furniture and an
16
+ outline from the group tree. Optional TrueType embedding through
17
+ `@pdf-lib/fontkit`.
package/LICENSE ADDED
@@ -0,0 +1,219 @@
1
+ # Quario License Agreement
2
+
3
+ > **In short:** Quario is commercial software with visible source.
4
+ > Evaluate it free for as long as you like. Evaluation output is
5
+ > watermarked. Anything else needs a per-developer license, after
6
+ > which deployment is unlimited and royalty-free. This summary is not
7
+ > the license; the full terms below govern.
8
+
9
+ Version 1.0, August 2026
10
+
11
+ This agreement is between you and Webstronauts, trading as Quario,
12
+ established in the Netherlands ("we", "us"), and governs the Software
13
+ defined below.
14
+
15
+ Quario is commercial software. Its packages ship with readable source
16
+ code, but it is not open source. No rights are granted beyond those in
17
+ this agreement.
18
+
19
+ ## 1. Definitions
20
+
21
+ - The **Software** is any software we publish under the Quario name,
22
+ including the `quario` and `@quario/*` packages, except where a
23
+ product states its own terms. Each release states the edition of this
24
+ agreement that governs it.
25
+ - **You** means the individual exercising rights under this agreement
26
+ and, where that individual acts for a company or other organization,
27
+ that organization. In that case the organization is the licensee, and
28
+ the rights and limits in this agreement apply to it as a whole.
29
+ - An **Application** is software you create that incorporates the
30
+ Software and adds substantial functionality beyond it. A
31
+ licensee-operated service that embeds the Software and renders
32
+ reports for your End Users is an Application under this agreement,
33
+ even if it does little more than render.
34
+ - A **Developer** is any individual (employee or contractor working on
35
+ your behalf) who writes or maintains code that uses the Software's
36
+ APIs, or who creates or maintains report definitions for use with the
37
+ Software. That includes people who never install or run the Software
38
+ themselves, when their definitions are rendered by a service that
39
+ embeds it.
40
+ - A **Seat** is a paid license for one named Developer, purchased from
41
+ us or an authorized reseller.
42
+ - **End Users** are people who interact with the Software only through
43
+ an Application: running it, viewing the reports it produces, or
44
+ supplying parameters or other input to existing report definitions
45
+ the Application provides. Creating or maintaining report definitions
46
+ makes someone a Developer. End Users do not purchase a license; their
47
+ rights come from section 5.
48
+
49
+ ## 2. Acceptance
50
+
51
+ This file is our standing offer. Anyone who exercises the evaluation
52
+ rights in section 3 accepts them, subject to their conditions. All
53
+ other rights arise when Seats are purchased. Where a purchase is made
54
+ for an organization, the person completing it confirms that they are
55
+ authorized to accept this agreement on the organization's behalf, and
56
+ acceptance covers the whole organization. If you do not accept this
57
+ agreement, do not use the Software.
58
+
59
+ ## 3. Evaluation
60
+
61
+ You may download, install, and use the Software free of charge to
62
+ evaluate it: assessing whether it suits your purposes, and building
63
+ prototypes or proofs of concept. Evaluation covers the full feature
64
+ set, needs no key, and has no fixed time limit. The Software marks
65
+ evaluation output as described in section 6.
66
+
67
+ Evaluation is limited to assessment: you may not distribute any
68
+ Application containing the Software, use the Software in live business
69
+ operations, or expose it in services offered to third parties. The
70
+ moment your use goes beyond evaluating the Software, it requires Seats
71
+ under section 4.
72
+
73
+ ## 4. Developer licenses
74
+
75
+ Any use of the Software beyond section 3 requires a Seat for every
76
+ Developer, whether or not an Application has shipped. With those Seats
77
+ in place:
78
+
79
+ - Each licensed Developer may install and use the Software on any number
80
+ of machines for developing Applications.
81
+ - Automated build, test, and CI systems do not consume Seats.
82
+ - Seats are per named individual. They may be reassigned when a
83
+ Developer stops working with the Software, but not shared or pooled
84
+ between concurrently active Developers. Contractors' work for you is
85
+ covered by your Seats; work they do for other clients is not.
86
+
87
+ ## 5. Distribution and runtime
88
+
89
+ With Seats under section 4 in place, you may deploy and operate your
90
+ Applications yourself, and distribute the Software as an embedded part
91
+ of them, **royalty-free**: no additional fees or per-deployment
92
+ licenses are required for any number of servers, copies, or End Users.
93
+ You may embed your license key in an Application and ship it at no
94
+ charge, so section 6 does not mark that Application's output.
95
+
96
+ For that purpose, we grant you the right to pass through to each End
97
+ User, and to anyone who distributes or operates your Application on your
98
+ behalf, a limited, non-exclusive, non-transferable right to execute the
99
+ Software only as embedded in the Application. You must not permit them
100
+ to extract the Software from the Application or use it to develop
101
+ software of their own.
102
+
103
+ If you operate a service that embeds the Software and renders reports
104
+ for your End Users, that service is an Application under section 1. You
105
+ may run it royalty-free under this section, and you may put your license
106
+ key in it as in any Application. Calling applications that only request
107
+ renders from it need not incorporate the Software. People who use those
108
+ applications only as End Users under section 1 get the pass-through
109
+ rights above.
110
+
111
+ You may not:
112
+
113
+ - distribute the Software on its own, or as part of a product whose
114
+ primary purpose is to provide report-engine, report-design, or
115
+ document-generation capability to third-party developers;
116
+ - expose the Software's APIs to third parties as a development tool or
117
+ service that substitutes for those parties buying their own Seats. A
118
+ licensee-operated service that embeds the Software and renders for
119
+ your own End Users falls outside that ban;
120
+ - fork, mirror, repackage, rename, or wrap the Software, or republish
121
+ it on any package registry or code-hosting platform, other than as
122
+ embedded in an Application.
123
+
124
+ For as long as this agreement is in force, you may not create any tool,
125
+ library, product, or service that competes with the Software or any
126
+ other Quario product, whether the competition is direct or indirect;
127
+ this restriction does not survive termination. You may not remove or
128
+ alter copyright or license notices in the Software. If your Application
129
+ is itself a closed development product for third-party developers (OEM
130
+ redistribution), contact us for separate terms.
131
+
132
+ ## 6. License keys and output marking
133
+
134
+ The Software accepts a license key that evidences your Seats under
135
+ section 4. Key checks run offline.
136
+
137
+ Without a valid key, or with a key whose validity period does not cover
138
+ the installed version's release date, the Software runs with its full
139
+ feature set but marks its output as unlicensed. Using a key you are not
140
+ entitled to is a material breach of this agreement.
141
+
142
+ You may not remove, disable, or circumvent the key check or the output
143
+ marking, or distribute anything that does.
144
+
145
+ ## 7. Term, updates, and perpetual fallback
146
+
147
+ Seats are sold as an annual subscription. While the subscription is
148
+ active, licensed Developers receive all Software versions we release,
149
+ and support for the Software through our issue tracker and by email. We
150
+ provide support with commercially reasonable effort; we do not guarantee
151
+ specific response times.
152
+
153
+ If the subscription lapses, your license to the versions released during
154
+ your subscription is **perpetual, for the number of Seats held at the
155
+ lapse**: up to that many Developers may keep using those versions, and
156
+ your Applications may keep shipping them, forever, at no further cost.
157
+ Versions released after the lapse require renewal. We encode this rule
158
+ in purchased keys: a key remains valid for every version released
159
+ during its validity period, with no end date.
160
+
161
+ ## 8. Source availability
162
+
163
+ The Software's packages ship with readable source code so that you can
164
+ review and debug what you run. Readable source does not grant extra
165
+ rights: this agreement governs all use of the source, and you may
166
+ publish the Software (modified or not) only as part of an Application
167
+ under section 5.
168
+
169
+ ## 9. Ownership, trademarks, and feedback
170
+
171
+ We license the Software; we retain all intellectual property rights in
172
+ it. Report definitions, data, and output you create with the Software
173
+ are yours. "Quario" and the Quario logo are our trademarks; this
174
+ agreement grants no rights to use them beyond identifying the Software.
175
+ If you send us feedback or suggestions, we may use them without
176
+ obligation.
177
+
178
+ ## 10. Warranty and liability
179
+
180
+ We provide the Software as is. To the fullest extent the law allows, we
181
+ disclaim all warranties and conditions, express or implied, including
182
+ merchantability, fitness for a particular purpose, and non-infringement.
183
+
184
+ To the fullest extent the law allows: we are not liable for indirect or
185
+ consequential damages, loss of profits, loss of data, or business
186
+ interruption; and our total aggregate liability under this agreement is
187
+ limited to the greater of EUR 100 and the fees you paid us in the
188
+ twelve months preceding the event giving rise to the claim. These
189
+ limitations do not apply where the damage results from the intent or
190
+ deliberate recklessness (opzet of bewuste roekeloosheid) of us or our
191
+ management, or where mandatory law does not permit them.
192
+
193
+ ## 11. Termination and survival
194
+
195
+ If you materially breach this agreement, we may terminate it by notice.
196
+ If the breach can be remedied, we will first allow 30 days from that
197
+ notice to remedy it, and termination takes effect only if the breach
198
+ remains. On termination, the licenses granted here end.
199
+ Perpetual-fallback rights under section 7 survive termination only if
200
+ they arose from a lapse that predates the breach, and section 5
201
+ survives with them to the extent needed to keep exercising them.
202
+ Sections 8 through 12 survive any termination.
203
+
204
+ ## 12. General
205
+
206
+ Dutch law governs this agreement. Disputes go to the competent court for
207
+ our registered office. You may not assign this agreement without our
208
+ consent, except to a successor of your whole business; we may assign it
209
+ to a successor of ours. If a provision is unenforceable, the remainder
210
+ stays in force. Failure to enforce a provision does not waive it. This
211
+ agreement, together with your order for Seats, is the entire agreement
212
+ about the Software, unless we agree otherwise in writing.
213
+
214
+ ---
215
+
216
+ **Purchasing and contact:** <contact@getquario.com> ·
217
+ <https://getquario.com>
218
+
219
+ Copyright © 2026 Webstronauts. All rights reserved.
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # @quario/pdf
2
+
3
+ **The PDF render target for [quario](https://www.npmjs.com/package/quario).** Renders a report
4
+ definition straight to a paginated PDF. No headless browser, no print CSS, deterministic bytes.
5
+
6
+ quario does the typesetting: pagination, keep-together, tables, fonts, the document outline.
7
+ [pdf-lib](https://pdf-lib.js.org) writes the file. You get typesetting plus pdf-lib: no Chromium
8
+ in your container, no page-load race, no fonts-not-ready flake.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install quario @quario/pdf
14
+ ```
15
+
16
+ pdf-lib comes along as a dependency; the engine is a peer, installed beside it. ESM-only, Node 22+, and browser-ready through
17
+ any standards-based ESM bundler. The renderer returns bytes, so the host decides where they go.
18
+
19
+ Embedding TrueType fonts also needs the optional peer:
20
+
21
+ ```bash
22
+ npm install @pdf-lib/fontkit
23
+ ```
24
+
25
+ It loads only when you pass `fonts` to the factory. The base-14 families need nothing extra.
26
+
27
+ ## Quick start
28
+
29
+ ```js
30
+ import { writeFileSync } from "node:fs";
31
+ import { pdf } from "@quario/pdf";
32
+ import { quario } from "quario";
33
+
34
+ const report = quario().report(schema, funcs);
35
+ const bytes = await report.render(
36
+ pdf({ page: { size: "A4", margin: 54 }, meta: { title: "Sales 2026" } }),
37
+ data,
38
+ );
39
+
40
+ writeFileSync("sales.pdf", bytes);
41
+ ```
42
+
43
+ Any schema the HTML target renders works here unchanged. Add page bands when you want running
44
+ headers and page numbers. Page bands are the one part of the schema that only paginated targets
45
+ consume:
46
+
47
+ ```js
48
+ const paged = {
49
+ ...schema,
50
+ page: {
51
+ header: [{ type: "text", value: "{{ $.params.title }}", style: { size: 8, color: "#777777" } }],
52
+ footer: [
53
+ { type: "text", value: "{{ page.number }} / {{ page.total }}", style: { size: 8, align: "right" } },
54
+ ],
55
+ },
56
+ };
57
+ ```
58
+
59
+ `visible: "=page.number > 1"` on a band is the cover-page recipe. Gate band _visibility_ on
60
+ `page.number` only. Band heights are reserved before the real page count is known, so
61
+ `=page.total > 10` reserves nothing. `page.total` is fine in band _text_.
62
+
63
+ ## API
64
+
65
+ ### `pdf(options?)`
66
+
67
+ The target factory takes this target's host options, validates them at the call, and returns
68
+ the target you pass to `render`. `report()` compiles once and `report.render(pdf(options), data)`
69
+ resolves the document bytes. One compile serves any number of configurations (A4 and letter from
70
+ the same report). Compile at startup and render per request. Definition problems throw at
71
+ `report()`, at compile time.
72
+
73
+ ```js
74
+ const report = quario().report(schema, funcs);
75
+ const bytes = await report.render(pdf({ page: { size: "letter" } }), data);
76
+ ```
77
+
78
+ The compiled report carries `stream` (the raw event generator), `names`, `functions`, and
79
+ `paths`, like every quario report. Engine-level options (`query` budgets, the license key)
80
+ live on the instance, and `q.license` settles with the verification result.
81
+
82
+ Rendering is asynchronous and hands the loop back between batches, so a large report never blocks the
83
+ host. Render-time failures reject with located errors.
84
+
85
+ ### Options
86
+
87
+ Page geometry is target configuration, never schema:
88
+
89
+ ```js
90
+ {
91
+ page: { size: "A4", margin: 54 }, // 'A4' | 'letter' | [width, height] in points
92
+ baseSize: 10, // base font size in points
93
+ meta: { title, author, subject }, // optional /Info — strings only, never a date
94
+ fonts: { Inter: { regular, bold, italic, boldItalic } }, // TrueType bytes
95
+ }
96
+ ```
97
+
98
+ `size` defaults to `A4` (595.28 × 841.89 pt); an array is a custom `[width, height]` in points. An
99
+ unknown size name is a definition error, thrown by the factory. `margin` applies to all four sides and defaults to 54
100
+ (0.75 in). `baseSize` defaults to 10 and style `size` declarations override it per element; line
101
+ leading is 1.4× a line's largest font size.
102
+
103
+ ## Layout
104
+
105
+ **Guaranteed** normative behavior a conforming target must produce:
106
+
107
+ - Table header rows repeat after every page break, with a rule under the header and above the
108
+ total row.
109
+ - A group header always travels with its first content unit: the first detail lines, or a table's
110
+ header plus its first row. An instance's headers also repeat at the top of every page it
111
+ continues onto (outermost first, then content) and stop when the instance ends.
112
+ - The total row keeps the last data row with it.
113
+ - A row that fits on a page is never split across one.
114
+ - `break: "page"` on a group opens a fresh page per instance.
115
+ - `reset: "page"` on a group does the same and restarts `page.number` / `page.total` for that instance's sequence.
116
+ - Authored column `width` percentages are honoured; remaining columns are measured from
117
+ content and scaled to fill the rest. A table whose authored shares leave the width-less columns
118
+ nothing is a definition error, so there is no over-commitment case.
119
+ - Hidden cells keep their column slot.
120
+ - Page bands render on every page, their heights reserved out of the body area.
121
+ - An image draws at the size its `fit` dictates (`natural` at 96 dpi capped at the content
122
+ width, `width` scaled to it, aspect ratio preserved) and is never split across a page break:
123
+ one that does not fit the remaining height moves whole to a fresh page.
124
+
125
+ **Best-effort** current implementation, pinned by no test and changeable
126
+ without a breaking change: paragraph widow and orphan avoidance, slicing a row taller than the
127
+ page, downscaling an image taller than any page, embedding repeated image bytes once, and where
128
+ an image's page break lands. Do not write definitions that depend on these.
129
+
130
+ Bands flow vertically in walk order, matching the HTML target's divs. Group nesting adds no
131
+ indentation, but every instance opens with a structural half-line gap so groups read as blocks
132
+ without authored margins.
133
+
134
+ ## Fonts and text
135
+
136
+ The base-14 Helvetica, Times, and Courier families carry the default output (`family: "sans"`,
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.
140
+
141
+ For full Unicode, supply TrueType families and select them by name:
142
+
143
+ ```js
144
+ import { readFileSync } from "node:fs";
145
+
146
+ const target = pdf({
147
+ fonts: { Inter: { regular: readFileSync("Inter-Regular.ttf"), bold: readFileSync("Inter-Bold.ttf") } },
148
+ });
149
+ // then, in the schema: style: { family: "Inter" }
150
+ ```
151
+
152
+ Missing variants fall back to the family's regular. Embedded text uses the font's own metrics, is
153
+ subset to the glyphs the document uses, and carries a ToUnicode map so extraction and
154
+ copy see the original text. Fonts are trusted host assets, like registered functions.
155
+
156
+ ## Outline
157
+
158
+ A grouped report carries a document outline (bookmarks) mirroring the group tree: one open entry
159
+ per instance, nested by depth, titled by the instance's first group-header text, or `name: key`
160
+ when there is none. Ungrouped reports have no outline.
161
+
162
+ ## Determinism
163
+
164
+ The document's dates are pinned rather than stamped and `meta` is opt-in, so the same schema,
165
+ data, and options produce **byte-identical** output. That makes report bytes cacheable, diffable,
166
+ and safe to compare in tests.
167
+
168
+ ## Unlicensed marking
169
+
170
+ An unlicensed render draws the wording from `report-start.marking` once per page, after
171
+ everything else: a translucent grey line corner-to-corner over content and page furniture. The
172
+ per-page presence is normative; exact geometry is best-effort. A licensed render draws nothing.
173
+
174
+ ## Page furniture
175
+
176
+ Paper size, margins, and fonts are options, not schema. Authored watermarks and finer break
177
+ control stay out of scope. Keep-together and continuation headers are by construction, above.
178
+
179
+ ## License
180
+
181
+ Commercial software with readable source. Free, unlimited, watermarked evaluation; per-developer
182
+ licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
183
+
184
+ Pass your license key once, on the instance; it is verified offline:
185
+
186
+ ```js
187
+ const q = quario({ license: "quario_..." });
188
+ await q.license; // { licensed: true, licensee: "Acme BV", id: "1-ACME" }
189
+ ```
package/lib/balance.js ADDED
@@ -0,0 +1,90 @@
1
+ // The one number a balanced region decides: how tall each of its strips is
2
+ // (ADR 0020 — 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
+ };