@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.
- package/CHANGELOG.md +81 -0
- package/README.md +82 -2
- package/dist/index.js +4476 -1786
- package/dist/pdfstreamworker.js +38 -0
- package/dist/types/packages/engine/entry.d.ts +64 -0
- package/dist/types/src/engine/data/guard.d.ts +4 -1
- package/dist/types/src/engine/data/stream.d.ts +48 -0
- package/dist/types/src/engine/index.d.ts +16 -1
- package/dist/types/src/engine/items/chart-kit.d.ts +6 -0
- package/dist/types/src/engine/paged.d.ts +19 -0
- package/dist/types/src/engine/stream.d.ts +56 -0
- package/dist/types/src/engine/text/measure.d.ts +2 -0
- package/dist/types/src/exporters/docx.d.ts +5 -1
- package/dist/types/src/exporters/html.d.ts +9 -5
- package/dist/types/src/exporters/pdfa.d.ts +11 -0
- package/dist/types/src/exporters/pdfpaint.d.ts +99 -0
- package/dist/types/src/exporters/pdfstream.d.ts +199 -0
- package/dist/types/src/exporters/pdfstreamtags.d.ts +45 -0
- package/dist/types/src/exporters/spill.d.ts +20 -0
- package/dist/types/src/exporters/subset.d.ts +1 -0
- package/examples/stream-1m.mjs +100 -0
- package/package.json +3 -2
- package/pool/worker.js +1 -1
- package/dist/index.js.map +0 -6
|
@@ -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;
|
|
@@ -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.
|
|
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.
|
|
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,
|
|
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 }) => {
|