@quario/pdf 0.2.0 → 0.4.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 +96 -0
- package/README.md +6 -4
- package/lib/embed.js +111 -0
- package/lib/index.d.ts +5 -14
- package/lib/index.js +35 -132
- package/lib/painter.js +205 -0
- package/package.json +6 -4
- package/lib/balance.js +0 -90
- package/lib/canvas.js +0 -376
- package/lib/fonts.js +0 -180
- package/lib/image.js +0 -58
- package/lib/layout.js +0 -1663
- package/lib/style.js +0 -106
- package/lib/text.js +0 -247
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,102 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-09-03
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **A split slot's box now fills the split's height.** A slot's background and
|
|
15
|
+
border stopped at that slot's own content and now take the split's height,
|
|
16
|
+
the way a table cell's take their row's. See the `@quario/layout` changelog
|
|
17
|
+
for the rule and for what it costs a report that relied on the short box.
|
|
18
|
+
|
|
19
|
+
- **A date string under `format: "date"` now presents as a date.** The
|
|
20
|
+
drawn text comes from the engine's `format()` helper, which revives the two
|
|
21
|
+
read forms. See the `quario` changelog for the forms and the timezone rule.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **The layout moved to `@quario/layout`; this target paints its list.**
|
|
26
|
+
Measurement, wrapping, pagination, page furniture, the marking's geometry
|
|
27
|
+
and the page-size table now live in the layout package, which this target
|
|
28
|
+
depends on and runs first; pdf-lib writes the resulting display list. **The
|
|
29
|
+
move itself changed no output**, byte for byte, for the same input; the fix
|
|
30
|
+
below is a separate change and does move some. The `page` and `fonts`
|
|
31
|
+
options are the layout's own, so the same object configures `pdf()`, the
|
|
32
|
+
viewer and the editor. The font mapping's shape is now refused at the
|
|
33
|
+
factory call rather than at render.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **A nested group's footer keeps the page columns.** Inside a page-column
|
|
38
|
+
region, the first footer of a group nested in it ended the region: the
|
|
39
|
+
strips collapsed and every band after it was laid out across the page, as
|
|
40
|
+
if `columns` were absent. Only the node that declared `columns` closes its
|
|
41
|
+
own region now — a nested group's header and footer are region content,
|
|
42
|
+
laid out in the strips.
|
|
43
|
+
|
|
44
|
+
- **A padded or bordered item or row that breaks across a page keeps its
|
|
45
|
+
box.** Content too tall for one page is drawn as slices, and a slice now
|
|
46
|
+
carries the box sides the break left it — the top on the first, the bottom
|
|
47
|
+
on the last. Previously an item that overflowed lost its box entirely, so
|
|
48
|
+
the same declaration drew a border on short data and none on long; a
|
|
49
|
+
bordered table cell drew all four sides on every slice, putting an edge
|
|
50
|
+
across the middle of its own text; and a row's own border vanished when
|
|
51
|
+
the row was too tall for a page. A slice's lines are also drawn inside
|
|
52
|
+
the horizontal padding they were wrapped for, rather than flush to the
|
|
53
|
+
column. **This changes output** for reports where such content breaks.
|
|
54
|
+
|
|
55
|
+
## [0.3.0] - 2026-09-02
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **`format` stringifies at the edge from the instance locale.** A PDF
|
|
60
|
+
without a host locale still uses `en-US` / UTC, so the bytes stay
|
|
61
|
+
reproducible.
|
|
62
|
+
|
|
63
|
+
- **A height-declared report header pins from the page top.** Leftover sits
|
|
64
|
+
under the items; the next band starts at the pin. The half-line group gap
|
|
65
|
+
drops after it. `page.margin` on the document is the inset when the host
|
|
66
|
+
omits it; both is a render error.
|
|
67
|
+
|
|
68
|
+
- **`spaceBefore` / `spaceAfter` skip the cursor.** Adjacent gaps add.
|
|
69
|
+
`spaceBefore` drops at a fresh body page or strip top; page-band items
|
|
70
|
+
keep it.
|
|
71
|
+
|
|
72
|
+
- **The box is honoured in layout.** Padding and border inset wrap width; a
|
|
73
|
+
named padding `0` beats the cell omakase (`PADX` 6 / `PADY` 2) on that
|
|
74
|
+
side. Incomplete sides draw nothing.
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
|
|
78
|
+
- **The last data row keeps the whole emitted total block.** A stack taller
|
|
79
|
+
than a page degrades like a tall row.
|
|
80
|
+
|
|
81
|
+
- **An unstyled table has no rules.** This target used to draw two 0.5 pt
|
|
82
|
+
rules, under the header and above the total, that no declaration could
|
|
83
|
+
ask for or refuse. Those strokes are gone. The strokes an author wants
|
|
84
|
+
are the box they declared. Cell padding omakase stays on undeclared cell
|
|
85
|
+
sides.
|
|
86
|
+
|
|
87
|
+
- **A visible text item occupies a line at its own `size`, empty or not.** An
|
|
88
|
+
empty or whitespace-only value used to sit at the report's base leading
|
|
89
|
+
(~14 pt at the 10 pt baseline). It now occupies `1.4 ×` the item's size, the
|
|
90
|
+
same as a glyph line. A literal newline is a line break; the blank line
|
|
91
|
+
among `"a\n\nb"` is that size too. A table cell that is empty or only
|
|
92
|
+
horizontal whitespace still has no content height.
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
|
|
96
|
+
- **A missing TrueType glyph draws as `?`, not `.notdef`.** Characters
|
|
97
|
+
outside WinAnsi already substituted `?`; an embedded face's cmap holes
|
|
98
|
+
drew a box instead, and text extraction hid it. Same rule for every
|
|
99
|
+
face: a character the face cannot draw becomes `?`.
|
|
100
|
+
|
|
101
|
+
- **CR, LF, and CRLF are one hard line break.** Cell text already broke
|
|
102
|
+
on LF; a CRLF left a CR on the previous line, and a lone CR did not
|
|
103
|
+
break. All three are now one break, matching `SCHEMA.md` Cell values.
|
|
104
|
+
Wrapping still applies within each line.
|
|
105
|
+
|
|
10
106
|
## [0.2.0] - 2026-09-01
|
|
11
107
|
|
|
12
108
|
### Added
|
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. [pdf-lib](https://pdf-lib.js.org) 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
|
|
@@ -135,8 +136,9 @@ without authored margins.
|
|
|
135
136
|
|
|
136
137
|
The base-14 Helvetica, Times, and Courier families carry the default output (`family: "sans"`,
|
|
137
138
|
`"serif"`, `"mono"`, each with regular, bold, italic, and bold-italic faces), using WinAnsi
|
|
138
|
-
encoding. Characters
|
|
139
|
-
text is untrusted data and one stray character must not take a document down.
|
|
139
|
+
encoding. Characters a face cannot draw render as `?` rather than failing the report, because cell
|
|
140
|
+
text is untrusted data and one stray character must not take a document down. WinAnsi is the
|
|
141
|
+
base-14 limit; a TrueType face's cmap is its own.
|
|
140
142
|
|
|
141
143
|
For full Unicode, supply TrueType families and select them by name:
|
|
142
144
|
|
package/lib/embed.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
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 "pdf-lib";
|
|
15
|
+
|
|
16
|
+
let BASE = {
|
|
17
|
+
sans: [
|
|
18
|
+
StandardFonts.Helvetica,
|
|
19
|
+
StandardFonts.HelveticaBold,
|
|
20
|
+
StandardFonts.HelveticaOblique,
|
|
21
|
+
StandardFonts.HelveticaBoldOblique,
|
|
22
|
+
],
|
|
23
|
+
serif: [
|
|
24
|
+
StandardFonts.TimesRoman,
|
|
25
|
+
StandardFonts.TimesRomanBold,
|
|
26
|
+
StandardFonts.TimesRomanItalic,
|
|
27
|
+
StandardFonts.TimesRomanBoldItalic,
|
|
28
|
+
],
|
|
29
|
+
mono: [
|
|
30
|
+
StandardFonts.Courier,
|
|
31
|
+
StandardFonts.CourierBold,
|
|
32
|
+
StandardFonts.CourierOblique,
|
|
33
|
+
StandardFonts.CourierBoldOblique,
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** @typedef {{ families: Record<string, any[]> }} Fonts */
|
|
38
|
+
|
|
39
|
+
// Host-supplied TrueType families need a font parser, which is an optional
|
|
40
|
+
// peer: base-14 reports install nothing extra. Loaded only when `fonts` is
|
|
41
|
+
// passed, and a missing package is reported as what it is.
|
|
42
|
+
/** @type {(doc: any) => Promise<void>} */
|
|
43
|
+
let useFontkit = async (doc) => {
|
|
44
|
+
let fontkit;
|
|
45
|
+
try {
|
|
46
|
+
fontkit = (await import("@pdf-lib/fontkit")).default;
|
|
47
|
+
} catch {
|
|
48
|
+
throw Error("options.fonts: install @pdf-lib/fontkit to embed TrueType families");
|
|
49
|
+
}
|
|
50
|
+
doc.registerFontkit(fontkit);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** @type {(doc: any) => Promise<Record<string, any[]>>} */
|
|
54
|
+
let embedBase = async (doc) => {
|
|
55
|
+
/** @type {Record<string, any[]>} */
|
|
56
|
+
let families = {};
|
|
57
|
+
for (let [name, variants] of Object.entries(BASE))
|
|
58
|
+
families[name] = await Promise.all(variants.map((v) => doc.embedFont(v)));
|
|
59
|
+
return families;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** @type {(doc: any, src: any, path: string, variant: string) => Promise<any>} */
|
|
63
|
+
let embedOne = async (doc, src, path, variant) => {
|
|
64
|
+
try {
|
|
65
|
+
return await doc.embedFont(src[variant], { subset: true });
|
|
66
|
+
} catch (e) {
|
|
67
|
+
throw Error(path + "." + variant + ": " + /** @type {Error} */ (e).message);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Missing variants fall back to the family's regular, so a face is always
|
|
72
|
+
// resolvable once the family is named. The shape was checked at the layout's
|
|
73
|
+
// factory call; this only embeds.
|
|
74
|
+
/** @type {(doc: any, name: string, def: any) => Promise<any[]>} */
|
|
75
|
+
let familyOf = async (doc, name, def) => {
|
|
76
|
+
let path = "options.fonts." + name;
|
|
77
|
+
/** @type {any[]} */
|
|
78
|
+
let faces = [];
|
|
79
|
+
for (let variant of ["regular", "bold", "italic", "boldItalic"])
|
|
80
|
+
faces.push(def[variant] == null ? faces[0] : await embedOne(doc, def, path, variant));
|
|
81
|
+
return faces;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** @type {(doc: any, custom: any, families: Record<string, any[]>) => Promise<void>} */
|
|
85
|
+
let embedCustom = async (doc, custom, families) => {
|
|
86
|
+
if (!(custom && Object.keys(custom).length)) return;
|
|
87
|
+
await useFontkit(doc);
|
|
88
|
+
for (let [name, def] of Object.entries(custom))
|
|
89
|
+
families[name.toLowerCase()] = await familyOf(doc, name, def);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Embed every face the list can name and return the registry the painter
|
|
94
|
+
* reads.
|
|
95
|
+
*
|
|
96
|
+
* @param {any} doc The pdf-lib document.
|
|
97
|
+
* @param {any} custom `options.fonts`, or null.
|
|
98
|
+
* @returns {Promise<Fonts>}
|
|
99
|
+
*/
|
|
100
|
+
export async function embedFonts(doc, custom) {
|
|
101
|
+
let families = await embedBase(doc);
|
|
102
|
+
await embedCustom(doc, custom, families);
|
|
103
|
+
return { families };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The embedded font one list op names.
|
|
108
|
+
*
|
|
109
|
+
* @type {(fonts: Fonts, ref: { family: string, variant: number }) => any}
|
|
110
|
+
*/
|
|
111
|
+
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 {
|
package/lib/index.js
CHANGED
|
@@ -1,88 +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
|
|
9
|
-
* (`pdf({ page })`)
|
|
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
|
+
* (`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
16
|
import { PDFDocument } from "pdf-lib";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { embedFonts } from "./fonts.js";
|
|
22
|
-
import { flow, furniture } from "./layout.js";
|
|
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) => number} */
|
|
61
|
-
let marginOf = (margin = 54, width, height) => {
|
|
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("options.page.margin: 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, settled here beside the validation
|
|
75
|
-
// that produced them. This frame never moves at all: it serves every render the
|
|
76
|
-
// target compiles, so a narrowing here would be one document's page bands
|
|
77
|
-
// reserved out of the next one too. The one narrowing a render does make is the
|
|
78
|
-
// band flow's, off that render's own canvas.
|
|
79
|
-
/** @type {(options: any) => import('./canvas.js').Frame} */
|
|
80
|
-
let geometry = (options) => {
|
|
81
|
-
let { page = {} } = options ?? {};
|
|
82
|
-
let { width, height } = pair(named(page.size));
|
|
83
|
-
return frame(width, height, marginOf(page.margin, width, height), BASE);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
26
|
// Optional document information. Never a date: pdf-lib stamps the current time
|
|
87
27
|
// unless told otherwise, and a timestamp would make the same input render
|
|
88
28
|
// different bytes on every run. Each key names the pdf-lib setter it feeds,
|
|
@@ -101,27 +41,11 @@ let describe = (doc, meta) => {
|
|
|
101
41
|
}
|
|
102
42
|
};
|
|
103
43
|
|
|
104
|
-
/** @type {(
|
|
105
|
-
let
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (i % 50 === 49) await breathe();
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/** @type {(canvas: import('./canvas.js').Drawing, bands: any,
|
|
114
|
-
* pages: { number: number, total: number }[]) => Promise<void>} */
|
|
115
|
-
let furnish = async (canvas, bands, pages) => {
|
|
116
|
-
if (!bands) return;
|
|
117
|
-
await overPages(canvas, (i) => furniture(canvas, bands, pages[i]));
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/** @type {(canvas: import('./canvas.js').Drawing, marking: any) => Promise<void>} */
|
|
121
|
-
let markPages = async (canvas, marking) => {
|
|
122
|
-
if (!marking) return;
|
|
123
|
-
let mark = stamp(canvas, marking);
|
|
124
|
-
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) };
|
|
125
49
|
};
|
|
126
50
|
|
|
127
51
|
/**
|
|
@@ -129,50 +53,29 @@ let markPages = async (canvas, marking) => {
|
|
|
129
53
|
* `quario().report(schema).render(pdf({ page }), data)` resolves the
|
|
130
54
|
* complete document as a `Uint8Array` (the host writes the file). Rendering
|
|
131
55
|
* hands the loop back between event and page batches, so large reports stay
|
|
132
|
-
* 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.
|
|
133
58
|
*
|
|
134
59
|
* @param {PdfOptions} [options] Host controls (see SCHEMA.md, "The PDF target").
|
|
135
60
|
* @returns {{name: "pdf", compile: (stream: any) => (data?: any) => Promise<Uint8Array>}}
|
|
136
61
|
* The target (see SCHEMA.md, "Instances and targets").
|
|
137
62
|
*/
|
|
138
63
|
export function pdf(options) {
|
|
139
|
-
let
|
|
140
|
-
let
|
|
141
|
-
custom = options?.fonts;
|
|
64
|
+
let { page, fonts: custom, meta } = options ?? {};
|
|
65
|
+
let paged = layout({ page, fonts: custom });
|
|
142
66
|
/** @type {(stream: any) => (data?: any) => Promise<Uint8Array>} */
|
|
143
|
-
let compile = (stream) =>
|
|
144
|
-
let
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// page bands off it — and hands it back with the marks. What is left on it
|
|
156
|
-
// is what the passes below want: the band closures to render per page, and
|
|
157
|
-
// the marking's wording to stamp.
|
|
158
|
-
// Null when nothing settled one, which reads as a document owing neither.
|
|
159
|
-
let { marks, opening, pages } = finish();
|
|
160
|
-
opening = opening || {};
|
|
161
|
-
// The passes below run over the finished pages, not the stream — no walk
|
|
162
|
-
// at all — so they open each page themselves and breathe on their own
|
|
163
|
-
// rather than through the driver.
|
|
164
|
-
await furnish(canvas, opening.page, pages);
|
|
165
|
-
// Every image the walk and the furniture pass placed, embedded and drawn
|
|
166
|
-
// now: a walk handler cannot await, and the page bands' images are not
|
|
167
|
-
// placed until the pass above has run. Before the marking, so an image can
|
|
168
|
-
// never cover it.
|
|
169
|
-
await canvas.pictures();
|
|
170
|
-
// The unlicensed marking goes on last, over content and page furniture
|
|
171
|
-
// alike, once per page (LICENSE section 6). Its wording rode in on
|
|
172
|
-
// `report-start`; only the placement is this target's.
|
|
173
|
-
await markPages(canvas, opening.marking);
|
|
174
|
-
outline(doc, marks, canvas.refs);
|
|
175
|
-
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
|
+
};
|
|
176
79
|
};
|
|
177
80
|
return { name: "pdf", compile };
|
|
178
81
|
}
|