@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 +47 -0
- package/README.md +33 -13
- package/lib/index.d.ts +1 -1
- package/lib/painter.js +36 -2
- package/package.json +4 -4
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,
|
|
62
|
-
`=page.total > 10` reserves
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
127
|
-
without a breaking change: paragraph widow and orphan avoidance,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
53
|
+
"quario": "^0.7.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"fontkit": {
|