@lpdfio/lpdf 0.31.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,26 +53,28 @@ 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();
42
72
  this._files = new Map();
73
+ this._profiles = new Map();
43
74
  this._markdownStyles = new Map();
44
75
  this._disposed = false;
76
+ this._timeout = (0, render_queue_1.renderTimeout)(options.timeout);
77
+ this._maxMemoryMb = (0, _shared_1.maxMemoryMb)(options.maxMemoryMb);
45
78
  }
46
79
  /**
47
80
  * Set the license key. Returns `this` for chaining.
@@ -64,7 +97,7 @@ class PdfEngine {
64
97
  */
65
98
  checkLicenseKey(key) {
66
99
  this._throwIfDisposed();
67
- return JSON.parse(wasmModule.check_license(key ?? this._licenseKey, nowUnix()));
100
+ return JSON.parse(checkLicense(key ?? this._licenseKey, nowUnix()));
68
101
  }
69
102
  /**
70
103
  * Register raw TTF/OTF bytes for a custom font name used in `<font src="…">`.
@@ -93,6 +126,16 @@ class PdfEngine {
93
126
  this._files.set(name, bytes);
94
127
  return this;
95
128
  }
129
+ /**
130
+ * Register the bytes of an ICC color profile, for the `<profile>` of the document's `<assets>` whose `ref`, or with
131
+ * no ref whose name, is `name`: the press profile a PDF/X document names in its output. Call before `render`.
132
+ * Returns `this` for chaining.
133
+ */
134
+ loadProfile(name, bytes) {
135
+ this._throwIfDisposed();
136
+ this._profiles.set(name, bytes);
137
+ return this;
138
+ }
96
139
  /**
97
140
  * Register the text of a file of Markdown rules, the root of which is a `<markdown-styles>`, for the `src` that
98
141
  * names it, as `<markdown-styles src="…">` does. Call before `render`. Returns `this` for chaining.
@@ -111,10 +154,10 @@ class PdfEngine {
111
154
  }
112
155
  [Symbol.dispose]() { this.dispose(); }
113
156
  /**
114
- * Hand the engine the files of Markdown rules that a document names: the ones loaded here, and the ones read from
115
- * 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`.
116
159
  */
117
- _loadMarkdownStyles(engine, srcs) {
160
+ _markdownStylesFor(srcs) {
118
161
  const files = new Map(this._markdownStyles);
119
162
  for (const src of srcs) {
120
163
  if (!files.has(src)) {
@@ -124,15 +167,13 @@ class PdfEngine {
124
167
  catch { /* not found; the engine says so */ }
125
168
  }
126
169
  }
127
- for (const [src, text] of files) {
128
- engine.load_markdown_styles(src, text);
129
- }
170
+ return files;
130
171
  }
131
172
  /**
132
- * Hand the engine the files the document carries: the ones loaded here, and the ones read from the `src` of a file
133
- * 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.
134
175
  */
135
- _loadFiles(engine, srcs) {
176
+ _filesFor(srcs) {
136
177
  const files = new Map(this._files);
137
178
  for (const [key, src] of srcs) {
138
179
  if (files.has(key))
@@ -141,12 +182,28 @@ class PdfEngine {
141
182
  files.set(key, (0, node_fs_1.readFileSync)(src));
142
183
  }
143
184
  catch (e) {
144
- 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)}`);
145
186
  }
146
187
  }
147
- for (const [key, bytes] of files) {
148
- engine.load_file(key, bytes);
188
+ return files;
189
+ }
190
+ /**
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.
193
+ */
194
+ _profilesFor(srcs) {
195
+ const profiles = new Map(this._profiles);
196
+ for (const [key, src] of srcs) {
197
+ if (profiles.has(key))
198
+ continue;
199
+ try {
200
+ profiles.set(key, (0, node_fs_1.readFileSync)(src));
201
+ }
202
+ catch (e) {
203
+ throw new render_error_1.LpdfRenderError(`profile '${key}' of <assets>: cannot read its src '${src}': ${e instanceof Error ? e.message : String(e)}`);
204
+ }
149
205
  }
206
+ return profiles;
150
207
  }
151
208
  _throwIfDisposed() {
152
209
  if (this._disposed)
@@ -154,89 +211,115 @@ class PdfEngine {
154
211
  }
155
212
  async render(input, callOptions = {}) {
156
213
  this._throwIfDisposed();
157
- const engine = new WasmEngine(this._licenseKey);
158
- // Without a clock the engine cannot check the key's expiry, and an expired key renders as
159
- // though it were current.
160
- engine.set_now(nowUnix());
161
- if (callOptions.createdOn) {
162
- engine.set_created_on(callOptions.createdOn);
163
- }
164
- let pdf;
165
- try {
166
- if (typeof input === 'string') {
167
- // XML path — auto-load fonts and images declared via src="…".
168
- const xml = input;
169
- const allFonts = new Map(this._fonts);
170
- for (const [key, src] of extractAssetSrcs(xml, 'font')) {
171
- if (!allFonts.has(key)) {
172
- try {
173
- allFonts.set(key, (0, node_fs_1.readFileSync)(src));
174
- }
175
- 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));
176
269
  }
270
+ catch { /* not found; Rust falls back to Helvetica */ }
177
271
  }
178
- for (const [name, bytes] of allFonts) {
179
- engine.load_font(name, bytes);
180
- }
181
- for (const [name, bytes] of this._images) {
182
- engine.load_image(name, bytes);
183
- }
184
- this._loadMarkdownStyles(engine, markdownStylesSrcs(xml));
185
- this._loadFiles(engine, extractAssetSrcs(xml, 'file'));
186
- // Auto-load images declared via <image src="…"> that weren't pre-loaded.
187
- for (const [key, src] of extractAssetSrcs(xml, 'image')) {
188
- if (!this._images.has(key)) {
189
- try {
190
- engine.load_image(key, (0, node_fs_1.readFileSync)(src));
191
- }
192
- 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));
193
277
  }
278
+ catch { /* skip unresolvable image */ }
194
279
  }
195
- const dataJson = callOptions.data != null ? JSON.stringify(callOptions.data) : null;
196
- pdf = engine.render_pdf(xml, dataJson, ...(0, _shared_1.encryptionArguments)(callOptions, secureRandomBytes));
197
280
  }
198
- else {
199
- // JSON (Kit tree) path — pass JSON directly to render_tree_pdf. Fonts and images the
200
- // document declares with a src are read from there, as on the XML path.
201
- const json = JSON.stringify(input);
202
- const allFonts = new Map(this._fonts);
203
- for (const [key, src] of assetSrcsOfDocument(input, 'fonts')) {
204
- if (!allFonts.has(key)) {
205
- try {
206
- allFonts.set(key, (0, node_fs_1.readFileSync)(src));
207
- }
208
- 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));
209
293
  }
294
+ catch { /* not found; Rust falls back to Helvetica */ }
210
295
  }
211
- for (const [name, bytes] of allFonts) {
212
- engine.load_font(name, bytes);
213
- }
214
- const allImages = new Map(this._images);
215
- for (const [key, src] of assetSrcsOfDocument(input, 'images')) {
216
- if (!allImages.has(key)) {
217
- try {
218
- allImages.set(key, (0, node_fs_1.readFileSync)(src));
219
- }
220
- 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));
221
301
  }
302
+ catch { /* skip unresolvable image */ }
222
303
  }
223
- for (const [name, bytes] of allImages) {
224
- engine.load_image(name, bytes);
225
- }
226
- this._loadMarkdownStyles(engine, markdownStylesSrcsOfDocument(input));
227
- this._loadFiles(engine, fileSrcsOfDocument(input));
228
- pdf = engine.render_tree_pdf(json, ...(0, _shared_1.encryptionArguments)(callOptions, secureRandomBytes));
229
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) };
230
309
  }
231
- catch (e) {
232
- engine.free();
233
- if (e instanceof LpdfRenderError)
234
- throw e;
235
- const msg = e instanceof Error ? e.message : String(e);
236
- throw new LpdfRenderError(msg);
237
- }
238
- engine.free();
239
- 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
+ };
240
323
  }
241
324
  }
242
325
  exports.PdfEngine = PdfEngine;
@@ -269,7 +352,7 @@ function xmlElements(xml, tag) {
269
352
  }
270
353
  return elements;
271
354
  }
272
- /** The `ref ?? name → src` pairs of the `<font>`, `<image>` or `<file>` elements in XML. */
355
+ /** The `ref ?? name → src` pairs of the `<font>`, `<image>`, `<file>` or `<profile>` elements in XML. */
273
356
  function extractAssetSrcs(xml, tag) {
274
357
  const result = new Map();
275
358
  for (const attributes of xmlElements(xml, tag)) {
@@ -315,7 +398,7 @@ function fileSrcsOfDocument(document) {
315
398
  }
316
399
  return result;
317
400
  }
318
- /** The `ref ?? name → src` pairs of the fonts or images a document declares in `attrs.assets`. */
401
+ /** The `ref ?? name → src` pairs of the fonts, images or color profiles a document declares in `attrs.assets`. */
319
402
  function assetSrcsOfDocument(document, kind) {
320
403
  const result = new Map();
321
404
  const declared = document.attrs['assets']?.[kind] ?? [];
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, 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 } 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 `-`,
@@ -49,12 +54,18 @@ export interface DocumentTokens {
49
54
  lineHeight?: TokenRow<number>;
50
55
  }
51
56
  /**
52
- * The fonts and images the document declares, as the `assets` element of the XML does. A font or image
53
- * is picked by its `name`: text sets `font` to a font's name, an `img` sets `name` to an image's.
57
+ * The fonts, images and color profiles the document declares, as the `assets` element of the XML does. A font or
58
+ * image is picked by its `name`: text sets `font` to a font's name, an `img` sets `name` to an image's, and the
59
+ * output's `profile` names a profile.
54
60
  */
55
61
  export interface DocumentAssets {
56
62
  fonts?: FontAttr[];
57
63
  images?: ImageAttr[];
64
+ /**
65
+ * ICC color profiles: the profile of the printing press a PDF/X document is made for, which the printer gives out.
66
+ * Each is read from its `src`, or loaded with `loadProfile` under its `ref` or name.
67
+ */
68
+ profiles?: ProfileAttr[];
58
69
  }
59
70
  export interface DocumentMeta {
60
71
  title?: string;
@@ -62,6 +73,11 @@ export interface DocumentMeta {
62
73
  subject?: string;
63
74
  keywords?: string;
64
75
  creator?: string;
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
+ */
80
+ lang?: string;
65
81
  }
66
82
  /**
67
83
  * How the PDF file is written, as the `output` element of the XML is: whether it is encrypted, and what a reader
@@ -78,24 +94,90 @@ export interface DocumentOutput {
78
94
  allow?: string;
79
95
  /**
80
96
  * Writes the PDF as PDF/A, the standard for files kept for the long term: `2b`, or `2u` when its text must also
81
- * be searchable. The render needs `createdOn`, and every text a font of `assets`. What PDF/A cannot hold is an
82
- * 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.
83
105
  */
84
106
  pdfa?: string;
107
+ /**
108
+ * Writes the PDF as PDF/UA-1, a tagged PDF a screen reader can read: `1`. It combines with `pdfa`. The render
109
+ * needs `createdOn`, the meta a `title` and a `lang`, every image and barcode of the layout an `alt`, and every
110
+ * text a font of `assets`. What PDF/UA cannot hold is an error that says what to change.
111
+ */
112
+ pdfua?: string;
113
+ /**
114
+ * Writes the PDF as PDF/X-4, for a commercial printer: `4`. It needs `profile`, and combines with `pdfua`. Every
115
+ * page gets a TrimBox and a BleedBox, RGB is tied to sRGB, and links print as text, as PDF/X allows nothing
116
+ * clickable. The render needs `createdOn`, the meta a `title`, and every text a font of `assets`. What PDF/X cannot
117
+ * hold is an error that says what to change: a profile that is not a printer profile, a built-in font, a form
118
+ * field, a CMYK color with a profile that is not a CMYK one, `encrypt`, `pdfa`, attachments.
119
+ */
120
+ pdfx?: string;
121
+ /** The profile of `assets` that a PDF/X file is made for: the ICC profile of the printing press. It needs `pdfx`. */
122
+ profile?: string;
123
+ /**
124
+ * Writes the PDF linearized, for fast web view: page 1 comes first in the file, with hints that say where every
125
+ * other page is, so a viewer that fetches the file by byte ranges, as a browser does, shows page 1 while the rest
126
+ * downloads. Each page then names only the fonts and images it draws. With `encrypt` the file is encrypted first
127
+ * and linearized last. Any change to the file afterwards, such as a stamp, ends the linearization.
128
+ */
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;
85
138
  }
86
139
  export interface SectionAttr {
87
140
  size?: string;
88
141
  orientation?: string;
142
+ /** How far this section's pages reach past their trim edge, such as `5mm`. Overrides the document's; `0mm` takes it away. */
143
+ bleed?: string;
89
144
  background?: string;
90
145
  title?: string;
91
146
  debug?: boolean;
92
147
  type?: string;
93
148
  tocIndentStep?: string;
94
149
  footnoteCounter?: string;
150
+ /**
151
+ * The number of the section's first page, which starts a new run of pages: `1` restarts the count. Without it the
152
+ * pages count on from the section before. `{page}` prints a page's number, and `{pages}` how many pages its run has.
153
+ */
154
+ pageStart?: number;
155
+ /**
156
+ * How the section's page numbers are written: `decimal`, `lower-alpha`, `upper-alpha`, `lower-roman` or
157
+ * `upper-roman`. Without it the section keeps the format before.
158
+ */
159
+ pageFormat?: string;
160
+ /** Text before the section's page numbers, such as `A-`. Without it the section keeps the prefix before; `''` takes it away. */
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;
95
172
  }
96
173
  export interface DocumentAttr {
97
174
  size?: string;
98
175
  orientation?: string;
176
+ /**
177
+ * How far each page reaches past its trim edge, such as `3mm`, for a background or an image that runs off the page
178
+ * when it is cut. The page grows by it and its background fills it; the layout keeps to the trimmed page.
179
+ */
180
+ bleed?: string;
99
181
  background?: string;
100
182
  font?: string;
101
183
  assets?: DocumentAssets;
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
+ }