@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/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 geometry is target configuration
9
- * (`pdf({ page })`), never schema.
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 {(page: any) => any} */
73
- let marginArg = (page) => page.margin ?? 54;
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("options.page.margin: expected a non-negative number smaller than half the page");
64
+ fits || err(path + ": expected a non-negative number smaller than half the page");
80
65
  return margin;
81
66
  };
82
67
 
83
- /** @type {(options: any) => any} */
84
- let baseArg = (options) => options?.baseSize ?? 10;
85
-
86
- /** @type {(base: any) => number} */
87
- let baseOf = (base) => {
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, settled here beside the validation
94
- // that produced them. This frame never moves at all: it serves every render the
95
- // target compiles, so a narrowing here would be one document's page bands
96
- // reserved out of the next one too. The one narrowing a render does make is the
97
- // band flow's, off that render's own canvas.
98
- /** @type {(options: any) => import('./canvas.js').Frame} */
99
- let geometry = (options) => {
100
- let page = pageOf(options);
101
- let { width, height } = pair(pageSize(page));
102
- return frame(width, height, marginOf(marginArg(page), width, height), baseOf(baseArg(options)));
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 META = [
106
- ["title", "setTitle"],
107
- ["author", "setAuthor"],
108
- ["subject", "setSubject"],
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
- if (meta) for (let entry of META) field(doc, meta, entry);
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
- let geo = geometry(options);
165
- let { meta, fonts: custom } = host(options);
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
- await walk(stream(data), handlers);
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
- let { bands, marking } = openingOf(opening);
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, bands, pages);
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
  };