@quario/layout 0.1.0 → 0.3.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 +95 -0
- package/README.md +1 -1
- package/lib/box.js +5 -1
- package/lib/canvas.js +38 -63
- package/lib/fonts.js +16 -31
- package/lib/image.js +1 -1
- package/lib/index.d.ts +17 -3
- package/lib/index.js +17 -11
- package/lib/layout.js +536 -299
- package/lib/page.js +41 -9
- package/lib/paint.js +37 -9
- package/lib/settings.js +88 -0
- package/lib/style.js +24 -2
- package/lib/text.js +20 -26
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,101 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-09-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- The text join reads a cell's own `currency` code, ahead of the instance's
|
|
15
|
+
default, so every target built on this package presents a per-cell
|
|
16
|
+
denomination.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **A group instance that renders nothing no longer takes up space.** A group
|
|
21
|
+
whose header and footer items all resolve `visible: false`, with nothing
|
|
22
|
+
visible under it either, used to open the same half-line gap as any other
|
|
23
|
+
instance and could push the content after it onto a new page. A run of them
|
|
24
|
+
spaced whatever followed by a half-line each, so collapsing a level left the
|
|
25
|
+
rows above it at uneven distances. Such an instance now occupies nothing at
|
|
26
|
+
all and adds no PDF bookmark, which is what makes a group collapsible while
|
|
27
|
+
its rows stay in the aggregates. A group that declares `break: "page"` or
|
|
28
|
+
`reset: "page"` still starts its page either way. Every document with such a
|
|
29
|
+
group renders slightly shorter than it did.
|
|
30
|
+
|
|
31
|
+
- **Numbers presented through `format` now show a fixed two fraction digits,
|
|
32
|
+
matching every other target** — `1,000.00` where `1,000` was rendered,
|
|
33
|
+
`21.00%` where `21%` was, and a currency's own minor units in place of a
|
|
34
|
+
universal two. Because this package presents text and then **measures** it,
|
|
35
|
+
a formatted cell is now up to three characters wider than it was: a line
|
|
36
|
+
that just fitted can wrap, which can move a page break in a PDF, the viewer,
|
|
37
|
+
or an editor preview. Nothing else about wrapping changed.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **An image failure now names the item that asked for the bytes.** A file too
|
|
42
|
+
short to carry a size failed saying only that the size could not be read,
|
|
43
|
+
naming no item, so a report with two pictures gave no way to tell which one
|
|
44
|
+
was bad. The message is now prefixed with the item's `source` path, as every
|
|
45
|
+
other render error is.
|
|
46
|
+
|
|
47
|
+
- **An image the browser cannot decode no longer costs the whole page.** A
|
|
48
|
+
PNG or JPEG whose pixel data is corrupt past the size in its header lays out
|
|
49
|
+
like any other — the size is all that is read of it — and used to throw out
|
|
50
|
+
of `paint()` before its first draw op, leaving the page blank: no white
|
|
51
|
+
fill, none of the other content, and no licence marking. The image is now
|
|
52
|
+
drawn as nothing and the page is drawn around it, so what a bad image costs
|
|
53
|
+
is the image.
|
|
54
|
+
|
|
55
|
+
## [0.2.0] - 2026-09-05
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **A spanning cell is drawn as one box** across the columns it covers,
|
|
60
|
+
starting where the first of them starts. It takes no part in allocating their
|
|
61
|
+
widths, and a column no cell votes on opens at the cell-padding floor, so an
|
|
62
|
+
empty table still shows its geometry. A spanning row too tall for the page it
|
|
63
|
+
is on slices like any other, each slice following the geometry of the page or
|
|
64
|
+
page-column strip it lands in.
|
|
65
|
+
|
|
66
|
+
- **`valign`** places a cell's lines, or a slot's lines or picture, in the slack
|
|
67
|
+
its row or split leaves over its own content: middle halves it, bottom takes
|
|
68
|
+
it. The box does not move, and a picture is not scaled. A row too tall for
|
|
69
|
+
any page, sliced across pages, paints from the top.
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
- **A row's box is drawn by the row's cells.** A box declared on a table row
|
|
74
|
+
used to be one rect across the summed column widths; it is now each covered
|
|
75
|
+
cell's own, so a row's `borderBottom` still reads as one continuous edge
|
|
76
|
+
while a row's `borderLeft` becomes an edge on every cell rather than one at
|
|
77
|
+
the row's outer left. A row's border also occupies height now, as a cell's
|
|
78
|
+
always has, so a bordered row is taller by its border's width.
|
|
79
|
+
|
|
80
|
+
- **Measuring TrueType families now needs `fontkit`, not `@pdf-lib/fontkit`.**
|
|
81
|
+
Install `fontkit` instead; nothing else about `options.fonts` changes. The
|
|
82
|
+
old package's bundle crashed with a bare `ReferenceError` on any OpenType
|
|
83
|
+
face needing a shaping state machine — which is every Devanagari, Bengali,
|
|
84
|
+
Tamil, Khmer or Myanmar webfont in practice, and some fifty further scripts
|
|
85
|
+
besides. Those faces now measure where they used to throw. Which scripts a
|
|
86
|
+
face supports remains the font's and the parser's to answer, not this
|
|
87
|
+
package's.
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
|
|
91
|
+
- **A group header no longer strands above a split.** A split cannot be broken
|
|
92
|
+
across a page, so a header that introduces one has to keep the whole of it
|
|
93
|
+
company — but the keep-together test measured a split by its first two lines
|
|
94
|
+
of text, and a split holds its text in its slots, not in itself. It therefore
|
|
95
|
+
measured as nothing: the header was drawn at the foot of the page with room
|
|
96
|
+
reserved for none of what followed, the split moved on to the next page, and
|
|
97
|
+
the instance's header repeated above it there. A record card whose total row
|
|
98
|
+
is a split was drawn twice — once orphaned at a page bottom, once whole. The
|
|
99
|
+
header now moves with the split, exactly as it already moved with an image.
|
|
100
|
+
|
|
101
|
+
Where a split is taller than the page has left, the header behaves as it
|
|
102
|
+
always has before content no page can hold: it degrades to plain paginated
|
|
103
|
+
flow and repeats nothing.
|
|
104
|
+
|
|
10
105
|
## [0.1.0] - 2026-09-03
|
|
11
106
|
|
|
12
107
|
### Added
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ The engine is a peer. The one runtime dependency is `@pdf-lib/standard-fonts`, t
|
|
|
18
18
|
base-14 PDF families. To lay out with your own TrueType families, add the optional peer:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npm install
|
|
21
|
+
npm install fontkit
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
ESM-only, Node 22+, and browser-ready through a standards-based bundler. CSP-safe: no
|
package/lib/box.js
CHANGED
|
@@ -106,6 +106,10 @@ let paintEdge = (canvas, x, yTop, w, h, style, side) => {
|
|
|
106
106
|
|
|
107
107
|
/** @type {Slice} */
|
|
108
108
|
let WHOLE = { first: true, more: false };
|
|
109
|
+
// Owns both ends: a block, or a row, drawn in one piece. `sliced` builds fresh
|
|
110
|
+
// slices, so this is a predicate rather than identity against `WHOLE`.
|
|
111
|
+
/** @type {(slice: Slice) => boolean} */
|
|
112
|
+
let isWhole = ({ first, more }) => first && !more;
|
|
109
113
|
|
|
110
114
|
// The sides one slice still owns. Left and right belong to every slice; the
|
|
111
115
|
// top is the first slice's and the bottom the last's, so a middle slice
|
|
@@ -140,4 +144,4 @@ let paintBox = (canvas, x, yTop, w, h, style, bg, slice = WHOLE) => {
|
|
|
140
144
|
if (style) for (let side of sidesOf(slice)) paintEdge(canvas, x, yTop, w, h, style, side);
|
|
141
145
|
};
|
|
142
146
|
|
|
143
|
-
export { CELL_PAD, NO_PAD, WHOLE, insetOf, paintBox, sliceInset, unbox };
|
|
147
|
+
export { CELL_PAD, NO_PAD, WHOLE, insetOf, isWhole, paintBox, sliceInset, unbox };
|
package/lib/canvas.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The drawing surface. Every mark the band flow makes goes through here, so
|
|
3
|
-
* nothing above this module knows
|
|
3
|
+
* nothing above this module knows how a page is drawn on. What a page *is* —
|
|
4
|
+
* its box, and the content box a margin leaves — is `page.js`'s `Frame`, which
|
|
5
|
+
* arrives here already derived: nothing below that module works one out. What
|
|
6
|
+
* this one owns is what becomes of it after — `blank` spreads it flat onto the
|
|
7
|
+
* canvas and `adopt` narrows `top`/`bottom` in place, which is why `layout.js`
|
|
8
|
+
* re-derives the untouched page frame it hangs a header from.
|
|
4
9
|
*
|
|
5
10
|
* Two adapters satisfy one interface. `listing` records marks onto the display
|
|
6
11
|
* list — pages of ops and hit boxes, which the painters consume; `measuring`
|
|
7
12
|
* moves the cursor and marks nothing, which is how `probe` reserves a band's
|
|
8
13
|
* height without emitting it. Measuring is therefore a choice of adapter
|
|
9
14
|
* rather than a mode every primitive has to remember to check — and since a
|
|
10
|
-
* measuring canvas is built from a frame and
|
|
15
|
+
* measuring canvas is built from a frame and the render's settings, there is no
|
|
11
16
|
* parameter through which a list could reach it. Pagination is the one thing
|
|
12
17
|
* measuring must never do: a page turn would reset the cursor mid-measure and
|
|
13
18
|
* return a silently wrong height, so `newPage` throws there.
|
|
@@ -21,60 +26,27 @@
|
|
|
21
26
|
import { baseSans } from "./fonts.js";
|
|
22
27
|
import { BLACK, dressed, shift } from "./style.js";
|
|
23
28
|
|
|
29
|
+
/** @typedef {import('./page.js').Frame} Frame */
|
|
24
30
|
/** @typedef {import('./text.js').Line} Line */
|
|
25
|
-
/** @typedef {import('./text.js').Metrics} Metrics */
|
|
26
31
|
/** @typedef {import('./style.js').Color} Color */
|
|
32
|
+
// A canvas holds a render's settings and turns nothing into one: what a
|
|
33
|
+
// settings is, and how an opening event becomes it, is settings.js's.
|
|
34
|
+
/** @typedef {import('./settings.js').Settings} Settings */
|
|
27
35
|
// The list's own shapes — `Op`, `Box`, `Page` — are described once, in the
|
|
28
36
|
// hand-written public declarations, and read back here.
|
|
29
37
|
/** @import { Box, Op, Page } from './index.d.ts' */
|
|
30
38
|
|
|
31
|
-
// The page box and the content box. Settled at `report-start` and fixed
|
|
32
|
-
// thereafter: the geometry is the factory's and never moves, while `base` and
|
|
33
|
-
// `family` take the report default one event into the render, before anything
|
|
34
|
-
// is measured. The page bands are measured against the page box and take their
|
|
35
|
-
// height off `top`/`bottom` through `adopt` below, while the first page is
|
|
36
|
-
// still untouched.
|
|
37
|
-
/**
|
|
38
|
-
* `family` is the report default's typeface, normalised, and `base` its size —
|
|
39
|
-
* the two declarations that default is narrowed to. They ride the frame so a
|
|
40
|
-
* measuring canvas built off it reads the same pair the listing canvas does,
|
|
41
|
-
* and so the default reaches a node as a fallback rather than a merged layer;
|
|
42
|
-
* `layout.js`'s `adoptDefault` carries why. `family` is null when the report
|
|
43
|
-
* declares none.
|
|
44
|
-
*
|
|
45
|
-
* @typedef {{ width: number, height: number, margin: number, base: number,
|
|
46
|
-
* content: number, top: number, bottom: number, family: string | null,
|
|
47
|
-
* locale?: string, currency?: string, timeZone?: string }} Frame
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
// A frame from the page box and the base size: how `content`/`top`/`bottom`
|
|
51
|
-
// fall out of a page and a margin is derived here, once, so no caller and no
|
|
52
|
-
// suite has to restate it and drift from what a real render uses.
|
|
53
|
-
/**
|
|
54
|
-
* @type {(width: number, height: number, margin: number, base: number,
|
|
55
|
-
* family?: string | null) => Frame}
|
|
56
|
-
*/
|
|
57
|
-
let frame = (width, height, margin, base, family = null) => ({
|
|
58
|
-
width,
|
|
59
|
-
height,
|
|
60
|
-
margin,
|
|
61
|
-
base,
|
|
62
|
-
family,
|
|
63
|
-
content: width - 2 * margin,
|
|
64
|
-
top: height - margin,
|
|
65
|
-
bottom: margin,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
39
|
// `y` is the cursor on the open page and `fresh` says nothing has been drawn on
|
|
69
40
|
// it yet, which is what makes a break legal. `count` is how many pages exist.
|
|
70
41
|
/**
|
|
71
|
-
* @typedef {Frame &
|
|
42
|
+
* @typedef {Frame & { settings: Settings, y: number, fresh: boolean,
|
|
43
|
+
* count: number,
|
|
72
44
|
* newPage: () => void,
|
|
73
45
|
* rect: (color: Color, x: number, y: number, w: number, h: number) => void,
|
|
74
46
|
* stroke: (x1: number, y1: number, x2: number, y2: number, thickness: number,
|
|
75
47
|
* color: Color, dash: number[] | null) => void,
|
|
76
|
-
* picture: (
|
|
77
|
-
* w: number, h: number) => void,
|
|
48
|
+
* picture: (path: string | undefined, bytes: Uint8Array, format: string,
|
|
49
|
+
* x: number, y: number, w: number, h: number) => void,
|
|
78
50
|
* drawLine: (line: Line, x: number, yTop: number, avail: number,
|
|
79
51
|
* align: any) => void,
|
|
80
52
|
* box: (path: string | undefined, x: number, yTop: number, w: number,
|
|
@@ -89,15 +61,15 @@ let frame = (width, height, margin, base, family = null) => ({
|
|
|
89
61
|
* watermark: (mark: Mark) => void }} Listing
|
|
90
62
|
*/
|
|
91
63
|
|
|
92
|
-
// What every canvas starts as, whichever adapter it is: its frame and
|
|
93
|
-
// and a cursor that has drawn nothing yet. Shared because
|
|
94
|
-
// compares member names, not their values — two hand-written
|
|
95
|
-
// could drift in what they start from and nothing would notice.
|
|
64
|
+
// What every canvas starts as, whichever adapter it is: its frame and the
|
|
65
|
+
// render's settings, and a cursor that has drawn nothing yet. Shared because
|
|
66
|
+
// the parity test compares member names, not their values — two hand-written
|
|
67
|
+
// copies of this could drift in what they start from and nothing would notice.
|
|
96
68
|
/**
|
|
97
|
-
* @type {(box: Frame,
|
|
98
|
-
* Frame &
|
|
69
|
+
* @type {(box: Frame, render: Settings) =>
|
|
70
|
+
* Frame & { settings: Settings, y: number, fresh: boolean }}
|
|
99
71
|
*/
|
|
100
|
-
let blank = (box,
|
|
72
|
+
let blank = (box, render) => ({ ...box, settings: render, y: 0, fresh: true });
|
|
101
73
|
|
|
102
74
|
// Text decoration in the text colour. Thickness and offset come from the
|
|
103
75
|
// line's ascender (the face metric already measured for baseline placement).
|
|
@@ -125,9 +97,9 @@ let decorateLine = (stroke, line, left, baseline) => {
|
|
|
125
97
|
/**
|
|
126
98
|
* The adapter that records: a canvas whose marks become the display list.
|
|
127
99
|
*
|
|
128
|
-
* @type {(box: Frame,
|
|
100
|
+
* @type {(box: Frame, render: Settings) => Listing}
|
|
129
101
|
*/
|
|
130
|
-
let listing = (box,
|
|
102
|
+
let listing = (box, render) => {
|
|
131
103
|
// A page under construction: its `number`/`total` are the target's to add
|
|
132
104
|
// once the count is known.
|
|
133
105
|
/** @type {any} */
|
|
@@ -162,9 +134,12 @@ let listing = (box, fonts) => {
|
|
|
162
134
|
dash,
|
|
163
135
|
});
|
|
164
136
|
|
|
137
|
+
// `path` rides along for the one consumer that needs to name the item back
|
|
138
|
+
// to the author: a target whose embedder rejects the bytes has nothing else
|
|
139
|
+
// to say which image it was, the box beside the op being a different record.
|
|
165
140
|
/** @type {Canvas['picture']} */
|
|
166
|
-
let picture = (bytes, format, x, y, w, h) =>
|
|
167
|
-
page.ops.push({ kind: "image", bytes, format, x, y: down(y + h), w, h });
|
|
141
|
+
let picture = (path, bytes, format, x, y, w, h) =>
|
|
142
|
+
page.ops.push({ kind: "image", path, bytes, format, x, y: down(y + h), w, h });
|
|
168
143
|
|
|
169
144
|
// `advances` rides only where a painter can use it: a base-14 face's
|
|
170
145
|
// one-per-code-point shaping, which is what lets a screen stand-in be
|
|
@@ -226,7 +201,7 @@ let listing = (box, fonts) => {
|
|
|
226
201
|
|
|
227
202
|
/** @type {Listing} */
|
|
228
203
|
let canvas = {
|
|
229
|
-
...blank(box,
|
|
204
|
+
...blank(box, render),
|
|
230
205
|
get count() {
|
|
231
206
|
return pages.length;
|
|
232
207
|
},
|
|
@@ -253,10 +228,10 @@ let MARKS_NOTHING = () => {};
|
|
|
253
228
|
* The adapter that only measures: the same cursor arithmetic with every mark
|
|
254
229
|
* discarded. It holds no page, so a measurement cannot record.
|
|
255
230
|
*
|
|
256
|
-
* @type {(box: Frame,
|
|
231
|
+
* @type {(box: Frame, render: Settings) => Canvas}
|
|
257
232
|
*/
|
|
258
|
-
let measuring = (box,
|
|
259
|
-
...blank(box,
|
|
233
|
+
let measuring = (box, render) => ({
|
|
234
|
+
...blank(box, render),
|
|
260
235
|
count: 0,
|
|
261
236
|
newPage: () => {
|
|
262
237
|
throw new Error("probe reached newPage: the measuring path must not paginate");
|
|
@@ -272,10 +247,10 @@ let measuring = (box, fonts) => ({
|
|
|
272
247
|
* Take on a content box someone else worked out — how much page furniture
|
|
273
248
|
* needs is layout's policy (`reserve` there), while which of a frame's members
|
|
274
249
|
* may move at all is this module's invariant. `top` and `bottom` are the two,
|
|
275
|
-
* and the only two read here: the page box
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
250
|
+
* and the only two read here: the page box is fixed for the whole document, so
|
|
251
|
+
* the rest of the frame that arrives is this canvas's own and is left alone.
|
|
252
|
+
* Layout reads the bounds live, so every page the body then flows through sees
|
|
253
|
+
* the narrowed box.
|
|
279
254
|
*
|
|
280
255
|
* Legal only while the render has not committed to a page — the band flow
|
|
281
256
|
* adopts from its `report-start` handler, with its own first page open but
|
|
@@ -332,4 +307,4 @@ let stamp = (canvas, text) => {
|
|
|
332
307
|
};
|
|
333
308
|
};
|
|
334
309
|
|
|
335
|
-
export { adopt, decorateLine,
|
|
310
|
+
export { adopt, decorateLine, listing, measuring, stamp };
|
package/lib/fonts.js
CHANGED
|
@@ -20,25 +20,16 @@ import { Encodings, Font, FontNames } from "@pdf-lib/standard-fonts";
|
|
|
20
20
|
// The three families the vocabulary names, four variants each, indexed by
|
|
21
21
|
// `bold + 2 * italic` — the same ordering the schema's `bold`/`italic` flags
|
|
22
22
|
// produce. Unknown family names fall back to sans, as SCHEMA.md specifies.
|
|
23
|
+
// A family's four names are one stem plus the weight and slant suffixes that
|
|
24
|
+
// family spells, so each family names its two and the four are walked out of
|
|
25
|
+
// them rather than written twelve times over.
|
|
26
|
+
/** @type {(stem: string, slant: string) => string[]} */
|
|
27
|
+
let variants = (stem, slant) =>
|
|
28
|
+
["", "Bold", slant, "Bold" + slant].map((v) => /** @type {any} */ (FontNames)[stem + v]);
|
|
23
29
|
let BASE = {
|
|
24
|
-
sans:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
FontNames.HelveticaOblique,
|
|
28
|
-
FontNames.HelveticaBoldOblique,
|
|
29
|
-
],
|
|
30
|
-
serif: [
|
|
31
|
-
FontNames.TimesRoman,
|
|
32
|
-
FontNames.TimesRomanBold,
|
|
33
|
-
FontNames.TimesRomanItalic,
|
|
34
|
-
FontNames.TimesRomanBoldItalic,
|
|
35
|
-
],
|
|
36
|
-
mono: [
|
|
37
|
-
FontNames.Courier,
|
|
38
|
-
FontNames.CourierBold,
|
|
39
|
-
FontNames.CourierOblique,
|
|
40
|
-
FontNames.CourierBoldOblique,
|
|
41
|
-
],
|
|
30
|
+
sans: variants("Helvetica", "Oblique"),
|
|
31
|
+
serif: variants("TimesRoman", "Italic"),
|
|
32
|
+
mono: variants("Courier", "Oblique"),
|
|
42
33
|
};
|
|
43
34
|
|
|
44
35
|
/**
|
|
@@ -69,9 +60,6 @@ let BASE = {
|
|
|
69
60
|
*/
|
|
70
61
|
/** @typedef {{ families: Record<string, Face[]> }} Fonts */
|
|
71
62
|
|
|
72
|
-
/** @type {(text: string) => string[]} */
|
|
73
|
-
let codePoints = (text) => Array.from(text);
|
|
74
|
-
|
|
75
63
|
// A base-14 face: AFM widths and kerning over WinAnsi. The width of a string
|
|
76
64
|
// is the sum of its glyph widths plus the kerning between neighbours — the
|
|
77
65
|
// same arithmetic pdf-lib's standard-font embedder does, kept in step so the
|
|
@@ -88,7 +76,7 @@ let standard = (family, variant, name) => {
|
|
|
88
76
|
let encoding = Encodings.WinAnsi;
|
|
89
77
|
/** @type {(text: string) => { code: number, name: string }[]} */
|
|
90
78
|
let glyphs = (text) =>
|
|
91
|
-
|
|
79
|
+
Array.from(text).map((char) => encoding.encodeUnicodeCodePoint(char.codePointAt(0) ?? 0));
|
|
92
80
|
/** @type {(glyph: { name: string }, next?: { name: string }) => number} */
|
|
93
81
|
let advance = (glyph, next) =>
|
|
94
82
|
(font.getWidthOfGlyph(glyph.name) || 250) +
|
|
@@ -153,10 +141,6 @@ let baseFamilies = () =>
|
|
|
153
141
|
]),
|
|
154
142
|
));
|
|
155
143
|
|
|
156
|
-
// A render's own copy, so adding a host's families cannot reach the memo.
|
|
157
|
-
/** @type {() => Record<string, Face[]>} */
|
|
158
|
-
let loadBase = () => ({ ...baseFamilies() });
|
|
159
|
-
|
|
160
144
|
/**
|
|
161
145
|
* The base-14 sans regular, whatever a host mapped `sans` to. Reached only by
|
|
162
146
|
* the marking, which is the one thing on a page a host's font mapping must not
|
|
@@ -171,9 +155,9 @@ export let baseSans = () => baseFamilies().sans[0];
|
|
|
171
155
|
/** @type {() => Promise<any>} */
|
|
172
156
|
let useFontkit = async () => {
|
|
173
157
|
try {
|
|
174
|
-
return
|
|
158
|
+
return await import("fontkit");
|
|
175
159
|
} catch {
|
|
176
|
-
throw Error("options.fonts: install
|
|
160
|
+
throw Error("options.fonts: install fontkit to measure TrueType families");
|
|
177
161
|
}
|
|
178
162
|
};
|
|
179
163
|
|
|
@@ -287,7 +271,8 @@ let loadCustom = async (custom, families) => {
|
|
|
287
271
|
* @returns {Promise<Fonts>}
|
|
288
272
|
*/
|
|
289
273
|
export async function loadFonts(custom) {
|
|
290
|
-
|
|
274
|
+
// A render's own copy, so adding a host's families cannot reach the memo.
|
|
275
|
+
let families = { ...baseFamilies() };
|
|
291
276
|
await loadCustom(custom, families);
|
|
292
277
|
return { families };
|
|
293
278
|
}
|
|
@@ -397,7 +382,7 @@ let replaceUndrawable = (set, text) => {
|
|
|
397
382
|
let sub = set.has(0x3f) ? "?" : "";
|
|
398
383
|
// Encodedness is a per-code-point question, so that is the unit to ask it
|
|
399
384
|
// in — the grapheme clusters the rule protects are undrawable anyway.
|
|
400
|
-
return
|
|
385
|
+
return Array.from(text)
|
|
401
386
|
.map((char) => (set.has(char.codePointAt(0) ?? -1) ? char : sub))
|
|
402
387
|
.join("");
|
|
403
388
|
};
|
|
@@ -405,5 +390,5 @@ let replaceUndrawable = (set, text) => {
|
|
|
405
390
|
/** @type {(font: any, text: string) => boolean} */
|
|
406
391
|
let encodable = (font, text) => {
|
|
407
392
|
let set = charsetOf(font);
|
|
408
|
-
return
|
|
393
|
+
return Array.from(text).every((char) => set.has(char.codePointAt(0) ?? -1));
|
|
409
394
|
};
|
package/lib/image.js
CHANGED
|
@@ -53,6 +53,6 @@ export let intrinsic = (bytes, format) => {
|
|
|
53
53
|
// The engine vouched for the magic numbers, not for the rest of the file:
|
|
54
54
|
// a truncated header reaches here as a zero, and failing loudly beats
|
|
55
55
|
// drawing an image with no size (SCHEMA.md, "Image item").
|
|
56
|
-
if (!(w > 0 && h > 0)) throw Error("
|
|
56
|
+
if (!(w > 0 && h > 0)) throw Error("could not read the image's size from its bytes");
|
|
57
57
|
return { w: w * PER_PX, h: h * PER_PX };
|
|
58
58
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Target } from "quario";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Page geometry, in PostScript points. Host configuration, except that a
|
|
5
|
+
* document may declare `page.margin` in the host's stead — never both.
|
|
6
|
+
*/
|
|
4
7
|
export interface LayoutPage {
|
|
5
8
|
/** Named size or `[width, height]` in points. Default `'A4'`. */
|
|
6
9
|
size?: "A4" | "letter" | [number, number];
|
|
@@ -26,7 +29,7 @@ export interface LayoutOptions {
|
|
|
26
29
|
* TrueType families to measure against, selected from styles by
|
|
27
30
|
* `family: '<name>'`. Pass the same record to `pdf({ fonts })` and to
|
|
28
31
|
* `paint()`, so preview and document break their lines in the same places
|
|
29
|
-
* and draw in the same faces. Requires the optional peer
|
|
32
|
+
* and draw in the same faces. Requires the optional peer `fontkit`;
|
|
30
33
|
* the base-14 families need nothing extra.
|
|
31
34
|
*
|
|
32
35
|
* Hold the record and the buffers in it across renders rather than building
|
|
@@ -81,9 +84,14 @@ export interface LineOp {
|
|
|
81
84
|
dash: number[] | null;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* An image, placed. `bytes` is the array the source expression yielded, and
|
|
89
|
+
* `path` names the schema node it came from — the one thing a target whose
|
|
90
|
+
* embedder rejects those bytes has to report the failure with.
|
|
91
|
+
*/
|
|
85
92
|
export interface ImageOp {
|
|
86
93
|
kind: "image";
|
|
94
|
+
path: string | undefined;
|
|
87
95
|
bytes: Uint8Array;
|
|
88
96
|
format: "png" | "jpeg";
|
|
89
97
|
x: number;
|
|
@@ -197,6 +205,12 @@ export const PX_PER_POINT: number;
|
|
|
197
205
|
* Paint one page onto a Canvas 2D context: a white page, then every op in
|
|
198
206
|
* order. `scale` is device pixels per point. `fonts` is the same record
|
|
199
207
|
* given to `layout()`, so a TrueType family draws in its own face.
|
|
208
|
+
*
|
|
209
|
+
* It awaits its faces and images before it draws, and it draws whatever
|
|
210
|
+
* happened in between: a canvas re-sized under a call still in flight is
|
|
211
|
+
* filled at that call's own `scale`, not the size it now has. A caller that
|
|
212
|
+
* repaints one canvas at changing scales owns that, by not letting a
|
|
213
|
+
* superseded call reach a canvas still on screen.
|
|
200
214
|
*/
|
|
201
215
|
export function paint(
|
|
202
216
|
ctx: CanvasRenderingContext2D,
|
package/lib/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { listing, stamp } from "./canvas.js";
|
|
|
25
25
|
import { checkFonts, loadFonts } from "./fonts.js";
|
|
26
26
|
import { flow, furniture } from "./layout.js";
|
|
27
27
|
import { geometry } from "./page.js";
|
|
28
|
+
import { settings } from "./settings.js";
|
|
28
29
|
|
|
29
30
|
export { PX_PER_POINT, hit, paint } from "./paint.js";
|
|
30
31
|
export { pageBox } from "./page.js";
|
|
@@ -78,8 +79,13 @@ export function layout(options) {
|
|
|
78
79
|
let fonts = await loadFonts(custom);
|
|
79
80
|
let gen = stream(data);
|
|
80
81
|
let first = gen.next();
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
// The opening event carries both halves this render is built from: the page
|
|
83
|
+
// frame and the document-wide settings. Peeked once here — the settings are
|
|
84
|
+
// complete and immutable by construction, so no later event can settle them
|
|
85
|
+
// a second time.
|
|
86
|
+
let opening = first.done ? null : first.value;
|
|
87
|
+
let geo = geometry(options?.page, opening);
|
|
88
|
+
let canvas = listing(geo, settings(fonts, opening));
|
|
83
89
|
// The band flow owns the placement state and every handler over it, and
|
|
84
90
|
// opens the first page as it is built; this file only hands it the stream.
|
|
85
91
|
let { handlers, finish } = flow(canvas);
|
|
@@ -88,21 +94,21 @@ export function layout(options) {
|
|
|
88
94
|
yield* gen;
|
|
89
95
|
}
|
|
90
96
|
await walk(events(), handlers);
|
|
91
|
-
// The flow
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
let { marks,
|
|
97
|
-
|
|
97
|
+
// The flow reserved the page bands off the opening event on its way past;
|
|
98
|
+
// the passes below want the rest of it — the band closures to render per
|
|
99
|
+
// page, and the marking's wording to stamp. It is the same event peeked
|
|
100
|
+
// above, so this file reads it straight rather than through `finish`.
|
|
101
|
+
// Empty when nothing was peeked, which reads as a document owing neither.
|
|
102
|
+
let { marks, pages } = finish();
|
|
103
|
+
let doc = opening || {};
|
|
98
104
|
// The passes below run over the finished pages, not the stream — no walk
|
|
99
105
|
// at all — so they open each page themselves and breathe on their own
|
|
100
106
|
// rather than through the driver.
|
|
101
|
-
await furnish(canvas,
|
|
107
|
+
await furnish(canvas, doc.page, pages);
|
|
102
108
|
// The unlicensed marking goes on last, over content and page furniture
|
|
103
109
|
// alike, once per page (LICENSE section 6). Its wording rode in on
|
|
104
110
|
// `report-start`; only the placement is this layout's.
|
|
105
|
-
await markPages(canvas,
|
|
111
|
+
await markPages(canvas, doc.marking);
|
|
106
112
|
return /** @type {Layout} */ ({
|
|
107
113
|
width: geo.width,
|
|
108
114
|
height: geo.height,
|