@lpdfio/lpdf 0.32.0 → 0.33.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/dist/engine.js CHANGED
@@ -3,13 +3,44 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PdfEngine = exports.LpdfRenderError = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const node_worker_threads_1 = require("node:worker_threads");
6
8
  const _shared_1 = require("./_shared");
7
- // require() path is relative to the compiled output at dist/engine.js.
8
- // In the published package, wasm-pack artifacts live in dist/wasm/.
9
- // In the monorepo, `npm run build` copies them there from <root>/dist/node/.
10
- // eslint-disable-next-line @typescript-eslint/no-require-imports
11
- const wasmModule = require('./wasm/lpdf.js');
12
- const WasmEngine = wasmModule.LpdfEngine;
9
+ const render_error_1 = require("./render-error");
10
+ const stamp_1 = require("./stamp");
11
+ const render_queue_1 = require("./render-queue");
12
+ var render_error_2 = require("./render-error");
13
+ Object.defineProperty(exports, "LpdfRenderError", { enumerable: true, get: function () { return render_error_2.LpdfRenderError; } });
14
+ /**
15
+ * The engine's license check on this thread, loaded on first use: a render runs on the worker, which loads the engine
16
+ * itself, so an app that only renders loads it once.
17
+ */
18
+ function checkLicense(token, nowUnix) {
19
+ // require() path is relative to the compiled output at dist/engine.js.
20
+ // In the published package, wasm-pack artifacts live in dist/wasm/.
21
+ // In the monorepo, `npm run build` copies them there from <root>/dist/node/.
22
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
23
+ const wasm = require('./wasm/lpdf.js');
24
+ return wasm.check_license(token, nowUnix);
25
+ }
26
+ /**
27
+ * Starts the worker of `render-worker.js`. It keeps the process running only while it renders, so a script that has
28
+ * rendered ends by itself.
29
+ */
30
+ function startNodeWorker(events) {
31
+ const worker = new node_worker_threads_1.Worker((0, node_path_1.join)(__dirname, 'render-worker.js'));
32
+ worker.on('message', (reply) => events.replied(reply));
33
+ worker.on('error', error => events.failed(error.message));
34
+ worker.on('messageerror', error => events.failed(error.message));
35
+ worker.on('exit', code => events.failed(`The render worker stopped with exit code ${code}.`));
36
+ return {
37
+ post: job => worker.postMessage(job),
38
+ terminate: () => void worker.terminate(),
39
+ hold: busy => (busy ? worker.ref() : worker.unref()),
40
+ };
41
+ }
42
+ // One worker for the process, as renders ran one at a time on the calling thread before they moved to a worker.
43
+ let renderQueue;
13
44
  /**
14
45
  * The clock the engine checks a key's expiry against.
15
46
  *
@@ -22,20 +53,19 @@ const WasmEngine = wasmModule.LpdfEngine;
22
53
  function nowUnix() {
23
54
  return BigInt(Math.floor(Date.now() / 1000));
24
55
  }
25
- /** Thrown when the lpdf engine returns a layout or parse error. */
26
- class LpdfRenderError extends Error {
27
- constructor(message) {
28
- super(message);
29
- this.name = 'LpdfRenderError';
30
- }
31
- }
32
- exports.LpdfRenderError = LpdfRenderError;
33
56
  /** Node's secure generator, for the entropy of AES-256. */
34
57
  function secureRandomBytes(count) {
35
58
  return new Uint8Array((0, node_crypto_1.randomBytes)(count));
36
59
  }
37
60
  class PdfEngine {
38
- constructor() {
61
+ /**
62
+ * @param options - Engine-level options: `timeout`, the seconds a render may run before it rejects with an
63
+ * `LpdfRenderError` that names the limit, 5 when left out and no limit at 0; `maxMemoryMb`, the megabytes a render
64
+ * may allocate before it rejects with an `LpdfRenderError` that names the cap, 1024 when left out.
65
+ * @throws {RangeError} when `timeout` is negative or not a finite number, or `maxMemoryMb` is not a whole number
66
+ * from 1 to 4096.
67
+ */
68
+ constructor(options = {}) {
39
69
  this._licenseKey = '';
40
70
  this._fonts = new Map();
41
71
  this._images = new Map();
@@ -43,6 +73,8 @@ class PdfEngine {
43
73
  this._profiles = new Map();
44
74
  this._markdownStyles = new Map();
45
75
  this._disposed = false;
76
+ this._timeout = (0, render_queue_1.renderTimeout)(options.timeout);
77
+ this._maxMemoryMb = (0, _shared_1.maxMemoryMb)(options.maxMemoryMb);
46
78
  }
47
79
  /**
48
80
  * Set the license key. Returns `this` for chaining.
@@ -65,7 +97,7 @@ class PdfEngine {
65
97
  */
66
98
  checkLicenseKey(key) {
67
99
  this._throwIfDisposed();
68
- return JSON.parse(wasmModule.check_license(key ?? this._licenseKey, nowUnix()));
100
+ return JSON.parse(checkLicense(key ?? this._licenseKey, nowUnix()));
69
101
  }
70
102
  /**
71
103
  * Register raw TTF/OTF bytes for a custom font name used in `<font src="…">`.
@@ -122,10 +154,10 @@ class PdfEngine {
122
154
  }
123
155
  [Symbol.dispose]() { this.dispose(); }
124
156
  /**
125
- * Hand the engine the files of Markdown rules that a document names: the ones loaded here, and the ones read from
126
- * their `src`. A file that can't be read is left for the engine to report, naming the `src`.
157
+ * The files of Markdown rules that a document names: the ones loaded here, and the ones read from their `src`. A
158
+ * file that can't be read is left for the engine to report, naming the `src`.
127
159
  */
128
- _loadMarkdownStyles(engine, srcs) {
160
+ _markdownStylesFor(srcs) {
129
161
  const files = new Map(this._markdownStyles);
130
162
  for (const src of srcs) {
131
163
  if (!files.has(src)) {
@@ -135,15 +167,13 @@ class PdfEngine {
135
167
  catch { /* not found; the engine says so */ }
136
168
  }
137
169
  }
138
- for (const [src, text] of files) {
139
- engine.load_markdown_styles(src, text);
140
- }
170
+ return files;
141
171
  }
142
172
  /**
143
- * Hand the engine the files the document carries: the ones loaded here, and the ones read from the `src` of a file
144
- * that wasn't. A `src` that can't be read is an error that names the file and the path.
173
+ * The files the document carries: the ones loaded here, and the ones read from the `src` of a file that wasn't. A
174
+ * `src` that can't be read is an error that names the file and the path.
145
175
  */
146
- _loadFiles(engine, srcs) {
176
+ _filesFor(srcs) {
147
177
  const files = new Map(this._files);
148
178
  for (const [key, src] of srcs) {
149
179
  if (files.has(key))
@@ -152,18 +182,16 @@ class PdfEngine {
152
182
  files.set(key, (0, node_fs_1.readFileSync)(src));
153
183
  }
154
184
  catch (e) {
155
- throw new LpdfRenderError(`file '${key}' of <attachments>: cannot read its src '${src}': ${e instanceof Error ? e.message : String(e)}`);
185
+ throw new render_error_1.LpdfRenderError(`file '${key}' of <attachments>: cannot read its src '${src}': ${e instanceof Error ? e.message : String(e)}`);
156
186
  }
157
187
  }
158
- for (const [key, bytes] of files) {
159
- engine.load_file(key, bytes);
160
- }
188
+ return files;
161
189
  }
162
190
  /**
163
- * Hand the engine the color profiles the document declares: the ones loaded here, and the ones read from the `src`
164
- * of a profile that wasn't. A `src` that can't be read is an error that names the profile and the path.
191
+ * The color profiles the document declares: the ones loaded here, and the ones read from the `src` of a profile
192
+ * that wasn't. A `src` that can't be read is an error that names the profile and the path.
165
193
  */
166
- _loadProfiles(engine, srcs) {
194
+ _profilesFor(srcs) {
167
195
  const profiles = new Map(this._profiles);
168
196
  for (const [key, src] of srcs) {
169
197
  if (profiles.has(key))
@@ -172,12 +200,10 @@ class PdfEngine {
172
200
  profiles.set(key, (0, node_fs_1.readFileSync)(src));
173
201
  }
174
202
  catch (e) {
175
- throw new LpdfRenderError(`profile '${key}' of <assets>: cannot read its src '${src}': ${e instanceof Error ? e.message : String(e)}`);
203
+ throw new render_error_1.LpdfRenderError(`profile '${key}' of <assets>: cannot read its src '${src}': ${e instanceof Error ? e.message : String(e)}`);
176
204
  }
177
205
  }
178
- for (const [key, bytes] of profiles) {
179
- engine.load_profile(key, bytes);
180
- }
206
+ return profiles;
181
207
  }
182
208
  _throwIfDisposed() {
183
209
  if (this._disposed)
@@ -185,91 +211,115 @@ class PdfEngine {
185
211
  }
186
212
  async render(input, callOptions = {}) {
187
213
  this._throwIfDisposed();
188
- const engine = new WasmEngine(this._licenseKey);
189
- // Without a clock the engine cannot check the key's expiry, and an expired key renders as
190
- // though it were current.
191
- engine.set_now(nowUnix());
192
- if (callOptions.createdOn) {
193
- engine.set_created_on(callOptions.createdOn);
194
- }
195
- let pdf;
196
- try {
197
- if (typeof input === 'string') {
198
- // XML path — auto-load fonts and images declared via src="…".
199
- const xml = input;
200
- const allFonts = new Map(this._fonts);
201
- for (const [key, src] of extractAssetSrcs(xml, 'font')) {
202
- if (!allFonts.has(key)) {
203
- try {
204
- allFonts.set(key, (0, node_fs_1.readFileSync)(src));
205
- }
206
- catch { /* not found; Rust falls back to Helvetica */ }
214
+ const job = this._job(input, callOptions);
215
+ renderQueue ?? (renderQueue = new render_queue_1.RenderQueue(startNodeWorker));
216
+ return renderQueue.render(job, this._timeout);
217
+ }
218
+ /**
219
+ * Draw PNG images over the pages of an existing PDF, and return the PDF with them (Node.js). Nothing is laid out:
220
+ * each image is drawn in its stamp's box, over the pages its `page` names.
221
+ *
222
+ * The output is the input, byte for byte, with an incremental update after it, so an owner password and its
223
+ * permissions stay, and so does a digital signature, for the revision it signed. A PDF/A file stays PDF/A. Without a
224
+ * license key each stamped page gets the attribution line in its top-right corner. A stamp covers what is under it
225
+ * and does not remove it: the text under an image can still be selected and found, so a stamp is not a redaction.
226
+ *
227
+ * The stamp runs on a worker thread, as a render does, with its `timeout` and `maxMemoryMb`. It rejects with an
228
+ * `LpdfRenderError` when the input is not a PDF, needs a password to open, or doesn't have a page a stamp names, and
229
+ * when an image is not a PNG or a stamp's options are wrong.
230
+ *
231
+ * @throws {TypeError} when a stamp's `image` is not the PNG's bytes.
232
+ */
233
+ async stamp(pdf, stamps) {
234
+ this._throwIfDisposed();
235
+ const { stamps: json, images } = (0, stamp_1.stampRequest)(stamps);
236
+ const job = {
237
+ licenseKey: this._licenseKey,
238
+ now: nowUnix(),
239
+ maxMemoryMb: this._maxMemoryMb,
240
+ createdOn: undefined,
241
+ fonts: new Map(),
242
+ images,
243
+ files: new Map(),
244
+ profiles: new Map(),
245
+ markdownStyles: new Map(),
246
+ document: { pdf, stamps: json },
247
+ encryption: [null, null, false, new Uint8Array()],
248
+ };
249
+ renderQueue ?? (renderQueue = new render_queue_1.RenderQueue(startNodeWorker));
250
+ return renderQueue.render(job, this._timeout);
251
+ }
252
+ /**
253
+ * A render of `input` with every file it needs, read here from the `src` of each font, image, file, profile and
254
+ * file of rules the document names and that wasn't loaded: the worker reads nothing itself.
255
+ */
256
+ _job(input, callOptions) {
257
+ const fonts = new Map(this._fonts);
258
+ const images = new Map(this._images);
259
+ let document;
260
+ let markdownStyles;
261
+ let files;
262
+ let profiles;
263
+ if (typeof input === 'string') {
264
+ // XML path — auto-load fonts and images declared via src="…".
265
+ for (const [key, src] of extractAssetSrcs(input, 'font')) {
266
+ if (!fonts.has(key)) {
267
+ try {
268
+ fonts.set(key, (0, node_fs_1.readFileSync)(src));
207
269
  }
270
+ catch { /* not found; Rust falls back to Helvetica */ }
208
271
  }
209
- for (const [name, bytes] of allFonts) {
210
- engine.load_font(name, bytes);
211
- }
212
- for (const [name, bytes] of this._images) {
213
- engine.load_image(name, bytes);
214
- }
215
- this._loadMarkdownStyles(engine, markdownStylesSrcs(xml));
216
- this._loadFiles(engine, extractAssetSrcs(xml, 'file'));
217
- this._loadProfiles(engine, extractAssetSrcs(xml, 'profile'));
218
- // Auto-load images declared via <image src="…"> that weren't pre-loaded.
219
- for (const [key, src] of extractAssetSrcs(xml, 'image')) {
220
- if (!this._images.has(key)) {
221
- try {
222
- engine.load_image(key, (0, node_fs_1.readFileSync)(src));
223
- }
224
- catch { /* skip unresolvable image */ }
272
+ }
273
+ for (const [key, src] of extractAssetSrcs(input, 'image')) {
274
+ if (!images.has(key)) {
275
+ try {
276
+ images.set(key, (0, node_fs_1.readFileSync)(src));
225
277
  }
278
+ catch { /* skip unresolvable image */ }
226
279
  }
227
- const dataJson = callOptions.data != null ? JSON.stringify(callOptions.data) : null;
228
- pdf = engine.render_pdf(xml, dataJson, ...(0, _shared_1.encryptionArguments)(callOptions, secureRandomBytes));
229
280
  }
230
- else {
231
- // JSON (Kit tree) path — pass JSON directly to render_tree_pdf. Fonts and images the
232
- // document declares with a src are read from there, as on the XML path.
233
- const json = JSON.stringify(input);
234
- const allFonts = new Map(this._fonts);
235
- for (const [key, src] of assetSrcsOfDocument(input, 'fonts')) {
236
- if (!allFonts.has(key)) {
237
- try {
238
- allFonts.set(key, (0, node_fs_1.readFileSync)(src));
239
- }
240
- catch { /* not found; Rust falls back to Helvetica */ }
281
+ markdownStyles = this._markdownStylesFor(markdownStylesSrcs(input));
282
+ files = this._filesFor(extractAssetSrcs(input, 'file'));
283
+ profiles = this._profilesFor(extractAssetSrcs(input, 'profile'));
284
+ document = { xml: input, data: callOptions.data != null ? JSON.stringify(callOptions.data) : null };
285
+ }
286
+ else {
287
+ // JSON (Kit tree) path. Fonts and images the document declares with a src are read from there, as on the XML
288
+ // path.
289
+ for (const [key, src] of assetSrcsOfDocument(input, 'fonts')) {
290
+ if (!fonts.has(key)) {
291
+ try {
292
+ fonts.set(key, (0, node_fs_1.readFileSync)(src));
241
293
  }
294
+ catch { /* not found; Rust falls back to Helvetica */ }
242
295
  }
243
- for (const [name, bytes] of allFonts) {
244
- engine.load_font(name, bytes);
245
- }
246
- const allImages = new Map(this._images);
247
- for (const [key, src] of assetSrcsOfDocument(input, 'images')) {
248
- if (!allImages.has(key)) {
249
- try {
250
- allImages.set(key, (0, node_fs_1.readFileSync)(src));
251
- }
252
- catch { /* skip unresolvable image */ }
296
+ }
297
+ for (const [key, src] of assetSrcsOfDocument(input, 'images')) {
298
+ if (!images.has(key)) {
299
+ try {
300
+ images.set(key, (0, node_fs_1.readFileSync)(src));
253
301
  }
302
+ catch { /* skip unresolvable image */ }
254
303
  }
255
- for (const [name, bytes] of allImages) {
256
- engine.load_image(name, bytes);
257
- }
258
- this._loadMarkdownStyles(engine, markdownStylesSrcsOfDocument(input));
259
- this._loadFiles(engine, fileSrcsOfDocument(input));
260
- this._loadProfiles(engine, assetSrcsOfDocument(input, 'profiles'));
261
- pdf = engine.render_tree_pdf(json, ...(0, _shared_1.encryptionArguments)(callOptions, secureRandomBytes));
262
304
  }
305
+ markdownStyles = this._markdownStylesFor(markdownStylesSrcsOfDocument(input));
306
+ files = this._filesFor(fileSrcsOfDocument(input));
307
+ profiles = this._profilesFor(assetSrcsOfDocument(input, 'profiles'));
308
+ document = { tree: JSON.stringify(input) };
263
309
  }
264
- catch (e) {
265
- engine.free();
266
- if (e instanceof LpdfRenderError)
267
- throw e;
268
- const msg = e instanceof Error ? e.message : String(e);
269
- throw new LpdfRenderError(msg);
270
- }
271
- engine.free();
272
- return pdf;
310
+ return {
311
+ licenseKey: this._licenseKey,
312
+ now: nowUnix(),
313
+ maxMemoryMb: this._maxMemoryMb,
314
+ createdOn: callOptions.createdOn,
315
+ fonts,
316
+ images,
317
+ files,
318
+ profiles,
319
+ markdownStyles,
320
+ document,
321
+ encryption: (0, _shared_1.encryptionArguments)(callOptions, secureRandomBytes),
322
+ };
273
323
  }
274
324
  }
275
325
  exports.PdfEngine = PdfEngine;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export { PdfEngine, LpdfRenderError } from './engine';
2
2
  export type { RenderOptions, EngineOptions } from './_shared';
3
- export { FieldType, Pin, Orientation, SectionType, FootnoteCounter, Cipher, Permission, FileRelationship, BuiltinFont, PageScope, } from './constants';
3
+ export type { StampAttr } from './stamp';
4
+ export { FieldType, Pin, Orientation, SectionType, FootnoteCounter, ColumnFill, Cipher, Permission, FileRelationship, BuiltinFont, PageScope, } from './constants';
4
5
  export type { PdfDocument, LpdfSectionNode, LpdfSectionChild, LpdfLayoutBlock, DocumentTokens, TokenSlot, TokenRow, DocumentAssets, DocumentMeta, DocumentOutput, SectionAttr, DocumentAttr, } from './kit';
5
- export type { ColumnAttr, RowAttr, GridAttr, ListAttr, LiAttr, FrameAttr, LinkAttr, TableAttr, TheadAttr, TrAttr, TdAttr, TextAttr, CodeAttr, SpanAttr, CodeSpanAttr, DividerAttr, ImgAttr, ShapeAttr, BarcodeAttr, RegionAttr, FieldAttr, BookmarkAttr, LayerAttr, RectAttr, CircleAttr, EllipseAttr, LineAttr, PathAttr, TextAtAttr, ImgAtAttr, BarcodeAtAttr, FontAttr, ImageAttr, FileAttr, ProfileAttr, LayoutAttr, GradientToken, GradientStop, MarkdownAttr, MarkdownStylesAttr, MarkdownH1Attr, MarkdownH2Attr, MarkdownH3Attr, MarkdownH4Attr, MarkdownH5Attr, MarkdownH6Attr, MarkdownPAttr, MarkdownCodeAttr, MarkdownQuoteAttr, MarkdownLiAttr, MarkdownAAttr, MarkdownStrongAttr, MarkdownEmAttr, MarkdownDelAttr, MarkdownTableAttr, MarkdownThAttr, MarkdownTdAttr, } from './attrs';
6
+ export type { ColumnAttr, RowAttr, GridAttr, ListAttr, LiAttr, FrameAttr, LinkAttr, TableAttr, TheadAttr, TrAttr, TdAttr, TextAttr, CodeAttr, SpanAttr, CodeSpanAttr, DividerAttr, ImgAttr, ShapeAttr, BarcodeAttr, RegionAttr, FieldAttr, BookmarkAttr, LayerAttr, RectAttr, CircleAttr, EllipseAttr, LineAttr, PathAttr, TextAtAttr, ImgAtAttr, BarcodeAtAttr, FontAttr, ImageAttr, FileAttr, ProfileAttr, LayoutAttr, GradientToken, GradientStop, RulingToken, RulingLines, MarkdownAttr, MarkdownStylesAttr, MarkdownH1Attr, MarkdownH2Attr, MarkdownH3Attr, MarkdownH4Attr, MarkdownH5Attr, MarkdownH6Attr, MarkdownPAttr, MarkdownCodeAttr, MarkdownQuoteAttr, MarkdownLiAttr, MarkdownAAttr, MarkdownStrongAttr, MarkdownEmAttr, MarkdownDelAttr, MarkdownTableAttr, MarkdownThAttr, MarkdownTdAttr, } from './attrs';
6
7
  export type { LpdfNode, LpdfContainerNode, LpdfListNode, LpdfLiNode, LpdfTextNode, LpdfCodeNode, LpdfSpanNode, LpdfCodeSpanNode, LpdfFnNode, LpdfDividerNode, LpdfImgNode, LpdfShapeNode, LpdfBarcodeNode, LpdfTableNode, LpdfTheadNode, LpdfTrNode, LpdfTdNode, LpdfRegionNode, LpdfFieldNode, LpdfBookmarkNode, } from './layout';
7
8
  export { LMarkdown } from './markdown';
8
9
  export type { LpdfMarkdownNode, LpdfMarkdownStylesNode, LpdfMarkdownContentNode, LpdfMarkdownRuleNode, } from './markdown';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NoAttr = exports.L = exports.CanvasTransform = exports.LMarkdown = exports.PageScope = exports.BuiltinFont = exports.FileRelationship = exports.Permission = exports.Cipher = exports.FootnoteCounter = exports.SectionType = exports.Orientation = exports.Pin = exports.FieldType = exports.LpdfRenderError = exports.PdfEngine = void 0;
3
+ exports.NoAttr = exports.L = exports.CanvasTransform = exports.LMarkdown = exports.PageScope = exports.BuiltinFont = exports.FileRelationship = exports.Permission = exports.Cipher = exports.ColumnFill = exports.FootnoteCounter = exports.SectionType = exports.Orientation = exports.Pin = exports.FieldType = exports.LpdfRenderError = exports.PdfEngine = void 0;
4
4
  // ── Engine ────────────────────────────────────────────────────────────────────
5
5
  var engine_1 = require("./engine");
6
6
  Object.defineProperty(exports, "PdfEngine", { enumerable: true, get: function () { return engine_1.PdfEngine; } });
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "Pin", { enumerable: true, get: function () { ret
12
12
  Object.defineProperty(exports, "Orientation", { enumerable: true, get: function () { return constants_1.Orientation; } });
13
13
  Object.defineProperty(exports, "SectionType", { enumerable: true, get: function () { return constants_1.SectionType; } });
14
14
  Object.defineProperty(exports, "FootnoteCounter", { enumerable: true, get: function () { return constants_1.FootnoteCounter; } });
15
+ Object.defineProperty(exports, "ColumnFill", { enumerable: true, get: function () { return constants_1.ColumnFill; } });
15
16
  Object.defineProperty(exports, "Cipher", { enumerable: true, get: function () { return constants_1.Cipher; } });
16
17
  Object.defineProperty(exports, "Permission", { enumerable: true, get: function () { return constants_1.Permission; } });
17
18
  Object.defineProperty(exports, "FileRelationship", { enumerable: true, get: function () { return constants_1.FileRelationship; } });
@@ -19,9 +19,6 @@
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.kitToXml = kitToXml;
22
- // Load the standalone kit_to_xml export from the compiled WASM module.
23
- // eslint-disable-next-line @typescript-eslint/no-require-imports
24
- const { kit_to_xml: wasmKitToXml } = require('./wasm/lpdf.js');
25
22
  /**
26
23
  * Convert an `LpdfDocument` tree (built with `LpdfKit`) into an lpdf XML
27
24
  * string.
@@ -33,5 +30,9 @@ const { kit_to_xml: wasmKitToXml } = require('./wasm/lpdf.js');
33
30
  * @returns A well-formed XML string with an `<?xml ...?>` declaration.
34
31
  */
35
32
  function kitToXml(doc) {
36
- return wasmKitToXml(JSON.stringify(doc));
33
+ // The engine is loaded on this thread on first use: a render runs on a worker, which loads it itself, so an app that
34
+ // only renders loads it once.
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
36
+ const wasm = require('./wasm/lpdf.js');
37
+ return wasm.kit_to_xml(JSON.stringify(doc));
37
38
  }
package/dist/kit.d.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  import type { LpdfBookmarkNode, LpdfNode } from './layout';
23
23
  import type { LpdfCanvasLayerNode } from './canvas';
24
24
  import type { LpdfMarkdownNode } from './markdown';
25
- import type { FileAttr, FontAttr, GradientToken, ImageAttr, LayoutAttr, ProfileAttr } from './attrs';
25
+ import type { FileAttr, FontAttr, GradientToken, ImageAttr, LayoutAttr, ProfileAttr, RulingToken } from './attrs';
26
26
  import { type AttrValue } from './_shared';
27
27
  /** A slot of a token row, from the smallest to the largest. */
28
28
  export type TokenSlot = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl';
@@ -31,6 +31,11 @@ export type TokenRow<T> = Partial<Record<TokenSlot, T>>;
31
31
  export interface DocumentTokens {
32
32
  colors?: Record<string, string>;
33
33
  gradients?: Record<string, GradientToken>;
34
+ /**
35
+ * Lines a section draws on every page with `ruling: 'name'`, by name: `{ graph: { origin: 'margin', lines: [{ step:
36
+ * '5mm' }, { step: '25mm', color: '#b5c4d6' }] } }`. Each set of lines is cut by a step or a count, in x, in y or both.
37
+ */
38
+ rulings?: Record<string, RulingToken>;
34
39
  /**
35
40
  * Names for the slots of the scale, each to the slot it stands for: `{ caption: 'xs' }` lets `fontSize: 'caption'`
36
41
  * mean the text size `xs`, and `gap: 'caption'` the space `xs`. A name is lowercase letters, digits and `-`,
@@ -68,7 +73,10 @@ export interface DocumentMeta {
68
73
  subject?: string;
69
74
  keywords?: string;
70
75
  creator?: string;
71
- /** The language the document is written in, as a language tag such as `en-US`. A PDF/UA document needs it. */
76
+ /**
77
+ * The language the document is written in, as a language tag such as `en-US`. A PDF/UA document needs it, and a
78
+ * PDF/A document of level a takes it only as a language tag.
79
+ */
72
80
  lang?: string;
73
81
  }
74
82
  /**
@@ -86,8 +94,14 @@ export interface DocumentOutput {
86
94
  allow?: string;
87
95
  /**
88
96
  * Writes the PDF as PDF/A, the standard for files kept for the long term: `2b`, or `2u` when its text must also
89
- * be searchable. The render needs `createdOn`, and every text a font of `assets`. What PDF/A cannot hold is an
90
- * error that says what to change: a built-in font, a CMYK color, a form field, a barcode's digits, `encrypt`.
97
+ * be searchable, or `2a` when it must also be tagged, and `3a`, `3b` or `3u` to carry attachments of any kind. The
98
+ * render needs `createdOn`, and every text a font of `assets`. Level a tags the file as `pdfua` does, without
99
+ * PDF/UA's checks: add `pdfua` for a file a screen reader can use. What PDF/A cannot hold is an error that says what
100
+ * to change: a built-in font, a CMYK color, a form field, a barcode's digits, `encrypt`, attachments in part 2, and
101
+ * in level a a `lang` that is not a language tag and a character of Unicode's private use area. With `3a`, `3b` or
102
+ * `3u`, an attachment named `factur-x.xml`, or `xrechnung.xml` for the XRECHNUNG profile, makes the file a Factur-X
103
+ * or ZUGFeRD e-invoice: the engine reads the profile from BT-24 of that XML and names the invoice in the file's
104
+ * metadata. The XML is attached as given, and its content is not checked.
91
105
  */
92
106
  pdfa?: string;
93
107
  /**
@@ -113,6 +127,14 @@ export interface DocumentOutput {
113
127
  * and linearized last. Any change to the file afterwards, such as a stamp, ends the linearization.
114
128
  */
115
129
  linearize?: boolean;
130
+ /**
131
+ * Refuses, in a Factur-X e-invoice, the attachments the Factur-X spec does not allow: an invoice file whose `mime`
132
+ * is not `text/xml`, an invoice file whose `relationship` its profile does not allow, and another file with no
133
+ * `mime` or one that is not of a format the spec lists (PDF, TXT, GIF, TIFF, JPG, CSV, XML, JSON, XLS, XLSX, ODS).
134
+ * Without it, every file is written as the document gives it. It does nothing in a document without
135
+ * `factur-x.xml` or `xrechnung.xml`. It is `facturx-strict` in the XML.
136
+ */
137
+ facturxStrict?: boolean;
116
138
  }
117
139
  export interface SectionAttr {
118
140
  size?: string;
@@ -137,6 +159,16 @@ export interface SectionAttr {
137
159
  pageFormat?: string;
138
160
  /** Text before the section's page numbers, such as `A-`. Without it the section keeps the prefix before; `''` takes it away. */
139
161
  pagePrefix?: string;
162
+ /** Sets the section's flow in this many columns, 1 to 12, each filled like a small page, left to right. */
163
+ columns?: number;
164
+ /** The space between two columns, a space token or a length; `l` by default. */
165
+ columnGap?: string;
166
+ /** A line in the middle of each gap between two columns, as a border, such as `0.5pt #ccc`. */
167
+ columnRule?: string;
168
+ /** How the last page's columns fill: `ColumnFill.Auto`, in order, the default, or `ColumnFill.Balance`, levelled. */
169
+ columnFill?: string;
170
+ /** The name of a ruling of the tokens, whose lines are drawn on every page of the section, under everything else. */
171
+ ruling?: string;
140
172
  }
141
173
  export interface DocumentAttr {
142
174
  size?: string;
package/dist/kit.js CHANGED
@@ -34,6 +34,13 @@ function section(attrs, nodes = []) {
34
34
  nodes,
35
35
  };
36
36
  }
37
+ /**
38
+ * The rulings of the tokens as the document tree takes them: a set of lines names its attributes as an element does,
39
+ * so `stepX` is `step-x`.
40
+ */
41
+ function rulingsTree(rulings) {
42
+ return Object.fromEntries(Object.entries(rulings).map(([name, { lines, ...ruling }]) => [name, { ...ruling, lines: (lines ?? []).map((set) => (0, _shared_1.buildAttrs)(set)) }]));
43
+ }
37
44
  function document(attrs, nodes = []) {
38
45
  const { assets, tokens, meta, output, attachments, ...restOpts } = attrs ?? {};
39
46
  const attrsObj = {
@@ -44,17 +51,19 @@ function document(attrs, nodes = []) {
44
51
  if (tokens !== undefined) {
45
52
  // A factor of the line-height row and a gradient's angle are numbers, which JSON can't hold as NaN or infinity.
46
53
  (0, _shared_1.assertFiniteIn)('tokens', tokens);
47
- const { textSize, lineHeight, ...rest } = tokens;
54
+ const { textSize, lineHeight, rulings, ...rest } = tokens;
48
55
  attrsObj['tokens'] = {
49
56
  ...rest,
50
57
  ...(textSize !== undefined ? { 'text-size': textSize } : {}),
51
58
  ...(lineHeight !== undefined ? { 'line-height': lineHeight } : {}),
59
+ ...(rulings !== undefined ? { rulings: rulingsTree(rulings) } : {}),
52
60
  };
53
61
  }
54
62
  if (meta !== undefined)
55
63
  attrsObj['meta'] = meta;
64
+ // The engine reads the schema's names: `facturxStrict` is `facturx-strict`.
56
65
  if (output !== undefined)
57
- attrsObj['output'] = output;
66
+ attrsObj['output'] = (0, _shared_1.buildAttrs)(output);
58
67
  if (attachments !== undefined)
59
68
  attrsObj['attachments'] = attachments;
60
69
  return {
@@ -0,0 +1,11 @@
1
+ /** What a render that crashes the engine rejects with, the same in every SDK of Lpdf. */
2
+ export declare const ENGINE_CRASHED: string;
3
+ /**
4
+ * Thrown when the lpdf engine returns a layout or parse error, a render runs past its timeout or its memory cap, or
5
+ * the engine crashes.
6
+ */
7
+ export declare class LpdfRenderError extends Error {
8
+ /** For an engine that crashed, the error the engine itself gave, such as `RuntimeError: unreachable`. */
9
+ readonly cause?: string;
10
+ constructor(message: string, cause?: string);
11
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LpdfRenderError = exports.ENGINE_CRASHED = void 0;
4
+ /** What a render that crashes the engine rejects with, the same in every SDK of Lpdf. */
5
+ exports.ENGINE_CRASHED = 'The engine crashed during the render. This is a bug in Lpdf, or the render ran out of memory before its cap ' +
6
+ 'stopped it: please report it with the document at https://github.com/lpdfio/lpdf/issues.';
7
+ /**
8
+ * Thrown when the lpdf engine returns a layout or parse error, a render runs past its timeout or its memory cap, or
9
+ * the engine crashes.
10
+ */
11
+ class LpdfRenderError extends Error {
12
+ constructor(message, cause) {
13
+ super(message);
14
+ this.name = 'LpdfRenderError';
15
+ if (cause !== undefined)
16
+ this.cause = cause;
17
+ }
18
+ }
19
+ exports.LpdfRenderError = LpdfRenderError;
@@ -0,0 +1,67 @@
1
+ /** The engine's render calls, as wasm-bindgen exposes them in both builds. */
2
+ export interface WasmRenderEngine {
3
+ render_pdf(xml: string, json_data: string | null, user_password: string | null, owner_password: string | null, skip_encryption: boolean, entropy: Uint8Array): Uint8Array;
4
+ render_tree_pdf(json: string, user_password: string | null, owner_password: string | null, skip_encryption: boolean, entropy: Uint8Array): Uint8Array;
5
+ stamp(pdf: Uint8Array, stamps: string): Uint8Array;
6
+ load_font(name: string, bytes: Uint8Array): void;
7
+ load_image(name: string, bytes: Uint8Array): void;
8
+ load_file(name: string, bytes: Uint8Array): void;
9
+ load_profile(name: string, bytes: Uint8Array): void;
10
+ load_markdown_styles(src: string, xml: string): void;
11
+ set_created_on(iso: string): void;
12
+ set_now(unix: bigint): void;
13
+ set_max_memory_mb(mb: number): void;
14
+ free(): void;
15
+ }
16
+ /** The user password, the owner password, `skipEncryption` and the entropy, in the order the render calls take them. */
17
+ export type EncryptionArguments = [string | null, string | null, boolean, Uint8Array];
18
+ /**
19
+ * One render: the document and every file it needs, already read, since a worker reads nothing itself. A stamp runs as
20
+ * a render does, with the PDF and the JSON of its stamps as its document and their PNGs as its images.
21
+ */
22
+ export interface RenderJob {
23
+ licenseKey: string;
24
+ /** The host's clock in Unix seconds, for the key's expiry. */
25
+ now: bigint;
26
+ /** The megabytes the render may allocate. */
27
+ maxMemoryMb: number;
28
+ createdOn: string | undefined;
29
+ fonts: Map<string, Uint8Array>;
30
+ images: Map<string, Uint8Array>;
31
+ files: Map<string, Uint8Array>;
32
+ profiles: Map<string, Uint8Array>;
33
+ markdownStyles: Map<string, string>;
34
+ /** An XML document with its data as JSON, a document tree as JSON, or a PDF to stamp. */
35
+ document: {
36
+ xml: string;
37
+ data: string | null;
38
+ } | {
39
+ tree: string;
40
+ } | {
41
+ pdf: Uint8Array;
42
+ stamps: string;
43
+ };
44
+ encryption: EncryptionArguments;
45
+ }
46
+ /**
47
+ * What the worker sends back: the PDF, or the engine's error. `broken` says that the engine trapped, after which its
48
+ * memory can't be trusted, so the worker is replaced before the next render. A crash of the engine has the error every
49
+ * SDK gives for one, and the engine's own as its `cause`.
50
+ */
51
+ export type RenderReply = {
52
+ pdf: Uint8Array;
53
+ } | {
54
+ error: string;
55
+ broken: boolean;
56
+ cause?: string;
57
+ };
58
+ /**
59
+ * Runs a render on an engine of its own, which is freed after it, and returns the PDF or the error. It never throws: a
60
+ * failure is the reply.
61
+ */
62
+ export declare function runRenderJob(createEngine: (licenseKey: string) => WasmRenderEngine, job: RenderJob): RenderReply;
63
+ /**
64
+ * The buffers a reply moves to the other thread instead of copying: the PDF's, when the PDF has a buffer of its own.
65
+ * A view into a larger buffer, such as the engine's memory, is copied.
66
+ */
67
+ export declare function transferList(reply: RenderReply): ArrayBuffer[];