@quario/pdf 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 +93 -0
- package/README.md +6 -5
- package/lib/balance.js +1 -1
- package/lib/box.js +111 -0
- package/lib/canvas.js +38 -24
- package/lib/fonts.js +71 -29
- package/lib/image.js +2 -7
- package/lib/index.d.ts +0 -2
- package/lib/index.js +60 -62
- package/lib/layout.js +445 -140
- package/lib/outline.js +18 -22
- package/lib/style.js +25 -12
- package/lib/text.js +39 -22
- package/package.json +4 -4
package/lib/index.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* headers, page bands, `break: "page"`, `reset: "page"` — while the writing is pdf-lib's, so
|
|
6
6
|
* this target carries a typesetter rather than a PDF implementation. Output
|
|
7
7
|
* is deterministic: the document carries no dates of its own, so the same
|
|
8
|
-
* input renders the same bytes. Page
|
|
9
|
-
* (`pdf({ page })`)
|
|
8
|
+
* 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
11
|
* This file is options, fonts and geometry, the walk itself, and the passes
|
|
12
12
|
* that follow it over the finished pages — page furniture and the marking,
|
|
@@ -36,29 +36,17 @@ let err = (msg) => {
|
|
|
36
36
|
throw Error(msg);
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
/** @type {(value: any) => boolean} */
|
|
40
|
-
let finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
41
|
-
|
|
42
39
|
/** @type {(value: number) => boolean} */
|
|
43
40
|
let positive = (value) => Number.isFinite(value) && value > 0;
|
|
44
41
|
|
|
45
|
-
/** @type {(options: any) => any} */
|
|
46
|
-
let pageOf = (options) => options?.page ?? {};
|
|
47
|
-
|
|
48
|
-
/** @type {(options: any) => { meta: any, fonts: any }} */
|
|
49
|
-
let host = (options) => ({ meta: options?.meta, fonts: options?.fonts });
|
|
50
|
-
|
|
51
42
|
/** @type {(size: any) => any} */
|
|
52
|
-
let named = (size) => {
|
|
43
|
+
let named = (size = "A4") => {
|
|
53
44
|
let dimensions = Array.isArray(size) ? size : SIZES[size];
|
|
54
45
|
// oxlint-disable-next-line no-unused-expressions
|
|
55
46
|
dimensions || err('options.page.size: unknown page size "' + size + '"');
|
|
56
47
|
return dimensions;
|
|
57
48
|
};
|
|
58
49
|
|
|
59
|
-
/** @type {(page: any) => any} */
|
|
60
|
-
let pageSize = (page) => named(page.size ?? "A4");
|
|
61
|
-
|
|
62
50
|
/** @type {(dimensions: any) => { width: number, height: number }} */
|
|
63
51
|
let pair = (dimensions) => {
|
|
64
52
|
let width = +dimensions[0],
|
|
@@ -69,58 +57,62 @@ let pair = (dimensions) => {
|
|
|
69
57
|
return { width, height };
|
|
70
58
|
};
|
|
71
59
|
|
|
72
|
-
/** @type {(
|
|
73
|
-
let
|
|
74
|
-
|
|
75
|
-
/** @type {(margin: any, width: number, height: number) => number} */
|
|
76
|
-
let marginOf = (margin, width, height) => {
|
|
77
|
-
let fits = finite(margin) && margin >= 0 && 2 * margin < Math.min(width, height);
|
|
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);
|
|
78
63
|
// oxlint-disable-next-line no-unused-expressions
|
|
79
|
-
fits || err("
|
|
64
|
+
fits || err(path + ": expected a non-negative number smaller than half the page");
|
|
80
65
|
return margin;
|
|
81
66
|
};
|
|
82
67
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let
|
|
88
|
-
// oxlint-disable-next-line no-unused-expressions
|
|
89
|
-
(finite(base) && base > 0) || err("options.baseSize: expected a positive number of points");
|
|
90
|
-
return base;
|
|
91
|
-
};
|
|
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;
|
|
92
73
|
|
|
93
|
-
// The page box and the content box in one
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
/** @type {(
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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) };
|
|
103
91
|
};
|
|
104
|
-
|
|
105
|
-
let
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
/** @type {(doc: any, meta: any, entry: string[]) => void} */
|
|
112
|
-
let field = (doc, meta, [key, method]) => {
|
|
113
|
-
if (typeof meta[key] === "string") doc[method](meta[key]);
|
|
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);
|
|
114
98
|
};
|
|
115
99
|
|
|
116
100
|
// Optional document information. Never a date: pdf-lib stamps the current time
|
|
117
101
|
// unless told otherwise, and a timestamp would make the same input render
|
|
118
|
-
// different bytes on every run.
|
|
102
|
+
// different bytes on every run. Each key names the pdf-lib setter it feeds,
|
|
103
|
+
// here rather than in a table, and a value of any other shape is left off.
|
|
119
104
|
/** @type {(doc: any, meta: any) => void} */
|
|
120
105
|
let describe = (doc, meta) => {
|
|
121
106
|
doc.setCreationDate(new Date(0));
|
|
122
107
|
doc.setModificationDate(new Date(0));
|
|
123
|
-
|
|
108
|
+
let field = (/** @type {string} */ method, /** @type {any} */ value) => {
|
|
109
|
+
if (typeof value === "string") doc[method](value);
|
|
110
|
+
};
|
|
111
|
+
if (meta) {
|
|
112
|
+
field("setTitle", meta.title);
|
|
113
|
+
field("setAuthor", meta.author);
|
|
114
|
+
field("setSubject", meta.subject);
|
|
115
|
+
}
|
|
124
116
|
};
|
|
125
117
|
|
|
126
118
|
/** @type {(canvas: import('./canvas.js').Drawing, draw: (i: number) => void) => Promise<void>} */
|
|
@@ -132,9 +124,6 @@ let overPages = async (canvas, draw) => {
|
|
|
132
124
|
}
|
|
133
125
|
};
|
|
134
126
|
|
|
135
|
-
/** @type {(opening: any) => { bands: any, marking: any }} */
|
|
136
|
-
let openingOf = (opening) => ({ bands: opening?.page, marking: opening?.marking });
|
|
137
|
-
|
|
138
127
|
/** @type {(canvas: import('./canvas.js').Drawing, bands: any,
|
|
139
128
|
* pages: { number: number, total: number }[]) => Promise<void>} */
|
|
140
129
|
let furnish = async (canvas, bands, pages) => {
|
|
@@ -161,8 +150,9 @@ let markPages = async (canvas, marking) => {
|
|
|
161
150
|
* The target (see SCHEMA.md, "Instances and targets").
|
|
162
151
|
*/
|
|
163
152
|
export function pdf(options) {
|
|
164
|
-
|
|
165
|
-
let
|
|
153
|
+
geometry(options);
|
|
154
|
+
let meta = options?.meta,
|
|
155
|
+
custom = options?.fonts;
|
|
166
156
|
/** @type {(stream: any) => (data?: any) => Promise<Uint8Array>} */
|
|
167
157
|
let compile = (stream) => async (data) => {
|
|
168
158
|
let doc = await PDFDocument.create();
|
|
@@ -170,21 +160,29 @@ export function pdf(options) {
|
|
|
170
160
|
// Every face is embedded before the walk, so resolving a style to a
|
|
171
161
|
// font during layout stays synchronous.
|
|
172
162
|
let fonts = await embedFonts(doc, custom);
|
|
163
|
+
let gen = stream(data);
|
|
164
|
+
let first = gen.next();
|
|
165
|
+
let geo = geometry(options, first.done ? null : first.value);
|
|
173
166
|
let canvas = drawing(doc, geo, fonts);
|
|
174
167
|
// The band flow owns the placement state and every handler over it, and
|
|
175
168
|
// opens the first page as it is built; this file only hands it the stream.
|
|
176
169
|
let { handlers, finish } = flow(canvas);
|
|
177
|
-
|
|
170
|
+
function* events() {
|
|
171
|
+
if (!first.done) yield first.value;
|
|
172
|
+
yield* gen;
|
|
173
|
+
}
|
|
174
|
+
await walk(events(), handlers);
|
|
178
175
|
// The flow settled the opening event on its way past — it reserves the
|
|
179
176
|
// page bands off it — and hands it back with the marks. What is left on it
|
|
180
177
|
// is what the passes below want: the band closures to render per page, and
|
|
181
178
|
// the marking's wording to stamp.
|
|
179
|
+
// Null when nothing settled one, which reads as a document owing neither.
|
|
182
180
|
let { marks, opening, pages } = finish();
|
|
183
|
-
|
|
181
|
+
opening = opening || {};
|
|
184
182
|
// The passes below run over the finished pages, not the stream — no walk
|
|
185
183
|
// at all — so they open each page themselves and breathe on their own
|
|
186
184
|
// rather than through the driver.
|
|
187
|
-
await furnish(canvas,
|
|
185
|
+
await furnish(canvas, opening.page, pages);
|
|
188
186
|
// Every image the walk and the furniture pass placed, embedded and drawn
|
|
189
187
|
// now: a walk handler cannot await, and the page bands' images are not
|
|
190
188
|
// placed until the pass above has run. Before the marking, so an image can
|
|
@@ -193,7 +191,7 @@ export function pdf(options) {
|
|
|
193
191
|
// The unlicensed marking goes on last, over content and page furniture
|
|
194
192
|
// alike, once per page (LICENSE section 6). Its wording rode in on
|
|
195
193
|
// `report-start`; only the placement is this target's.
|
|
196
|
-
await markPages(canvas, marking);
|
|
194
|
+
await markPages(canvas, opening.marking);
|
|
197
195
|
outline(doc, marks, canvas.refs);
|
|
198
196
|
return doc.save();
|
|
199
197
|
};
|