@quario/pdf 0.3.0 → 0.5.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 +80 -1
- package/README.md +5 -4
- package/lib/embed.js +102 -0
- package/lib/index.d.ts +6 -15
- package/lib/index.js +35 -153
- package/lib/outline.js +1 -1
- package/lib/painter.js +205 -0
- package/package.json +12 -9
- package/lib/balance.js +0 -90
- package/lib/box.js +0 -111
- package/lib/canvas.js +0 -389
- package/lib/fonts.js +0 -223
- package/lib/image.js +0 -58
- package/lib/layout.js +0 -1804
- package/lib/style.js +0 -97
- package/lib/text.js +0 -258
package/CHANGELOG.md
CHANGED
|
@@ -7,13 +7,92 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-09-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A spanning cell draws as one box** across the columns it covers, through
|
|
15
|
+
`@quario/layout`. It takes no part in measuring them, and a spanning row that
|
|
16
|
+
slices across pages or page-column strips follows the geometry of the strip
|
|
17
|
+
each slice lands in.
|
|
18
|
+
- **`valign`** on table cells and split slots, through `@quario/layout`: middle
|
|
19
|
+
and bottom place the content in the height its row or split leaves over it.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **A row's box is drawn by the row's cells**, through `@quario/layout`. A box
|
|
24
|
+
declared on a table row used to be one rect across the columns; each covered
|
|
25
|
+
cell now draws its own. A row's `borderBottom` still reads as one continuous
|
|
26
|
+
edge, a row's `borderLeft` becomes an edge on every cell rather than one at
|
|
27
|
+
the row's outer left, and a bordered row is taller by its border's width,
|
|
28
|
+
since a border occupies height as a cell's always has.
|
|
29
|
+
|
|
30
|
+
- **Embedding TrueType fonts now needs `fontkit`, not `@pdf-lib/fontkit`.**
|
|
31
|
+
Install `fontkit` instead; nothing else about `options.fonts` changes. The
|
|
32
|
+
old package's bundle crashed with a bare `ReferenceError` on any OpenType
|
|
33
|
+
face needing a shaping state machine — which is every Devanagari, Bengali,
|
|
34
|
+
Tamil, Khmer or Myanmar webfont in practice, and some fifty further scripts
|
|
35
|
+
besides. Those faces now measure, embed and draw. Which scripts a face
|
|
36
|
+
supports remains the font's and the parser's to answer, not this package's.
|
|
37
|
+
|
|
38
|
+
- **`pdf-lib` is now `@cantoo/pdf-lib`.** A maintained fork, and what
|
|
39
|
+
`fontkit`'s subsetting requires. Rendered documents are unchanged in what
|
|
40
|
+
they draw: every drawn string is identical and every filled area lands in
|
|
41
|
+
the same place, but the file's bytes differ, so a host comparing digests
|
|
42
|
+
against stored output will see them move once.
|
|
43
|
+
|
|
44
|
+
## [0.4.0] - 2026-09-03
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **A split slot's box now fills the split's height.** A slot's background and
|
|
49
|
+
border stopped at that slot's own content and now take the split's height,
|
|
50
|
+
the way a table cell's take their row's. See the `@quario/layout` changelog
|
|
51
|
+
for the rule and for what it costs a report that relied on the short box.
|
|
52
|
+
|
|
53
|
+
- **A date string under `format: "date"` now presents as a date.** The
|
|
54
|
+
drawn text comes from the engine's `format()` helper, which revives the two
|
|
55
|
+
read forms. See the `quario` changelog for the forms and the timezone rule.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- **The layout moved to `@quario/layout`; this target paints its list.**
|
|
60
|
+
Measurement, wrapping, pagination, page furniture, the marking's geometry
|
|
61
|
+
and the page-size table now live in the layout package, which this target
|
|
62
|
+
depends on and runs first; pdf-lib writes the resulting display list. **The
|
|
63
|
+
move itself changed no output**, byte for byte, for the same input; the fix
|
|
64
|
+
below is a separate change and does move some. The `page` and `fonts`
|
|
65
|
+
options are the layout's own, so the same object configures `pdf()`, the
|
|
66
|
+
viewer and the editor. The font mapping's shape is now refused at the
|
|
67
|
+
factory call rather than at render.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **A nested group's footer keeps the page columns.** Inside a page-column
|
|
72
|
+
region, the first footer of a group nested in it ended the region: the
|
|
73
|
+
strips collapsed and every band after it was laid out across the page, as
|
|
74
|
+
if `columns` were absent. Only the node that declared `columns` closes its
|
|
75
|
+
own region now — a nested group's header and footer are region content,
|
|
76
|
+
laid out in the strips.
|
|
77
|
+
|
|
78
|
+
- **A padded or bordered item or row that breaks across a page keeps its
|
|
79
|
+
box.** Content too tall for one page is drawn as slices, and a slice now
|
|
80
|
+
carries the box sides the break left it — the top on the first, the bottom
|
|
81
|
+
on the last. Previously an item that overflowed lost its box entirely, so
|
|
82
|
+
the same declaration drew a border on short data and none on long; a
|
|
83
|
+
bordered table cell drew all four sides on every slice, putting an edge
|
|
84
|
+
across the middle of its own text; and a row's own border vanished when
|
|
85
|
+
the row was too tall for a page. A slice's lines are also drawn inside
|
|
86
|
+
the horizontal padding they were wrapped for, rather than flush to the
|
|
87
|
+
column. **This changes output** for reports where such content breaks.
|
|
88
|
+
|
|
10
89
|
## [0.3.0] - 2026-09-02
|
|
11
90
|
|
|
12
91
|
### Added
|
|
13
92
|
|
|
14
93
|
- **`format` stringifies at the edge from the instance locale.** A PDF
|
|
15
94
|
without a host locale still uses `en-US` / UTC, so the bytes stay
|
|
16
|
-
reproducible
|
|
95
|
+
reproducible.
|
|
17
96
|
|
|
18
97
|
- **A height-declared report header pins from the page top.** Leftover sits
|
|
19
98
|
under the items; the next band starts at the pin. The half-line group gap
|
package/README.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
**The PDF render target for [quario](https://www.npmjs.com/package/quario).** Renders a report
|
|
4
4
|
definition straight to a paginated PDF. No headless browser, no print CSS, deterministic bytes.
|
|
5
5
|
|
|
6
|
-
quario does the typesetting
|
|
7
|
-
|
|
6
|
+
quario does the typesetting — pagination, keep-together, tables, fonts, the document outline —
|
|
7
|
+
through `@quario/layout`, the same layout the viewer paints on screen, so the preview and the
|
|
8
|
+
document break their pages in the same places. [`@cantoo/pdf-lib`](https://github.com/Cantoo-Scribe/pdf-lib) writes the file. You get typesetting plus pdf-lib: no Chromium
|
|
8
9
|
in your container, no page-load race, no fonts-not-ready flake.
|
|
9
10
|
|
|
10
11
|
## Install
|
|
@@ -13,13 +14,13 @@ in your container, no page-load race, no fonts-not-ready flake.
|
|
|
13
14
|
npm install quario @quario/pdf
|
|
14
15
|
```
|
|
15
16
|
|
|
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
|
+
`@cantoo/pdf-lib` comes along as a dependency; the engine is a peer, installed beside it. ESM-only, Node 22+, and browser-ready through
|
|
17
18
|
any standards-based ESM bundler. The renderer returns bytes, so the host decides where they go.
|
|
18
19
|
|
|
19
20
|
Embedding TrueType fonts also needs the optional peer:
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
|
-
npm install
|
|
23
|
+
npm install fontkit
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
It loads only when you pass `fonts` to the factory. The base-14 families need nothing extra.
|
package/lib/embed.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The faces a document can draw with, embedded before anything is painted.
|
|
3
|
+
*
|
|
4
|
+
* Every face is embedded on the document before the list is painted, so
|
|
5
|
+
* resolving a list op's face to a pdf-lib font is synchronous — pdf-lib's
|
|
6
|
+
* `embedFont` is async, and the paint is not. Embedding the twelve base-14
|
|
7
|
+
* faces up front costs under 200 bytes in the output, which is cheaper than a
|
|
8
|
+
* two-pass split to embed only what is used.
|
|
9
|
+
*
|
|
10
|
+
* The registry mirrors the layout's: the same family names, the same four
|
|
11
|
+
* variants indexed by `bold + 2 * italic`, a missing variant falling back to
|
|
12
|
+
* the family's regular — so a `FaceRef` on the list names one font here.
|
|
13
|
+
*/
|
|
14
|
+
import { StandardFonts } from "@cantoo/pdf-lib";
|
|
15
|
+
|
|
16
|
+
// A family's four names are one stem plus the weight and slant suffixes that
|
|
17
|
+
// family spells, so each family names its two and the four are walked out of
|
|
18
|
+
// them rather than written twelve times over.
|
|
19
|
+
/** @type {(stem: string, slant: string) => string[]} */
|
|
20
|
+
let variants = (stem, slant) =>
|
|
21
|
+
["", "Bold", slant, "Bold" + slant].map((v) => /** @type {any} */ (StandardFonts)[stem + v]);
|
|
22
|
+
let BASE = {
|
|
23
|
+
sans: variants("Helvetica", "Oblique"),
|
|
24
|
+
serif: variants("TimesRoman", "Italic"),
|
|
25
|
+
mono: variants("Courier", "Oblique"),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** @typedef {{ families: Record<string, any[]> }} Fonts */
|
|
29
|
+
|
|
30
|
+
// Host-supplied TrueType families need a font parser, which is an optional
|
|
31
|
+
// peer: base-14 reports install nothing extra. Loaded only when `fonts` is
|
|
32
|
+
// passed, and a missing package is reported as what it is.
|
|
33
|
+
/** @type {(doc: any) => Promise<void>} */
|
|
34
|
+
let useFontkit = async (doc) => {
|
|
35
|
+
let fontkit;
|
|
36
|
+
try {
|
|
37
|
+
fontkit = await import("fontkit");
|
|
38
|
+
} catch {
|
|
39
|
+
throw Error("options.fonts: install fontkit to embed TrueType families");
|
|
40
|
+
}
|
|
41
|
+
doc.registerFontkit(fontkit);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** @type {(doc: any) => Promise<Record<string, any[]>>} */
|
|
45
|
+
let embedBase = async (doc) => {
|
|
46
|
+
/** @type {Record<string, any[]>} */
|
|
47
|
+
let families = {};
|
|
48
|
+
for (let [name, variants] of Object.entries(BASE))
|
|
49
|
+
families[name] = await Promise.all(variants.map((v) => doc.embedFont(v)));
|
|
50
|
+
return families;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** @type {(doc: any, src: any, path: string, variant: string) => Promise<any>} */
|
|
54
|
+
let embedOne = async (doc, src, path, variant) => {
|
|
55
|
+
try {
|
|
56
|
+
return await doc.embedFont(src[variant], { subset: true });
|
|
57
|
+
} catch (e) {
|
|
58
|
+
throw Error(path + "." + variant + ": " + /** @type {Error} */ (e).message);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Missing variants fall back to the family's regular, so a face is always
|
|
63
|
+
// resolvable once the family is named. The shape was checked at the layout's
|
|
64
|
+
// factory call; this only embeds.
|
|
65
|
+
/** @type {(doc: any, name: string, def: any) => Promise<any[]>} */
|
|
66
|
+
let familyOf = async (doc, name, def) => {
|
|
67
|
+
let path = "options.fonts." + name;
|
|
68
|
+
/** @type {any[]} */
|
|
69
|
+
let faces = [];
|
|
70
|
+
for (let variant of ["regular", "bold", "italic", "boldItalic"])
|
|
71
|
+
faces.push(def[variant] == null ? faces[0] : await embedOne(doc, def, path, variant));
|
|
72
|
+
return faces;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** @type {(doc: any, custom: any, families: Record<string, any[]>) => Promise<void>} */
|
|
76
|
+
let embedCustom = async (doc, custom, families) => {
|
|
77
|
+
if (!(custom && Object.keys(custom).length)) return;
|
|
78
|
+
await useFontkit(doc);
|
|
79
|
+
for (let [name, def] of Object.entries(custom))
|
|
80
|
+
families[name.toLowerCase()] = await familyOf(doc, name, def);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Embed every face the list can name and return the registry the painter
|
|
85
|
+
* reads.
|
|
86
|
+
*
|
|
87
|
+
* @param {any} doc The pdf-lib document.
|
|
88
|
+
* @param {any} custom `options.fonts`, or null.
|
|
89
|
+
* @returns {Promise<Fonts>}
|
|
90
|
+
*/
|
|
91
|
+
export async function embedFonts(doc, custom) {
|
|
92
|
+
let families = await embedBase(doc);
|
|
93
|
+
await embedCustom(doc, custom, families);
|
|
94
|
+
return { families };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The embedded font one list op names.
|
|
99
|
+
*
|
|
100
|
+
* @type {(fonts: Fonts, ref: { family: string, variant: number }) => any}
|
|
101
|
+
*/
|
|
102
|
+
export let fontFor = (fonts, ref) => fonts.families[ref.family][ref.variant];
|
package/lib/index.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
+
import type { LayoutFontFamily, LayoutPage } from "@quario/layout";
|
|
1
2
|
import type { Target } from "quario";
|
|
2
3
|
|
|
3
|
-
/** Page geometry, in PostScript points
|
|
4
|
-
export
|
|
5
|
-
/** Named size or `[width, height]` in points. Default `'A4'`. */
|
|
6
|
-
size?: "A4" | "letter" | [number, number];
|
|
7
|
-
/** Margin on all four sides, in points. Default `54` (0.75 in). */
|
|
8
|
-
margin?: number;
|
|
9
|
-
}
|
|
4
|
+
/** Page geometry, in PostScript points: the layout's own, never schema. */
|
|
5
|
+
export type PdfPage = LayoutPage;
|
|
10
6
|
|
|
11
7
|
/** Optional document information; never includes dates, so output stays deterministic. */
|
|
12
8
|
export interface PdfMeta {
|
|
@@ -15,13 +11,8 @@ export interface PdfMeta {
|
|
|
15
11
|
subject?: string;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
|
-
/** One embeddable TrueType family
|
|
19
|
-
export
|
|
20
|
-
regular: Uint8Array | ArrayBuffer;
|
|
21
|
-
bold?: Uint8Array | ArrayBuffer;
|
|
22
|
-
italic?: Uint8Array | ArrayBuffer;
|
|
23
|
-
boldItalic?: Uint8Array | ArrayBuffer;
|
|
24
|
-
}
|
|
14
|
+
/** One embeddable TrueType family: the layout's own, measured and embedded from the same bytes. */
|
|
15
|
+
export type PdfFontFamily = LayoutFontFamily;
|
|
25
16
|
|
|
26
17
|
/** Host controls, taken and validated at the factory call. */
|
|
27
18
|
export interface PdfOptions {
|
|
@@ -30,7 +21,7 @@ export interface PdfOptions {
|
|
|
30
21
|
/**
|
|
31
22
|
* TrueType families to embed (subset), selected from styles by
|
|
32
23
|
* `family: '<name>'`. Fonts are trusted host assets. Requires the optional
|
|
33
|
-
* peer
|
|
24
|
+
* peer `fontkit`; the base-14 families need nothing extra.
|
|
34
25
|
*/
|
|
35
26
|
fonts?: Record<string, PdfFontFamily>;
|
|
36
27
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,102 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @quario/pdf — the PDF render target: a pure consumer of the engine's public
|
|
3
|
-
* event stream, passed to a compiled report's `render
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* is deterministic: the document carries no dates of its own, so the
|
|
8
|
-
* input renders the same bytes. Page size is target configuration
|
|
3
|
+
* event stream, passed to a compiled report's `render`. The layout is
|
|
4
|
+
* `@quario/layout`'s — one paged display list, the same one the viewer paints
|
|
5
|
+
* on screen (docs/adr/0039) — and **pdf-lib writes the file**, so this target
|
|
6
|
+
* is a painter of that list rather than a typesetter or a PDF implementation.
|
|
7
|
+
* Output is deterministic: the document carries no dates of its own, so the
|
|
8
|
+
* same input renders the same bytes. Page size is target configuration
|
|
9
9
|
* (`pdf({ page.size })`); `page.margin` may also be a document field.
|
|
10
10
|
*
|
|
11
|
-
* This file is options,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* file adds none. The work lives in the sibling modules, which ship beside it
|
|
16
|
-
* — the package publishes `lib/` verbatim.
|
|
11
|
+
* This file is options, the layout pass, and the document: faces embedded,
|
|
12
|
+
* pages painted, the group tree attached as bookmarks. The work lives in the
|
|
13
|
+
* sibling modules, which ship beside it — the package publishes `lib/`
|
|
14
|
+
* verbatim.
|
|
17
15
|
*/
|
|
18
|
-
import { PDFDocument } from "pdf-lib";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { embedFonts } from "./fonts.js";
|
|
22
|
-
import { flow, furniture } from "./layout.js";
|
|
16
|
+
import { PDFDocument } from "@cantoo/pdf-lib";
|
|
17
|
+
import { layout } from "@quario/layout";
|
|
18
|
+
import { embedFonts } from "./embed.js";
|
|
23
19
|
import { outline } from "./outline.js";
|
|
20
|
+
import { paint } from "./painter.js";
|
|
24
21
|
|
|
25
22
|
// The options are described once, in the hand-written public declarations, and
|
|
26
23
|
// read back here — a second copy in JSDoc is a copy that drifts.
|
|
27
24
|
/** @import { PdfOptions } from './index.d.ts' */
|
|
28
25
|
|
|
29
|
-
let SIZES = /** @type {Record<string, [number, number]>} */ ({
|
|
30
|
-
A4: [595.28, 841.89],
|
|
31
|
-
letter: [612, 792],
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
/** @type {(msg: string) => never} */
|
|
35
|
-
let err = (msg) => {
|
|
36
|
-
throw Error(msg);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/** @type {(value: number) => boolean} */
|
|
40
|
-
let positive = (value) => Number.isFinite(value) && value > 0;
|
|
41
|
-
|
|
42
|
-
/** @type {(size: any) => any} */
|
|
43
|
-
let named = (size = "A4") => {
|
|
44
|
-
let dimensions = Array.isArray(size) ? size : SIZES[size];
|
|
45
|
-
// oxlint-disable-next-line no-unused-expressions
|
|
46
|
-
dimensions || err('options.page.size: unknown page size "' + size + '"');
|
|
47
|
-
return dimensions;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/** @type {(dimensions: any) => { width: number, height: number }} */
|
|
51
|
-
let pair = (dimensions) => {
|
|
52
|
-
let width = +dimensions[0],
|
|
53
|
-
height = +dimensions[1];
|
|
54
|
-
// oxlint-disable-next-line no-unused-expressions
|
|
55
|
-
(positive(width) && positive(height)) ||
|
|
56
|
-
err("options.page.size: expected finite positive dimensions");
|
|
57
|
-
return { width, height };
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** @type {(margin: any, width: number, height: number, path?: string) => number} */
|
|
61
|
-
let marginOf = (margin = 54, width, height, path = "options.page.margin") => {
|
|
62
|
-
let fits = Number.isFinite(margin) && margin >= 0 && 2 * margin < Math.min(width, height);
|
|
63
|
-
// oxlint-disable-next-line no-unused-expressions
|
|
64
|
-
fits || err(path + ": expected a non-negative number smaller than half the page");
|
|
65
|
-
return margin;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// This target's baseline type size. Not a host option: a document's type size
|
|
69
|
-
// is the document's own, so it is `style.size` on the report and the number
|
|
70
|
-
// here is only what text renders at when nothing declares one. The XLSX target
|
|
71
|
-
// carries the same 10 for the same reason (docs/adr/0014, docs/adr/0033).
|
|
72
|
-
let BASE = 10;
|
|
73
|
-
|
|
74
|
-
// The page box and the content box in one. Size is settled at the factory;
|
|
75
|
-
// margin may come from the document's `report-start`, so a render peeks that
|
|
76
|
-
// event before opening a canvas.
|
|
77
|
-
/** @type {(page: any) => any} */
|
|
78
|
-
let hostMargin = (page) => (Object.hasOwn(page, "margin") ? page.margin : undefined);
|
|
79
|
-
/** @type {(opening: any) => any} */
|
|
80
|
-
let docMargin = (opening) => opening?.margin;
|
|
81
|
-
/** @type {(doc: any, host: any) => boolean} */
|
|
82
|
-
let bothMargins = (doc, host) => doc != null && host !== undefined;
|
|
83
|
-
/** @type {(fromDoc: boolean) => string} */
|
|
84
|
-
let marginPath = (fromDoc) => (fromDoc ? "page.margin" : "options.page.margin");
|
|
85
|
-
/** @type {(opening: any, page: any) => { value: any, path: string }} */
|
|
86
|
-
let marginChoice = (opening, page) => {
|
|
87
|
-
let host = hostMargin(page);
|
|
88
|
-
let doc = docMargin(opening);
|
|
89
|
-
if (bothMargins(doc, host)) err("page.margin: document and target both declare a margin");
|
|
90
|
-
return { value: doc ?? host, path: marginPath(doc != null) };
|
|
91
|
-
};
|
|
92
|
-
/** @type {(options: any, opening?: any) => import('./canvas.js').Frame} */
|
|
93
|
-
let geometry = (options, opening) => {
|
|
94
|
-
let { page = {} } = options ?? {};
|
|
95
|
-
let { width, height } = pair(named(page.size));
|
|
96
|
-
let chosen = marginChoice(opening, page);
|
|
97
|
-
return frame(width, height, marginOf(chosen.value, width, height, chosen.path), BASE);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
26
|
// Optional document information. Never a date: pdf-lib stamps the current time
|
|
101
27
|
// unless told otherwise, and a timestamp would make the same input render
|
|
102
28
|
// different bytes on every run. Each key names the pdf-lib setter it feeds,
|
|
@@ -115,27 +41,11 @@ let describe = (doc, meta) => {
|
|
|
115
41
|
}
|
|
116
42
|
};
|
|
117
43
|
|
|
118
|
-
/** @type {(
|
|
119
|
-
let
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (i % 50 === 49) await breathe();
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
/** @type {(canvas: import('./canvas.js').Drawing, bands: any,
|
|
128
|
-
* pages: { number: number, total: number }[]) => Promise<void>} */
|
|
129
|
-
let furnish = async (canvas, bands, pages) => {
|
|
130
|
-
if (!bands) return;
|
|
131
|
-
await overPages(canvas, (i) => furniture(canvas, bands, pages[i]));
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
/** @type {(canvas: import('./canvas.js').Drawing, marking: any) => Promise<void>} */
|
|
135
|
-
let markPages = async (canvas, marking) => {
|
|
136
|
-
if (!marking) return;
|
|
137
|
-
let mark = stamp(canvas, marking);
|
|
138
|
-
await overPages(canvas, () => canvas.watermark(mark));
|
|
44
|
+
/** @type {(meta: any, custom: any) => Promise<{ doc: any, fonts: import('./embed.js').Fonts }>} */
|
|
45
|
+
let open = async (meta, custom) => {
|
|
46
|
+
let doc = await PDFDocument.create();
|
|
47
|
+
describe(doc, meta);
|
|
48
|
+
return { doc, fonts: await embedFonts(doc, custom) };
|
|
139
49
|
};
|
|
140
50
|
|
|
141
51
|
/**
|
|
@@ -143,57 +53,29 @@ let markPages = async (canvas, marking) => {
|
|
|
143
53
|
* `quario().report(schema).render(pdf({ page }), data)` resolves the
|
|
144
54
|
* complete document as a `Uint8Array` (the host writes the file). Rendering
|
|
145
55
|
* hands the loop back between event and page batches, so large reports stay
|
|
146
|
-
* cooperative. Geometry and
|
|
56
|
+
* cooperative. Geometry and the font mapping are validated here, at the
|
|
57
|
+
* factory call — by the layout target, whose options they are.
|
|
147
58
|
*
|
|
148
59
|
* @param {PdfOptions} [options] Host controls (see SCHEMA.md, "The PDF target").
|
|
149
60
|
* @returns {{name: "pdf", compile: (stream: any) => (data?: any) => Promise<Uint8Array>}}
|
|
150
61
|
* The target (see SCHEMA.md, "Instances and targets").
|
|
151
62
|
*/
|
|
152
63
|
export function pdf(options) {
|
|
153
|
-
|
|
154
|
-
let
|
|
155
|
-
custom = options?.fonts;
|
|
64
|
+
let { page, fonts: custom, meta } = options ?? {};
|
|
65
|
+
let paged = layout({ page, fonts: custom });
|
|
156
66
|
/** @type {(stream: any) => (data?: any) => Promise<Uint8Array>} */
|
|
157
|
-
let compile = (stream) =>
|
|
158
|
-
let
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
let { handlers, finish } = flow(canvas);
|
|
170
|
-
function* events() {
|
|
171
|
-
if (!first.done) yield first.value;
|
|
172
|
-
yield* gen;
|
|
173
|
-
}
|
|
174
|
-
await walk(events(), handlers);
|
|
175
|
-
// The flow settled the opening event on its way past — it reserves the
|
|
176
|
-
// page bands off it — and hands it back with the marks. What is left on it
|
|
177
|
-
// is what the passes below want: the band closures to render per page, and
|
|
178
|
-
// the marking's wording to stamp.
|
|
179
|
-
// Null when nothing settled one, which reads as a document owing neither.
|
|
180
|
-
let { marks, opening, pages } = finish();
|
|
181
|
-
opening = opening || {};
|
|
182
|
-
// The passes below run over the finished pages, not the stream — no walk
|
|
183
|
-
// at all — so they open each page themselves and breathe on their own
|
|
184
|
-
// rather than through the driver.
|
|
185
|
-
await furnish(canvas, opening.page, pages);
|
|
186
|
-
// Every image the walk and the furniture pass placed, embedded and drawn
|
|
187
|
-
// now: a walk handler cannot await, and the page bands' images are not
|
|
188
|
-
// placed until the pass above has run. Before the marking, so an image can
|
|
189
|
-
// never cover it.
|
|
190
|
-
await canvas.pictures();
|
|
191
|
-
// The unlicensed marking goes on last, over content and page furniture
|
|
192
|
-
// alike, once per page (LICENSE section 6). Its wording rode in on
|
|
193
|
-
// `report-start`; only the placement is this target's.
|
|
194
|
-
await markPages(canvas, opening.marking);
|
|
195
|
-
outline(doc, marks, canvas.refs);
|
|
196
|
-
return doc.save();
|
|
67
|
+
let compile = (stream) => {
|
|
68
|
+
let lay = paged.compile(stream);
|
|
69
|
+
return async (data) => {
|
|
70
|
+
// The list measures against its own faces, so the document is written
|
|
71
|
+
// from the same breaks the preview shows; opening the document and
|
|
72
|
+
// embedding its faces depend on nothing the list decides, so they
|
|
73
|
+
// overlap the layout pass.
|
|
74
|
+
let [list, { doc, fonts }] = await Promise.all([lay(data), open(meta, custom)]);
|
|
75
|
+
let refs = await paint(doc, fonts, list);
|
|
76
|
+
outline(doc, list.marks, refs);
|
|
77
|
+
return doc.save();
|
|
78
|
+
};
|
|
197
79
|
};
|
|
198
80
|
return { name: "pdf", compile };
|
|
199
81
|
}
|
package/lib/outline.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* own position. pdf-lib has no bookmark API, so the objects are built through
|
|
8
8
|
* its low-level context — the only place in this target that does.
|
|
9
9
|
*/
|
|
10
|
-
import { PDFHexString, PDFName, PDFNumber } from "pdf-lib";
|
|
10
|
+
import { PDFHexString, PDFName, PDFNumber } from "@cantoo/pdf-lib";
|
|
11
11
|
|
|
12
12
|
// A PDF *text string*: UTF-16BE with a BOM, so a title survives whatever the
|
|
13
13
|
// author wrote. `PDFHexString.fromText` encodes exactly that.
|