@reportwright/engine 0.11.0 → 0.12.1

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.
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @typedef {{ write(u8: Uint8Array): any, once?: (ev: string, cb: () => void) => any }} Sink
3
+ * write may return a promise (awaited: back-pressure), or false with a 'drain' event to wait for (a Node Writable).
4
+ * @typedef {{ title: string, page: number, top?: number, level?: number }} OutlineEntry
5
+ * page: 1-based; top: the destination's y in PDF space (from the bottom), else the page fits the window.
6
+ */
7
+ /**
8
+ * @param {Sink} sink
9
+ * @param {{ fontStore: import('../engine/text/fonts.js').FontStore, fonts?: string[],
10
+ * subset?: (font: Uint8Array, codePoints: Iterable<number>, opt?: { glyphs?: Iterable<number> }) => Promise<Uint8Array>,
11
+ * id?: string }} opt
12
+ * fonts: font keys to name now (F1, F2… in this order); any other font is named on first use.
13
+ * subset: subset.js's subsetter; without it fonts embed in full. id: the file ID (32 hex digits), else random.
14
+ */
15
+ export function createPdfStream(sink: Sink, opt: {
16
+ fontStore: import("../engine/text/fonts.js").FontStore;
17
+ fonts?: string[];
18
+ subset?: (font: Uint8Array, codePoints: Iterable<number>, opt?: {
19
+ glyphs?: Iterable<number>;
20
+ }) => Promise<Uint8Array>;
21
+ id?: string;
22
+ }): {
23
+ /** @param {string} key @returns {string} the font's resource name (F1…) */
24
+ fontName: (key: string) => string;
25
+ /**
26
+ * The text's glyphs, one per character (as the engine measured it: no layout features), as hex for <…> Tj, with
27
+ * the glyphs recorded for the subset, W and ToUnicode.
28
+ * @param {string} key @param {string} text
29
+ */
30
+ encode(key: string, text: string): string;
31
+ /**
32
+ * The same glyphs as a literal string's body, two bytes each, escaped (as pdf.js draws plain text): ( … ) Tj.
33
+ * @param {string} key @param {string} text
34
+ */
35
+ literal(key: string, text: string): string;
36
+ /**
37
+ * Shaped glyphs drawn by ID (conjuncts, joined forms): [glyph ID, the text it stands for ('' for none)].
38
+ * @param {string} key @param {Iterable<[number, string]>} glyphs
39
+ */
40
+ addGlyphs(key: string, glyphs: Iterable<[number, string]>): void;
41
+ /**
42
+ * A name in the shared Resources dictionary for a value (an indirect ref "12 0 R" or an inline dictionary).
43
+ * @param {'ExtGState'|'XObject'|'Pattern'|'Shading'|'ColorSpace'} kind @param {string} value
44
+ */
45
+ resource(kind: "ExtGState" | "XObject" | "Pattern" | "Shading" | "ColorSpace", value: string, name?: any): string;
46
+ /**
47
+ * The ExtGState name for fill and stroke opacity, named by its values (the same name in every writer: pages
48
+ * painted elsewhere, parallel.js, use it too). @param {number} ca @param {number} CA
49
+ */
50
+ gs: (ca: number, CA: number) => string;
51
+ /** What this writer's pages used, for another writer to take over (pages painted elsewhere). */
52
+ used: () => {
53
+ fonts: (string | number[] | [number, string][])[][];
54
+ gs: string[];
55
+ };
56
+ /** Take over what pages painted elsewhere used (used() of their writer). @param {any} u */
57
+ take(u: any): void;
58
+ /**
59
+ * Write an object now (an image: its dictionary and its stream, already filtered). @returns {Promise<number>} its number
60
+ * @param {string} dict @param {Uint8Array} [stream]
61
+ */
62
+ object(dict: string, stream?: Uint8Array): Promise<number>;
63
+ /** An object number to write later (objectAt). */
64
+ reserve: () => number;
65
+ /** Write a reserved object. @param {number} n @param {string} dict @param {Uint8Array} [stream] */
66
+ objectAt: (n: number, dict: string, stream?: Uint8Array) => Promise<void>;
67
+ /**
68
+ * Write a reserved object whose text may be long (an array of every row's element): head, the parts as they come,
69
+ * tail; written out in place, never held whole. @param {number} n @param {string} head @param {Iterable<string>} parts @param {string} tail
70
+ */
71
+ bigObject(n: number, head: string, parts: Iterable<string>, tail: string): Promise<void>;
72
+ /** The object number of page i (0-based), written or not: links and outline entries may point ahead. @param {number} i */
73
+ pageRef(i: number): number;
74
+ /**
75
+ * The total page count, drawn at the end: a Form XObject whose origin is the text's baseline at the box's left;
76
+ * draw it with `q 1 0 0 1 x y cm /<name> Do Q`. align: within width.
77
+ * @param {{ font: string, size: number, width: number, align?: 'left'|'center'|'right', color?: string }} o
78
+ */
79
+ totalPages(o: {
80
+ font: string;
81
+ size: number;
82
+ width: number;
83
+ align?: "left" | "center" | "right";
84
+ color?: string;
85
+ }): string;
86
+ /**
87
+ * Write a page: its content stream (deflated) and its dictionary, now. Nothing of it stays but two offsets.
88
+ * @param {{ width: number, height: number, content?: string | Uint8Array, deflated?: Uint8Array, annots?: (string | { ref: number, dict: string })[], extra?: string }} p
89
+ * deflated: the content stream already compressed (zlib), as pages painted elsewhere come.
90
+ * content: the page's operators (a string holds bytes 0–255, as pdf.js's Content makes). annots: annotation
91
+ * dictionaries (with a reserved number: { ref, dict }). extra: more entries for the page dictionary (/StructParents 3).
92
+ */
93
+ addPage(p: {
94
+ width: number;
95
+ height: number;
96
+ content?: string | Uint8Array;
97
+ deflated?: Uint8Array;
98
+ annots?: (string | {
99
+ ref: number;
100
+ dict: string;
101
+ })[];
102
+ extra?: string;
103
+ }): Promise<void>;
104
+ /**
105
+ * The end of the file: totals, fonts, resources, page tree, outline, catalog, Info, xref, trailer.
106
+ * @param {{ outline?: OutlineEntry[], info?: { title?: string, author?: string, subject?: string, creationDate?: Date }, catalog?: string }} [o]
107
+ * catalog: more entries for the catalog (the structure tree, metadata, output intents).
108
+ * @returns {Promise<{ pages: number, bytes: number }>}
109
+ */
110
+ finish(o?: {
111
+ outline?: OutlineEntry[];
112
+ info?: {
113
+ title?: string;
114
+ author?: string;
115
+ subject?: string;
116
+ creationDate?: Date;
117
+ };
118
+ catalog?: string;
119
+ }): Promise<{
120
+ pages: number;
121
+ bytes: number;
122
+ }>;
123
+ };
124
+ /**
125
+ * A report to PDF bytes written to sink as its pages are laid out (planPaged: a flowing table, one window of rows at a
126
+ * time), else rendered whole and exported by exportPdf, the same file it always was, written to sink in one piece.
127
+ * opts: render()'s options and exportPdf's (fontStore, subset, title, author, creationDate, drillBase…). Tagged
128
+ * (PDF/UA-1) and PDF/A files stream too; encrypted and signed files, save options and PDF/A without a colour profile
129
+ * take the whole-model path (the result's why says which).
130
+ * @param {any} def @param {any} opts @param {Sink} sink
131
+ * @returns {Promise<{ streamed: boolean, pages: number, warnings: string[], why?: string }>}
132
+ */
133
+ export function exportPdfStream(def: any, opts: any, sink: Sink): Promise<{
134
+ streamed: boolean;
135
+ pages: number;
136
+ warnings: string[];
137
+ why?: string;
138
+ }>;
139
+ /**
140
+ * The pages of a paged plan (planPaged) as a PDF written to sink, page by page.
141
+ * @param {any} plan @param {any} opts fontStore, subset, title, author, creationDate, drillBase @param {Sink} sink
142
+ * @returns {Promise<{ pages: number, warnings: string[] }>}
143
+ */
144
+ export function writePagedPdf(plan: any, opts: any, sink: Sink): Promise<{
145
+ pages: number;
146
+ warnings: string[];
147
+ }>;
148
+ /**
149
+ * A page of the model as its content stream's operators and its link annotations, drawn with the shared painter
150
+ * (pdfpaint.js) as exportPdf draws it. env (paintEnv): the writer's names and glyph records, the page refs links jump
151
+ * to, the tagger when tagged. Pages painted in parallel (parallel.js) use a writer of their own and the same names.
152
+ * @param {any} pg @param {number} H @param {any} env
153
+ * @returns {{ content: string, annots: any[] }}
154
+ */
155
+ export function paintPage(pg: any, H: number, env: any): {
156
+ content: string;
157
+ annots: any[];
158
+ };
159
+ /**
160
+ * What paintPage draws with: a writer's resource names and glyph records (w), the fonts, shaped glyphs' texts.
161
+ * @param {any} w @param {any} fontStore @param {{ drillBase?: string, pageRef?: (i: number) => any, tags?: any }} [o]
162
+ */
163
+ export function paintEnv(w: any, fontStore: any, o?: {
164
+ drillBase?: string;
165
+ pageRef?: (i: number) => any;
166
+ tags?: any;
167
+ }): {
168
+ shared: {
169
+ font: (f: any) => any;
170
+ gs: (ca: any, CA: any) => any;
171
+ image: () => never;
172
+ };
173
+ pf: (key: string) => any;
174
+ font: (key: string) => any;
175
+ shaped: Map<string, Map<number, string>>;
176
+ shapedOf: (key: string) => Map<number, string>;
177
+ vmetrics: (key: string) => any;
178
+ literal: (key: string, text: string) => any;
179
+ pageRef: (i: number) => any;
180
+ reserve: () => any;
181
+ drillBase: string;
182
+ tags: any;
183
+ };
184
+ /**
185
+ * write may return a promise (awaited: back-pressure), or false with a 'drain' event to wait for (a Node Writable).
186
+ */
187
+ export type Sink = {
188
+ write(u8: Uint8Array): any;
189
+ once?: (ev: string, cb: () => void) => any;
190
+ };
191
+ /**
192
+ * page: 1-based; top: the destination's y in PDF space (from the bottom), else the page fits the window.
193
+ */
194
+ export type OutlineEntry = {
195
+ title: string;
196
+ page: number;
197
+ top?: number;
198
+ level?: number;
199
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The structure tree of a streamed, tagged PDF (PDF/UA-1, and PDF/A-2a with it): pdfua.js's tree for the shapes that
3
+ * stream (engine/paged.js), written as the pages are. Document > P / H1–H6 (text boxes), Table > TR > TH/TD (the table's
4
+ * header rows TH with Scope Column the first time, an artifact where they repeat; empty cells as empty TD; spans as
5
+ * ColSpan), Link > OBJR inside the element whose text it covers; page header and footer, borders and boxes artifacts.
6
+ * Each page's rows (TR, TD), its parent-tree array and its links' elements are written when the page ends; what stays
7
+ * is one number per row (the Table's kids) and two per page and link (the parent tree), written at the end.
8
+ * A cell's element is a few dozen bytes in an object stream (no /Type, which is optional; no /A on a plain TD): a
9
+ * million-row table has some 8 million of them, the most of the file's objects.
10
+ */
11
+ /**
12
+ * @param {any} w the PDF writer (pdfstream.js createPdfStream)
13
+ * @param {{ maxElements?: number }} [o] maxElements: past it the export stops (413)
14
+ */
15
+ export function createStreamTagger(w: any, { maxElements }?: {
16
+ maxElements?: number;
17
+ }): {
18
+ /** structure elements so far */
19
+ readonly count: number;
20
+ /** a new page (window: the paged window it came from; row ids are its own) @param {number} window */
21
+ page(window: number): void;
22
+ /**
23
+ * The marked content that opens item it: a structure element's MCID, or an artifact; null for a link.
24
+ * @param {any} it
25
+ */
26
+ open(it: any): string;
27
+ /**
28
+ * A link annotation of the page: its Link element (in the element of the text it covers, else the Document).
29
+ * @returns {number} the annotation's StructParent
30
+ * @param {any} it @param {number} annot the annotation's object number
31
+ */
32
+ link(it: any, annot: number): number;
33
+ /**
34
+ * The page is drawn: its elements and parent-tree array are written. @returns {Promise<string>} the page's entries
35
+ * @param {number} pageRef
36
+ */
37
+ end(pageRef: number): Promise<string>;
38
+ /**
39
+ * The tree's top: Table, Document, the parent tree, the root. @returns {Promise<string>} the catalog's entries
40
+ * @param {{ lang?: string }} o
41
+ */
42
+ finish({ lang }?: {
43
+ lang?: string;
44
+ }): Promise<string>;
45
+ };
@@ -0,0 +1,20 @@
1
+ /** At the server's start: remove the spill folders of processes that are gone (pw-spill-<pid>-…) and old-style ones. */
2
+ export function sweepAtBoot(root?: any): void;
3
+ export function spillSorter({ run, runBytes, frame, maxMB, totalMB, dir: root, staleMs }?: {
4
+ run?: number;
5
+ runBytes?: number;
6
+ frame?: number;
7
+ maxMB?: number;
8
+ totalMB?: number;
9
+ dir?: any;
10
+ staleMs?: number;
11
+ }): (cmp: (a: any, b: any) => number) => {
12
+ /** @param {any} r */
13
+ push(r: any): Promise<void>;
14
+ drain(): AsyncGenerator<any, void, unknown>;
15
+ close(): Promise<void>;
16
+ /** Tests: the runs written so far. */
17
+ readonly runs: number;
18
+ };
19
+ export function canSpill(): boolean;
20
+ export function spillShared(): any;
@@ -2,4 +2,5 @@
2
2
  export function createSubsetter(loadWasm: () => Promise<ArrayBuffer | Uint8Array>): (font: Uint8Array, codePoints: Iterable<number>, opt?: {
3
3
  glyphs?: Iterable<number>;
4
4
  layout?: boolean;
5
+ weight?: number;
5
6
  }) => Promise<Uint8Array>;
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ // A million-row ledger streamed to a PDF file, with the process's peak memory: the proof that a large report's PDF
3
+ // is written with flat memory. The rows are made as they are read (an async generator standing in for a database
4
+ // cursor), so nothing holds them all.
5
+ //
6
+ // npm i @reportwright/engine @reportwright/fonts
7
+ // node node_modules/@reportwright/engine/examples/stream-1m.mjs [rows=1000000] [out=ledger.pdf] [--plain] [--grouped] [--presorted] [--tagged] [--array[=text]] [--parallel=N]
8
+ //
9
+ // Prints one JSON line: { rows, pages, streamed, peakRssMB, ms, bytes, ... }. "Page N of M" is in the page footer
10
+ // (the rows are read twice: once to count the pages); --plain leaves M out (one pass). --grouped adds two group levels
11
+ // with subtotals (sorted through a disk spill; --presorted: the rows come in group order and are only checked); --tagged
12
+ // makes an accessible PDF/A (PDF/UA-1 + PDF/A-2a); --array: the rows as one array in the definition (--array=text: as
13
+ // JSON text); --parallel=N lays out and paints with N worker threads (serial by default: each thread costs 150-300 MB).
14
+ // RW_ENGINE: import the engine from another path (a checkout: packages/engine/entry.js); RW_PUBLIC: a folder with fonts/
15
+ // and harfbuzz-subset.wasm instead of @reportwright/fonts.
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { pathToFileURL } from 'node:url';
19
+
20
+ const args = process.argv.slice(2);
21
+ const flag = (f) => args.includes(`--${f}`);
22
+ const opt = (f) => args.find(a => a.startsWith(`--${f}=`))?.split('=')[1];
23
+ const pos = args.filter(a => !a.startsWith('--'));
24
+ const N = Number(pos[0]) || 1_000_000;
25
+ const file = pos[1] || 'ledger.pdf';
26
+ const engine = await import(process.env.RW_ENGINE ? pathToFileURL(path.resolve(process.env.RW_ENGINE)).href : '@reportwright/engine');
27
+ const { exportPdfStream, FontStore, fontFile, createSubsetter } = engine;
28
+
29
+ // fonts: @reportwright/fonts (the engine finds it), or a folder of them
30
+ let fonts, subset, icc;
31
+ if (process.env.RW_PUBLIC) {
32
+ const dir = path.resolve(process.env.RW_PUBLIC);
33
+ fonts = new FontStore(async (k) => fs.promises.readFile(path.join(dir, 'fonts', fontFile(k))));
34
+ subset = createSubsetter(() => fs.promises.readFile(path.join(dir, 'harfbuzz-subset.wasm')));
35
+ icc = fs.readFileSync(path.join(dir, 'icc', 'sRGB-v2-magic.icc'));
36
+ }
37
+
38
+ const presorted = flag('presorted');
39
+ const REGIONS = ['North', 'South', 'East', 'West'], TYPES = ['Deposit', 'Withdrawal', 'Transfer', 'Fee', 'Interest'];
40
+ /** the rows, a thousand at a time, made as they are asked for */
41
+ async function* rows() {
42
+ for (let i = 0; i < N; i += 1000) {
43
+ const batch = [];
44
+ for (let j = i; j < Math.min(N, i + 1000); j++) {
45
+ // --presorted: the rows come in group order (as a query ordered by region, type gives them)
46
+ const region = presorted ? REGIONS[Math.floor(j * 4 / N)] : REGIONS[(j >> 9) % 4], type = presorted ? TYPES[Math.floor(j * 20 / N) % 5] : TYPES[(j >> 6) % 5];
47
+ batch.push({ id: j + 1, account: `AC${String(10000 + (j * 7) % 9000)}`, region, type,
48
+ date: new Date(Date.UTC(2025, j % 12, 1 + (j % 28))), amount: Math.round(((j * 7919) % 200000 - 100000) * 1.37) / 100,
49
+ reference: j % 37 === 0 ? `Reversal of an earlier transfer between linked accounts, batch ${j}` : `REF-${j * 31}` });
50
+ }
51
+ yield batch;
52
+ }
53
+ }
54
+ const grouped = flag('grouped');
55
+ const total = (label) => [{ height: 15, style: { fontWeight: 'bold' }, cells: [{ value: label }, { value: '=CountRows()' }, null, null, null, { value: '=Sum(Fields.amount)', style: { format: 'N2', textAlign: 'right' } }, null] }];
56
+ const def = {
57
+ $schema: 'pagewright/report@1', name: 'Ledger', locale: 'en-IN', page: { size: 'A4', margins: [36, 36, 36, 36] },
58
+ // the rows come from opts.sqlRows below (a cursor); the query text is not run
59
+ dataSources: [{ name: 'db', type: 'sql', connection: 'ledger', query: 'select * from transactions order by id' }],
60
+ dataSets: [{ name: 'Tx', source: 'db', fields: [{ name: 'id', type: 'number' }, { name: 'account', type: 'string' }, { name: 'region', type: 'string' }, { name: 'type', type: 'string' },
61
+ { name: 'date', type: 'date' }, { name: 'amount', type: 'number' }, { name: 'reference', type: 'string' }] }],
62
+ sections: {
63
+ pageHeader: { height: 22, items: [{ type: 'textbox', name: 'Title', x: 0, y: 0, w: 300, h: 18, value: 'Transactions ledger', style: { fontWeight: 'bold', fontSize: 12 } }] },
64
+ body: { items: [{ type: 'table', name: 'Tx', dataSet: 'Tx', x: 0, y: 0, w: 523, h: 40,
65
+ columns: [{ width: 50 }, { width: 62 }, { width: 50 }, { width: 62 }, { width: 62 }, { width: 70 }, { width: 167 }],
66
+ header: [{ height: 16, style: { fontWeight: 'bold', backgroundColor: '#eef2f7' }, cells: ['Id', 'Account', 'Region', 'Type', 'Date', 'Amount', 'Reference'].map(value => ({ value })) }],
67
+ detail: [{ height: 14, style: { borderBottom: '0.5 solid #e5e7eb' }, cells: [{ value: '=Fields.id' }, { value: '=Fields.account' }, { value: '=Fields.region' }, { value: '=Fields.type' },
68
+ { value: '=Fields.date', style: { format: 'd' } }, { value: '=Fields.amount', style: { format: 'N2', textAlign: 'right' } }, { value: '=Fields.reference', style: { fontSize: 8 } }] }],
69
+ footer: total('Total'),
70
+ ...(grouped ? { groups: [
71
+ { name: 'Region', expr: '=Fields.region', repeatHeader: true, header: [{ height: 18, style: { fontWeight: 'bold', backgroundColor: '#dbeafe' }, cells: [{ value: '="Region " & Fields.region', colSpan: 3 }, null, null] }], footer: total('Region total') },
72
+ { name: 'Type', expr: '=Fields.type', header: [{ height: 15, cells: [null, { value: '=Fields.type' }] }], footer: total('Type total') },
73
+ ] } : {}) }] },
74
+ pageFooter: { height: 18, items: [{ type: 'textbox', name: 'Page', x: 0, y: 2, w: 523, h: 14, style: { textAlign: 'right' },
75
+ value: flag('plain') ? 'Page {Globals.PageNumber}' : 'Page {Globals.PageNumber} of {Globals.TotalPages}' }] },
76
+ },
77
+ };
78
+
79
+ let peak = 0;
80
+ const rss = () => { const m = process.memoryUsage.rss() / 1048576; if (m > peak) peak = m; };
81
+ rss();
82
+ const base = peak;
83
+ const timer = setInterval(rss, 20);
84
+ const t0 = Date.now();
85
+ const out = fs.createWriteStream(file);
86
+ // --array: the rows made first, one array in the report's JSON source (how a library caller often has them)
87
+ let arrayRssMB;
88
+ if (flag('array') || opt('array')) {
89
+ const all = [];
90
+ for await (const b of rows()) for (const x of b) all.push(x);
91
+ def.dataSources = [{ name: 'db', type: 'json', data: opt('array') === 'text' ? JSON.stringify(all) : all }];
92
+ rss(); arrayRssMB = Math.round(process.memoryUsage.rss() / 1048576);
93
+ }
94
+ const r = await exportPdfStream(def, { fonts, subset, title: 'Ledger', window: Number(opt('window')) || undefined, presorted, ...(flag('memsort') ? { spill: false } : {}), sqlRows: () => rows(), maxRows: Math.max(N, 5_000_000), timeoutMs: 24 * 3600e3,
95
+ ...(flag('tagged') ? { tagged: { lang: 'en-IN' }, pdfa: icc ? { icc } : true } : {}), ...(opt('parallel') ? { parallel: Number(opt('parallel')) } : {}) }, out);
96
+ await new Promise((ok, fail) => out.end((e) => (e ? fail(e) : ok())));
97
+ clearInterval(timer);
98
+ rss();
99
+ console.log(JSON.stringify({ rows: N, pages: r.pages, streamed: r.streamed, ...(r.why ? { why: r.why } : {}), peakRssMB: Math.round(peak), startRssMB: Math.round(base), ...(arrayRssMB ? { arrayRssMB } : {}), ms: Date.now() - t0,
100
+ bytes: fs.statSync(file).size, file, node: process.version, grouped, presorted, tagged: flag('tagged'), parallel: Number(opt('parallel')) || 1 }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reportwright/engine",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -51,6 +51,7 @@
51
51
  "files": [
52
52
  "dist",
53
53
  "pool",
54
+ "examples",
54
55
  "index.d.ts",
55
56
  "report.d.ts",
56
57
  "schema.json",
@@ -70,7 +71,7 @@
70
71
  "bidi-js": "1.0.3"
71
72
  },
72
73
  "peerDependencies": {
73
- "@reportwright/fonts": "0.11.0"
74
+ "@reportwright/fonts": "0.12.1"
74
75
  },
75
76
  "peerDependenciesMeta": {
76
77
  "@reportwright/fonts": {
package/pool/worker.js CHANGED
@@ -10,7 +10,7 @@ const EXPORT = {
10
10
  xlsx: (m, o) => exportXlsx(m, { fonts, ...o }),
11
11
  docx: (m, o) => exportDocx(m, { fonts, ...o }),
12
12
  pptx: (m, o) => exportPptx(m, { fonts, ...o }),
13
- html: async (m, o) => new TextEncoder().encode(await exportHtml(m, fonts, o)),
13
+ html: async (m, o) => new TextEncoder().encode(await exportHtml(m, { ...o, fonts })),
14
14
  csv: async (m, o) => new TextEncoder().encode(exportCsv(m, o)),
15
15
  };
16
16
  /** @type {import('node:worker_threads').MessagePort} */ (parentPort).on('message', async ({ def, opts }) => {