@quario/pdf 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-09-07
11
+
12
+ ### Added
13
+
14
+ - **Styled runs reach the page.** A cell value written as a list of styled runs
15
+ draws each run in its own face, size and colour, underlines and strikes
16
+ through per run rather than across the line, and paints a run's `background`
17
+ as a highlight behind its text.
18
+
19
+ ### Changed
20
+
21
+ - **A `format` inside a sentence now presents where the engine says it does.** A
22
+ cell mixing literal text and one interpolation under a `format` draws the
23
+ value plainly, as the spreadsheet and CSV targets already did; splitting the
24
+ value into styled runs is how a value inside a sentence is formatted.
25
+
26
+ ## [0.6.0] - 2026-09-07
27
+
28
+ ### Added
29
+
30
+ - **A cell's own `currency` code is honoured**, ahead of the instance's
31
+ default, so a listing whose rows arrive in different currencies presents each
32
+ in its own. A code the engine could not accept leaves the cell as plain
33
+ display text rather than presenting it in the instance's currency.
34
+
35
+ ### Changed
36
+
37
+ - **Numbers presented through `format` now show a fixed two fraction digits,
38
+ matching every other target.** `format: "number"` on `1000` renders
39
+ `1,000.00` where it rendered `1,000`, and `0.12345` renders `0.12` where it
40
+ rendered `0.123`; `format: "percent"` on `0.21` renders `21.00%` where it
41
+ rendered `21%`. `format: "currency"` follows the currency's own minor units,
42
+ so a JPY amount loses the two decimals it never had. `date` is unchanged, as
43
+ are grouping separators and symbol placement. The digits come from the
44
+ engine, so a cell reads the same here as it does in a workbook.
45
+
46
+ ### Fixed
47
+
48
+ - **An image these bytes cannot embed now fails with the item's name on it.**
49
+ A PNG whose pixel data is corrupt past the size in its header used to reject
50
+ the render with `Invalid typed array length: 0` — the PDF writer's own words,
51
+ naming neither an image nor an item, so a report with two pictures gave no
52
+ way to tell which was at fault. The message now begins with the item's
53
+ `source` path and says the image could not be embedded, keeping the writer's
54
+ account after it and on `cause`. A JPEG is unchanged: its header is read but
55
+ its pixel data never is, so a corrupt one still embeds without complaint.
56
+
10
57
  ## [0.5.0] - 2026-09-05
11
58
 
12
59
  ### Added
package/README.md CHANGED
@@ -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, so
62
- `=page.total > 10` reserves nothing. `page.total` is fine in band _text_.
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.
63
+ `page.total` is fine in band _text_.
63
64
 
64
65
  ## API
65
66
 
@@ -85,7 +86,9 @@ host. Render-time failures reject with located errors.
85
86
 
86
87
  ### Options
87
88
 
88
- Page geometry is target configuration, never schema:
89
+ 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.
89
92
 
90
93
  ```js
91
94
  {
@@ -96,7 +99,7 @@ Page geometry is target configuration, never schema:
96
99
  ```
97
100
 
98
101
  `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
102
+ unknown size name is an error the factory throws. `margin` applies to all four sides and defaults to 54
100
103
  (0.75 in). Text renders at 10pt when nothing declares a size: a document's type size is the
101
104
  document's own, so it is the report's `style.size`, not a host option. Line leading is 1.4× a line's
102
105
  largest font size.
@@ -105,12 +108,12 @@ largest font size.
105
108
 
106
109
  **Guaranteed** normative behavior a conforming target must produce:
107
110
 
108
- - Table header rows repeat after every page break, with a rule under the header and above the
109
- total row.
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.
110
113
  - A group header always travels with its first content unit: the first detail lines, or a table's
111
114
  header plus its first row. An instance's headers also repeat at the top of every page it
112
115
  continues onto (outermost first, then content) and stop when the instance ends.
113
- - The total row keeps the last data row with it.
116
+ - The total rows keep the last data row with them.
114
117
  - A row that fits on a page is never split across one.
115
118
  - `break: "page"` on a group opens a fresh page per instance.
116
119
  - `reset: "page"` on a group does the same and restarts `page.number` / `page.total` for that instance's sequence.
@@ -119,17 +122,32 @@ largest font size.
119
122
  nothing is a definition error, so there is no over-commitment case.
120
123
  - Hidden cells keep their column slot.
121
124
  - 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
+ 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
+ drops at a fresh page or strip top the same way.
129
+ - An item or row a page cannot hold whole still carries its box: each slice draws the sides the
130
+ 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
+ height, and a slot's `valign` places its content in that slack, as a table cell's does in its
133
+ row's.
134
+ - 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
+ 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.
122
139
  - An image draws at the size its `fit` dictates (`natural` at 96 dpi capped at the content
123
140
  width, `width` scaled to it, aspect ratio preserved) and is never split across a page break:
124
141
  one that does not fit the remaining height moves whole to a fresh page.
125
142
 
126
- **Best-effort** current implementation, pinned by no test and changeable
127
- without a breaking change: paragraph widow and orphan avoidance, slicing a row taller than the
143
+ **Best-effort** current implementation, pinned only as a regression guard where the layout suite
144
+ holds it, and changeable without a breaking change: paragraph widow and orphan avoidance, the
145
+ gutter between page-column strips and whether a region balances, slicing a row taller than the
128
146
  page, downscaling an image taller than any page, embedding repeated image bytes once, and where
129
147
  an image's page break lands. Do not write definitions that depend on these.
130
148
 
131
149
  Bands flow vertically in walk order, matching the HTML target's divs. Group nesting adds no
132
- indentation, but every instance opens with a structural half-line gap so groups read as blocks
150
+ indentation, but every instance that draws opens with a structural half-line gap so groups read as blocks
133
151
  without authored margins.
134
152
 
135
153
  ## Fonts and text
@@ -153,7 +171,9 @@ const target = pdf({
153
171
 
154
172
  Missing variants fall back to the family's regular. Embedded text uses the font's own metrics, is
155
173
  subset to the glyphs the document uses, and carries a ToUnicode map so extraction and
156
- copy see the original text. Fonts are trusted host assets, like registered functions.
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
176
+ the parser cannot read reject the render, located at the same option path.
157
177
 
158
178
  ## Outline
159
179
 
@@ -175,8 +195,8 @@ per-page presence is normative; exact geometry is best-effort. A licensed render
175
195
 
176
196
  ## Page furniture
177
197
 
178
- Paper size, margins, and fonts are options, not schema. Authored watermarks and finer break
179
- control stay out of scope. Keep-together and continuation headers are by construction, above.
198
+ Paper size and fonts are options, not schema; the margin is an option a document may declare in
199
+ its stead. Authored watermarks and finer break control stay out of scope. Keep-together and continuation headers are by construction, above.
180
200
 
181
201
  ## License
182
202
 
package/lib/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { LayoutFontFamily, LayoutPage } from "@quario/layout";
2
2
  import type { Target } from "quario";
3
3
 
4
- /** Page geometry, in PostScript points: the layout's own, never schema. */
4
+ /** Page geometry, in PostScript points: the layout's own, its margin one a document may declare instead. */
5
5
  export type PdfPage = LayoutPage;
6
6
 
7
7
  /** Optional document information; never includes dates, so output stays deterministic. */
package/lib/painter.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * and one logo on every page is embedded once, the marking after them so an
10
10
  * image can never cover it (LICENSE section 6).
11
11
  */
12
- import { breathe } from "quario";
12
+ import { breathe, imageError } from "quario";
13
13
  import { degrees, drawImage, drawText, rgb } from "@cantoo/pdf-lib";
14
14
  import { fontFor } from "./embed.js";
15
15
 
@@ -122,11 +122,45 @@ let paintMark = ({ page, height, fonts, faceKey }, op) => {
122
122
 
123
123
  // Keyed by the array the source expression yielded, so one logo reused
124
124
  // across pages is embedded once and every placement references it.
125
+ //
126
+ // The embedders parse the bytes, and what they throw when those bytes are bad
127
+ // says nothing an author could act on: a corrupt PNG used to reach a host as a
128
+ // 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
130
+ // read the size out of the header, so a file corrupt past that point arrives
131
+ // here looking like any other and this is the first place that knows. The
132
+ // embedder's own class and words survive behind the path, which is the shape
133
+ // SCHEMA.md documents for every render error.
134
+ //
135
+ // **What this cannot promise is symmetry between the two formats.** pdf-lib's
136
+ // PNG embedder decompresses the pixel data, while its JPEG embedder reads
137
+ // markers alone and never the scan data, so a JPEG corrupt past its frame
138
+ // header embeds without complaint and arrives here as no failure at all.
139
+ // Corrupt scan data cannot be told from good without decoding the image, which
140
+ // is not a cost a render should pay to find out what it is about to write.
141
+ //
142
+ // Closing part of the gap was weighed and declined (`quario-dqt8`). A JPEG
143
+ // carrying no `SOS` at all is structural, as cheap to read as the size, so the
144
+ // marker walk in `@quario/layout`'s `image.js` could say so — but measuring
145
+ // what that catches settled it: a JPEG's headers are roughly fixed while its
146
+ // scan grows with the image, so the check covers truncation only up to the
147
+ // point the scan starts. On the 882-byte `logo.jpg` fixture that is 85% of the
148
+ // file; on a photograph it is a fraction of a percent, and every truncation
149
+ // worth worrying about lands past it. What it would buy is the shape of a
150
+ // hand-built repro; what it would cost an author is a rule whose seam falls in
151
+ // an arbitrary place — a JPEG whose scan never starts fails, one whose scan is
152
+ // cut does not — and the PNG/JPEG asymmetry survives either way. So the line
153
+ // stays where the engine's own guarantee stops.
125
154
  /** @type {(painter: Painter, op: any) => Promise<any>} */
126
155
  let imageOf = async ({ doc, embedded }, op) => {
127
156
  let image = embedded.get(op.bytes);
128
157
  if (!image) {
129
- image = await (op.format === "png" ? doc.embedPng(op.bytes) : doc.embedJpg(op.bytes));
158
+ try {
159
+ image = await (op.format === "png" ? doc.embedPng(op.bytes) : doc.embedJpg(op.bytes));
160
+ } catch (cause) {
161
+ let said = /** @type {Error} */ (cause).message;
162
+ throw imageError(op.path, "the image could not be embedded: " + said, cause);
163
+ }
130
164
  embedded.set(op.bytes, image);
131
165
  }
132
166
  return image;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/pdf",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",
@@ -37,20 +37,20 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@cantoo/pdf-lib": "^2.9.1",
40
- "@quario/layout": "^0.2.0"
40
+ "@quario/layout": "^0.4.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.5.0",
47
+ "quario": "^0.7.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.5.0"
53
+ "quario": "^0.7.0"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "fontkit": {