@quario/layout 0.1.0 → 0.2.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 +50 -0
- package/README.md +1 -1
- package/lib/box.js +5 -1
- package/lib/canvas.js +105 -49
- package/lib/fonts.js +16 -31
- package/lib/index.d.ts +7 -1
- package/lib/index.js +2 -2
- package/lib/layout.js +297 -220
- package/lib/page.js +1 -7
- package/lib/paint.js +9 -4
- package/lib/style.js +25 -2
- package/lib/text.js +20 -26
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-09-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A spanning cell is drawn as one box** across the columns it covers,
|
|
15
|
+
starting where the first of them starts. It takes no part in allocating their
|
|
16
|
+
widths, and a column no cell votes on opens at the cell-padding floor, so an
|
|
17
|
+
empty table still shows its geometry. A spanning row too tall for the page it
|
|
18
|
+
is on slices like any other, each slice following the geometry of the page or
|
|
19
|
+
page-column strip it lands in.
|
|
20
|
+
|
|
21
|
+
- **`valign`** places a cell's lines, or a slot's lines or picture, in the slack
|
|
22
|
+
its row or split leaves over its own content: middle halves it, bottom takes
|
|
23
|
+
it. The box does not move, and a picture is not scaled. A row too tall for
|
|
24
|
+
any page, sliced across pages, paints from the top.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **A row's box is drawn by the row's cells.** A box declared on a table row
|
|
29
|
+
used to be one rect across the summed column widths; it is now each covered
|
|
30
|
+
cell's own, so a row's `borderBottom` still reads as one continuous edge
|
|
31
|
+
while a row's `borderLeft` becomes an edge on every cell rather than one at
|
|
32
|
+
the row's outer left. A row's border also occupies height now, as a cell's
|
|
33
|
+
always has, so a bordered row is taller by its border's width.
|
|
34
|
+
|
|
35
|
+
- **Measuring TrueType families now needs `fontkit`, not `@pdf-lib/fontkit`.**
|
|
36
|
+
Install `fontkit` instead; nothing else about `options.fonts` changes. The
|
|
37
|
+
old package's bundle crashed with a bare `ReferenceError` on any OpenType
|
|
38
|
+
face needing a shaping state machine — which is every Devanagari, Bengali,
|
|
39
|
+
Tamil, Khmer or Myanmar webfont in practice, and some fifty further scripts
|
|
40
|
+
besides. Those faces now measure where they used to throw. Which scripts a
|
|
41
|
+
face supports remains the font's and the parser's to answer, not this
|
|
42
|
+
package's.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **A group header no longer strands above a split.** A split cannot be broken
|
|
47
|
+
across a page, so a header that introduces one has to keep the whole of it
|
|
48
|
+
company — but the keep-together test measured a split by its first two lines
|
|
49
|
+
of text, and a split holds its text in its slots, not in itself. It therefore
|
|
50
|
+
measured as nothing: the header was drawn at the foot of the page with room
|
|
51
|
+
reserved for none of what followed, the split moved on to the next page, and
|
|
52
|
+
the instance's header repeated above it there. A record card whose total row
|
|
53
|
+
is a split was drawn twice — once orphaned at a page bottom, once whole. The
|
|
54
|
+
header now moves with the split, exactly as it already moved with an image.
|
|
55
|
+
|
|
56
|
+
Where a split is taller than the page has left, the header behaves as it
|
|
57
|
+
always has before content no page can hold: it degrades to plain paginated
|
|
58
|
+
flow and repeats nothing.
|
|
59
|
+
|
|
10
60
|
## [0.1.0] - 2026-09-03
|
|
11
61
|
|
|
12
62
|
### 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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* moves the cursor and marks nothing, which is how `probe` reserves a band's
|
|
8
8
|
* height without emitting it. Measuring is therefore a choice of adapter
|
|
9
9
|
* rather than a mode every primitive has to remember to check — and since a
|
|
10
|
-
* measuring canvas is built from a frame and
|
|
10
|
+
* measuring canvas is built from a frame and the render's settings, there is no
|
|
11
11
|
* parameter through which a list could reach it. Pagination is the one thing
|
|
12
12
|
* measuring must never do: a page turn would reset the cursor mid-measure and
|
|
13
13
|
* return a silently wrong height, so `newPage` throws there.
|
|
@@ -18,57 +18,77 @@
|
|
|
18
18
|
* it lands: a list coordinate is measured from the page's top-left corner,
|
|
19
19
|
* `y` descending, the way a screen reads. Points throughout.
|
|
20
20
|
*/
|
|
21
|
-
import { baseSans } from "./fonts.js";
|
|
22
|
-
import { BLACK, dressed, shift } from "./style.js";
|
|
21
|
+
import { baseSans, familyName } from "./fonts.js";
|
|
22
|
+
import { BLACK, dressed, shift, sizeOf } from "./style.js";
|
|
23
23
|
|
|
24
24
|
/** @typedef {import('./text.js').Line} Line */
|
|
25
|
-
/** @typedef {import('./text.js').Metrics} Metrics */
|
|
26
25
|
/** @typedef {import('./style.js').Color} Color */
|
|
27
26
|
// The list's own shapes — `Op`, `Box`, `Page` — are described once, in the
|
|
28
27
|
// hand-written public declarations, and read back here.
|
|
29
28
|
/** @import { Box, Op, Page } from './index.d.ts' */
|
|
30
29
|
|
|
31
|
-
// The page box and the content box
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
30
|
+
// The page box and the content box, and nothing whatever else: geometry, all
|
|
31
|
+
// of it derived below from a page and a margin. Fixed for the document — the
|
|
32
|
+
// one exception is `top`/`bottom`, which the page bands narrow once through
|
|
33
|
+
// `adopt` below, while the first page is still untouched. Everything else that
|
|
34
|
+
// holds for a whole render is `Settings`, next door; a frame carrying either
|
|
35
|
+
// half of that was a type with two lifetimes, and the copy that kept a probe
|
|
36
|
+
// reading the same locale as the draw had to be written out by hand.
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
38
|
+
* @typedef {{ width: number, height: number, margin: number,
|
|
39
|
+
* content: number, top: number, bottom: number }} Frame
|
|
48
40
|
*/
|
|
49
41
|
|
|
50
|
-
// A frame from the page box
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
* family?: string | null) => Frame}
|
|
56
|
-
*/
|
|
57
|
-
let frame = (width, height, margin, base, family = null) => ({
|
|
42
|
+
// A frame from the page box: how `content`/`top`/`bottom` fall out of a page
|
|
43
|
+
// and a margin is derived here, once, so no caller and no suite has to restate
|
|
44
|
+
// it and drift from what a real render uses.
|
|
45
|
+
/** @type {(width: number, height: number, margin: number) => Frame} */
|
|
46
|
+
let frame = (width, height, margin) => ({
|
|
58
47
|
width,
|
|
59
48
|
height,
|
|
60
49
|
margin,
|
|
61
|
-
base,
|
|
62
|
-
family,
|
|
63
50
|
content: width - 2 * margin,
|
|
64
51
|
top: height - margin,
|
|
65
52
|
bottom: margin,
|
|
66
53
|
});
|
|
67
54
|
|
|
55
|
+
// This layout's baseline type size. Not a host option: a document's type size
|
|
56
|
+
// is the document's own, so it is `style.size` on the report and the number
|
|
57
|
+
// here is only what text renders at when nothing declares one. The XLSX target
|
|
58
|
+
// carries the same 10 for the same reason (docs/adr/0014, docs/adr/0033).
|
|
59
|
+
let BASE = 10;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Everything that holds for a whole render and is not geometry: the faces to
|
|
63
|
+
* measure against, the report default narrowed to `family` and `size` (landing
|
|
64
|
+
* in `family` and `base` here), and the three intl facts a formatted value
|
|
65
|
+
* resolves in. Settled once, at
|
|
66
|
+
* `report-start`, by `adoptSettings` below, and only read thereafter.
|
|
67
|
+
*
|
|
68
|
+
* A canvas holds one of these by reference, never a copy, which is the whole
|
|
69
|
+
* reason it is an object. A measuring canvas built off the same settings reads
|
|
70
|
+
* exactly what the listing canvas reads, so a page band cannot be reserved
|
|
71
|
+
* against one locale and drawn in another — an agreement that used to rest on
|
|
72
|
+
* a hand-written copy staying in step.
|
|
73
|
+
*
|
|
74
|
+
* `family` is null when the report declares none, and the intl three are
|
|
75
|
+
* absent when the engine settled none.
|
|
76
|
+
*
|
|
77
|
+
* @typedef {{ fonts: import('./fonts.js').Fonts, base: number,
|
|
78
|
+
* family: string | null, locale?: string, currency?: string,
|
|
79
|
+
* timeZone?: string }} Settings
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
// What a render starts from: the loaded faces, and this target's own baseline
|
|
83
|
+
// standing in for a default no report has declared yet.
|
|
84
|
+
/** @type {(fonts: import('./fonts.js').Fonts) => Settings} */
|
|
85
|
+
let settings = (fonts) => ({ fonts, base: BASE, family: null });
|
|
86
|
+
|
|
68
87
|
// `y` is the cursor on the open page and `fresh` says nothing has been drawn on
|
|
69
88
|
// it yet, which is what makes a break legal. `count` is how many pages exist.
|
|
70
89
|
/**
|
|
71
|
-
* @typedef {Frame &
|
|
90
|
+
* @typedef {Frame & { settings: Settings, y: number, fresh: boolean,
|
|
91
|
+
* count: number,
|
|
72
92
|
* newPage: () => void,
|
|
73
93
|
* rect: (color: Color, x: number, y: number, w: number, h: number) => void,
|
|
74
94
|
* stroke: (x1: number, y1: number, x2: number, y2: number, thickness: number,
|
|
@@ -89,15 +109,15 @@ let frame = (width, height, margin, base, family = null) => ({
|
|
|
89
109
|
* watermark: (mark: Mark) => void }} Listing
|
|
90
110
|
*/
|
|
91
111
|
|
|
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.
|
|
112
|
+
// What every canvas starts as, whichever adapter it is: its frame and the
|
|
113
|
+
// render's settings, and a cursor that has drawn nothing yet. Shared because
|
|
114
|
+
// the parity test compares member names, not their values — two hand-written
|
|
115
|
+
// copies of this could drift in what they start from and nothing would notice.
|
|
96
116
|
/**
|
|
97
|
-
* @type {(box: Frame,
|
|
98
|
-
* Frame &
|
|
117
|
+
* @type {(box: Frame, render: Settings) =>
|
|
118
|
+
* Frame & { settings: Settings, y: number, fresh: boolean }}
|
|
99
119
|
*/
|
|
100
|
-
let blank = (box,
|
|
120
|
+
let blank = (box, render) => ({ ...box, settings: render, y: 0, fresh: true });
|
|
101
121
|
|
|
102
122
|
// Text decoration in the text colour. Thickness and offset come from the
|
|
103
123
|
// line's ascender (the face metric already measured for baseline placement).
|
|
@@ -125,9 +145,9 @@ let decorateLine = (stroke, line, left, baseline) => {
|
|
|
125
145
|
/**
|
|
126
146
|
* The adapter that records: a canvas whose marks become the display list.
|
|
127
147
|
*
|
|
128
|
-
* @type {(box: Frame,
|
|
148
|
+
* @type {(box: Frame, render: Settings) => Listing}
|
|
129
149
|
*/
|
|
130
|
-
let listing = (box,
|
|
150
|
+
let listing = (box, render) => {
|
|
131
151
|
// A page under construction: its `number`/`total` are the target's to add
|
|
132
152
|
// once the count is known.
|
|
133
153
|
/** @type {any} */
|
|
@@ -226,7 +246,7 @@ let listing = (box, fonts) => {
|
|
|
226
246
|
|
|
227
247
|
/** @type {Listing} */
|
|
228
248
|
let canvas = {
|
|
229
|
-
...blank(box,
|
|
249
|
+
...blank(box, render),
|
|
230
250
|
get count() {
|
|
231
251
|
return pages.length;
|
|
232
252
|
},
|
|
@@ -253,10 +273,10 @@ let MARKS_NOTHING = () => {};
|
|
|
253
273
|
* The adapter that only measures: the same cursor arithmetic with every mark
|
|
254
274
|
* discarded. It holds no page, so a measurement cannot record.
|
|
255
275
|
*
|
|
256
|
-
* @type {(box: Frame,
|
|
276
|
+
* @type {(box: Frame, render: Settings) => Canvas}
|
|
257
277
|
*/
|
|
258
|
-
let measuring = (box,
|
|
259
|
-
...blank(box,
|
|
278
|
+
let measuring = (box, render) => ({
|
|
279
|
+
...blank(box, render),
|
|
260
280
|
count: 0,
|
|
261
281
|
newPage: () => {
|
|
262
282
|
throw new Error("probe reached newPage: the measuring path must not paginate");
|
|
@@ -272,10 +292,10 @@ let measuring = (box, fonts) => ({
|
|
|
272
292
|
* Take on a content box someone else worked out — how much page furniture
|
|
273
293
|
* needs is layout's policy (`reserve` there), while which of a frame's members
|
|
274
294
|
* 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
|
-
*
|
|
295
|
+
* and the only two read here: the page box is fixed for the whole document, so
|
|
296
|
+
* the rest of the frame that arrives is this canvas's own and is left alone.
|
|
297
|
+
* Layout reads the bounds live, so every page the body then flows through sees
|
|
298
|
+
* the narrowed box.
|
|
279
299
|
*
|
|
280
300
|
* Legal only while the render has not committed to a page — the band flow
|
|
281
301
|
* adopts from its `report-start` handler, with its own first page open but
|
|
@@ -294,6 +314,42 @@ let adopt = (canvas, box) => {
|
|
|
294
314
|
canvas.y = canvas.top;
|
|
295
315
|
};
|
|
296
316
|
|
|
317
|
+
/**
|
|
318
|
+
* The document-wide facts off `report-start`, taken once, before anything is
|
|
319
|
+
* measured. The report default is narrowed to `family` and `size`, and each
|
|
320
|
+
* replaces this target's own baseline outright: row heights and band gaps scale
|
|
321
|
+
* with the document's type rather than staying at a size nothing is set in, and
|
|
322
|
+
* text declaring no family is set in the document's. Settling the pair here is
|
|
323
|
+
* the whole of this target's reading of docs/adr/0033 — the default reaches a
|
|
324
|
+
* node as a fallback the settings carry, never as a layer merged into its
|
|
325
|
+
* style, so a document-wide fact costs no allocation however many cells a
|
|
326
|
+
* report has.
|
|
327
|
+
*
|
|
328
|
+
* `sizeOf` and `familyName` are this target's one reading each of what a
|
|
329
|
+
* declared size and family amount to, so they read the default here too — both
|
|
330
|
+
* reach this unchecked from a computed style, and two spellings of that
|
|
331
|
+
* leniency would drift. Each falls back to what the settings already carry, so
|
|
332
|
+
* a default declaring one of the pair leaves the other alone, and one whose
|
|
333
|
+
* value is unusable leaves this target's own baseline standing.
|
|
334
|
+
*
|
|
335
|
+
* Settled once, the statement `adopt` makes about geometry: these hold for a
|
|
336
|
+
* whole render, so a second event setting them would mean two documents in one.
|
|
337
|
+
* The freeze is what enforces it, and the check is what names it.
|
|
338
|
+
*
|
|
339
|
+
* @param {Settings} render The render's settings.
|
|
340
|
+
* @param {any} event The `report-start` event.
|
|
341
|
+
*/
|
|
342
|
+
let adoptSettings = (render, event) => {
|
|
343
|
+
if (Object.isFrozen(render)) throw Error("adoptSettings: the render settings are fixed");
|
|
344
|
+
let style = event.style || {};
|
|
345
|
+
render.base = sizeOf(style, render.base);
|
|
346
|
+
render.family = familyName(style) || render.family;
|
|
347
|
+
render.locale = event.locale;
|
|
348
|
+
render.currency = event.currency;
|
|
349
|
+
render.timeZone = event.timeZone;
|
|
350
|
+
Object.freeze(render);
|
|
351
|
+
};
|
|
352
|
+
|
|
297
353
|
// The unlicensed-output marking (LICENSE section 6): one translucent line
|
|
298
354
|
// drawn corner-to-corner across the finished page — over the content, not
|
|
299
355
|
// under it, so no filled table header or background rectangle can cover it.
|
|
@@ -332,4 +388,4 @@ let stamp = (canvas, text) => {
|
|
|
332
388
|
};
|
|
333
389
|
};
|
|
334
390
|
|
|
335
|
-
export { adopt, decorateLine, frame, listing, measuring, stamp };
|
|
391
|
+
export { adopt, adoptSettings, decorateLine, frame, listing, measuring, settings, 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/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface LayoutOptions {
|
|
|
26
26
|
* TrueType families to measure against, selected from styles by
|
|
27
27
|
* `family: '<name>'`. Pass the same record to `pdf({ fonts })` and to
|
|
28
28
|
* `paint()`, so preview and document break their lines in the same places
|
|
29
|
-
* and draw in the same faces. Requires the optional peer
|
|
29
|
+
* and draw in the same faces. Requires the optional peer `fontkit`;
|
|
30
30
|
* the base-14 families need nothing extra.
|
|
31
31
|
*
|
|
32
32
|
* Hold the record and the buffers in it across renders rather than building
|
|
@@ -197,6 +197,12 @@ export const PX_PER_POINT: number;
|
|
|
197
197
|
* Paint one page onto a Canvas 2D context: a white page, then every op in
|
|
198
198
|
* order. `scale` is device pixels per point. `fonts` is the same record
|
|
199
199
|
* given to `layout()`, so a TrueType family draws in its own face.
|
|
200
|
+
*
|
|
201
|
+
* It awaits its faces and images before it draws, and it draws whatever
|
|
202
|
+
* happened in between: a canvas re-sized under a call still in flight is
|
|
203
|
+
* filled at that call's own `scale`, not the size it now has. A caller that
|
|
204
|
+
* repaints one canvas at changing scales owns that, by not letting a
|
|
205
|
+
* superseded call reach a canvas still on screen.
|
|
200
206
|
*/
|
|
201
207
|
export function paint(
|
|
202
208
|
ctx: CanvasRenderingContext2D,
|
package/lib/index.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* file adds none. The package publishes `lib/` verbatim.
|
|
22
22
|
*/
|
|
23
23
|
import { breathe, walk } from "quario";
|
|
24
|
-
import { listing, stamp } from "./canvas.js";
|
|
24
|
+
import { listing, settings, 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";
|
|
@@ -79,7 +79,7 @@ export function layout(options) {
|
|
|
79
79
|
let gen = stream(data);
|
|
80
80
|
let first = gen.next();
|
|
81
81
|
let geo = geometry(options?.page, first.done ? null : first.value);
|
|
82
|
-
let canvas = listing(geo, fonts);
|
|
82
|
+
let canvas = listing(geo, settings(fonts));
|
|
83
83
|
// The band flow owns the placement state and every handler over it, and
|
|
84
84
|
// opens the first page as it is built; this file only hands it the stream.
|
|
85
85
|
let { handlers, finish } = flow(canvas);
|