fullbleed 0.1.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/LICENSE +21 -0
- package/README.md +176 -0
- package/assets/fonts/BebasNeue-OFL.txt +93 -0
- package/assets/fonts/BebasNeue-Regular.ttf +0 -0
- package/assets/fonts/DMSerifDisplay-Italic.ttf +0 -0
- package/assets/fonts/DMSerifDisplay-OFL.txt +93 -0
- package/assets/fonts/DMSerifDisplay-Regular.ttf +0 -0
- package/assets/fonts/Inter-OFL.txt +93 -0
- package/assets/fonts/Inter-Variable.ttf +0 -0
- package/dist/THIRD_PARTY_NOTICES.txt +1131 -0
- package/dist/build.json +83 -0
- package/dist/engine.wasm +0 -0
- package/dist/index.d.cts +36 -0
- package/package.json +71 -0
- package/src/index.cjs +133 -0
- package/src/index.d.ts +36 -0
- package/src/index.js +3 -0
- package/src/worker.js +46 -0
package/dist/build.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"packageVersion": "0.1.1",
|
|
3
|
+
"engineVersion": "2.5.5",
|
|
4
|
+
"engineSource": "https://crates.io/crates/fullbleed/2.5.5",
|
|
5
|
+
"target": "wasm32-wasip1",
|
|
6
|
+
"memoryMaximumBytes": 536870912,
|
|
7
|
+
"fonts": [
|
|
8
|
+
"Inter-Variable.ttf",
|
|
9
|
+
"DMSerifDisplay-Regular.ttf",
|
|
10
|
+
"DMSerifDisplay-Italic.ttf",
|
|
11
|
+
"BebasNeue-Regular.ttf"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": [
|
|
14
|
+
{
|
|
15
|
+
"name": "fullbleed",
|
|
16
|
+
"version": "2.5.5",
|
|
17
|
+
"source": "registry+https://github.com/rust-lang/crates.io-index",
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "fullbleed_audit_contract",
|
|
22
|
+
"version": "0.1.3",
|
|
23
|
+
"source": "registry+https://github.com/rust-lang/crates.io-index",
|
|
24
|
+
"license": "MIT"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "rustc-hash",
|
|
28
|
+
"version": "2.1.3",
|
|
29
|
+
"source": "registry+https://github.com/rust-lang/crates.io-index",
|
|
30
|
+
"license": "Apache-2.0 OR MIT"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "subsetter",
|
|
34
|
+
"version": "0.2.6",
|
|
35
|
+
"source": "registry+https://github.com/rust-lang/crates.io-index",
|
|
36
|
+
"license": "MIT OR Apache-2.0"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "unicode-bidi",
|
|
40
|
+
"version": "0.3.18",
|
|
41
|
+
"source": "registry+https://github.com/rust-lang/crates.io-index",
|
|
42
|
+
"license": "MIT OR Apache-2.0"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"files": {
|
|
46
|
+
"dist/engine.wasm": {
|
|
47
|
+
"bytes": 5047938,
|
|
48
|
+
"sha256": "60f06871dcd0109f919581f21ba5dac716c2c160094f380d5dba58ec96f0d5ee"
|
|
49
|
+
},
|
|
50
|
+
"dist/THIRD_PARTY_NOTICES.txt": {
|
|
51
|
+
"bytes": 57660,
|
|
52
|
+
"sha256": "f369b7da5c0d54fb0be0917475e5964cfa276cfe3c7c830f7853213592d41ab8"
|
|
53
|
+
},
|
|
54
|
+
"assets/fonts/BebasNeue-OFL.txt": {
|
|
55
|
+
"bytes": 4337,
|
|
56
|
+
"sha256": "72082f6cb4d04be2ecf7cc7d9e1e7d73787f0af8a5a278a47cade70c16b78341"
|
|
57
|
+
},
|
|
58
|
+
"assets/fonts/BebasNeue-Regular.ttf": {
|
|
59
|
+
"bytes": 61400,
|
|
60
|
+
"sha256": "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73"
|
|
61
|
+
},
|
|
62
|
+
"assets/fonts/DMSerifDisplay-Italic.ttf": {
|
|
63
|
+
"bytes": 71208,
|
|
64
|
+
"sha256": "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45"
|
|
65
|
+
},
|
|
66
|
+
"assets/fonts/DMSerifDisplay-OFL.txt": {
|
|
67
|
+
"bytes": 4512,
|
|
68
|
+
"sha256": "aa49c876a28d8c0b0a3027d6c25b9c607eee8c0fb96165011d42a6ace1408fe1"
|
|
69
|
+
},
|
|
70
|
+
"assets/fonts/DMSerifDisplay-Regular.ttf": {
|
|
71
|
+
"bytes": 76580,
|
|
72
|
+
"sha256": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616"
|
|
73
|
+
},
|
|
74
|
+
"assets/fonts/Inter-OFL.txt": {
|
|
75
|
+
"bytes": 4377,
|
|
76
|
+
"sha256": "5b9321a4298cfeb6b34354164a1c3afc3db114569984c502b9b35d988fd58c57"
|
|
77
|
+
},
|
|
78
|
+
"assets/fonts/Inter-Variable.ttf": {
|
|
79
|
+
"bytes": 876576,
|
|
80
|
+
"sha256": "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/engine.wasm
ADDED
|
Binary file
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import type { Buffer } from 'node:buffer';
|
|
3
|
+
|
|
4
|
+
export interface RenderOptions {
|
|
5
|
+
html: string;
|
|
6
|
+
css?: string;
|
|
7
|
+
/** Additional TrueType font files, as bytes. Use their family names in CSS. */
|
|
8
|
+
fonts?: readonly Uint8Array[];
|
|
9
|
+
/** Relative asset names; reference them as assets/name.svg in HTML/CSS. */
|
|
10
|
+
assets?: Readonly<Record<string, Uint8Array>>;
|
|
11
|
+
/** 0 disables previews (default); otherwise 36–300 DPI. */
|
|
12
|
+
previewDpi?: number;
|
|
13
|
+
/** Includes initial engine loading. Default: 30000 ms. */
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
/** Reject documents larger than this. Default: 1000. */
|
|
16
|
+
maxPages?: number;
|
|
17
|
+
/** Default false: missing glyphs reject the job instead of silently emitting them. */
|
|
18
|
+
allowMissingGlyphs?: boolean;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RenderResult {
|
|
23
|
+
pdf: Buffer;
|
|
24
|
+
previews: Buffer[];
|
|
25
|
+
pages: number;
|
|
26
|
+
missingGlyphs: number;
|
|
27
|
+
engineVersion: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class FullbleedError extends Error {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
constructor(code: string, message: string, options?: ErrorOptions);
|
|
33
|
+
}
|
|
34
|
+
export function renderPdf(options: RenderOptions): Promise<RenderResult>;
|
|
35
|
+
export const engineVersion: string;
|
|
36
|
+
export const version: string;
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fullbleed",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Generate PDFs from HTML and CSS in Node.js with the Fullbleed Rust engine, bundled fonts, and optional PNG previews.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.cjs",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.cts",
|
|
16
|
+
"default": "./src/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src/",
|
|
25
|
+
"dist/",
|
|
26
|
+
"assets/fonts/",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "node tools/build.mjs",
|
|
32
|
+
"prepack": "node tools/check-build.mjs",
|
|
33
|
+
"test": "node --test test/*.test.mjs",
|
|
34
|
+
"check:types": "tsc -p tsconfig.json",
|
|
35
|
+
"verify:native": "node tools/verify-native.mjs",
|
|
36
|
+
"example": "node examples/invoice.mjs",
|
|
37
|
+
"verify:pack": "node tools/verify-pack.mjs"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@bjorn3/browser_wasi_shim": "0.4.2"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/fullbleed-engine/fullbleed-node.git"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/fullbleed-engine/fullbleed-node#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/fullbleed-engine/fullbleed-node/issues"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"pdf",
|
|
53
|
+
"html-to-pdf",
|
|
54
|
+
"pdf-generation",
|
|
55
|
+
"fullbleed",
|
|
56
|
+
"invoice",
|
|
57
|
+
"reports",
|
|
58
|
+
"rust",
|
|
59
|
+
"webassembly"
|
|
60
|
+
],
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
},
|
|
64
|
+
"fullbleed": {
|
|
65
|
+
"engineVersion": "2.5.5"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/node": "22.20.5",
|
|
69
|
+
"typescript": "7.0.2"
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/index.cjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
'use strict';
|
|
3
|
+
const { readFile } = require('node:fs/promises');
|
|
4
|
+
const { createHash } = require('node:crypto');
|
|
5
|
+
const { join } = require('node:path');
|
|
6
|
+
const { Worker } = require('node:worker_threads');
|
|
7
|
+
const { pathToFileURL } = require('node:url');
|
|
8
|
+
const pkg = require('../package.json');
|
|
9
|
+
|
|
10
|
+
class FullbleedError extends Error {
|
|
11
|
+
constructor(code, message, options) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.name = 'FullbleedError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const invalid = message => { throw new FullbleedError('INVALID_INPUT', message); };
|
|
18
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
19
|
+
const sourceLimit = 4_000_000;
|
|
20
|
+
const assetLimit = 64 * 1024 * 1024;
|
|
21
|
+
const root = join(__dirname, '..');
|
|
22
|
+
let runtimePromise;
|
|
23
|
+
|
|
24
|
+
function runtime() {
|
|
25
|
+
if (!runtimePromise) runtimePromise = (async () => {
|
|
26
|
+
const manifest = JSON.parse(await readFile(join(root, 'dist/build.json'), 'utf8'));
|
|
27
|
+
if (manifest.engineVersion !== pkg.fullbleed.engineVersion) throw new Error('Package and engine versions differ');
|
|
28
|
+
async function verified(name) {
|
|
29
|
+
const bytes = await readFile(join(root, name));
|
|
30
|
+
const expected = manifest.files[name];
|
|
31
|
+
if (!expected || expected.bytes !== bytes.length || expected.sha256 !== hash(bytes)) throw new Error(`Bundled file failed verification: ${name}`);
|
|
32
|
+
return new Uint8Array(bytes);
|
|
33
|
+
}
|
|
34
|
+
const [wasm, fonts] = await Promise.all([
|
|
35
|
+
verified('dist/engine.wasm'),
|
|
36
|
+
Promise.all(manifest.fonts.map(async name => ({ name: 'fonts/' + name, data: await verified('assets/fonts/' + name) }))),
|
|
37
|
+
]);
|
|
38
|
+
const module = await WebAssembly.compile(wasm);
|
|
39
|
+
const imports = WebAssembly.Module.imports(module);
|
|
40
|
+
if (!imports.every(i => i.module === 'wasi_snapshot_preview1' && !i.name.startsWith('sock_'))) throw new Error('Unexpected WebAssembly host imports');
|
|
41
|
+
return { module, fonts };
|
|
42
|
+
})().catch(cause => { throw new FullbleedError('ENGINE_LOAD_FAILED', 'Could not load the bundled Fullbleed engine: ' + cause.message, { cause }); });
|
|
43
|
+
return runtimePromise;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalize(options) {
|
|
47
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) invalid('Pass an options object with an html string.');
|
|
48
|
+
const supported = new Set(['html', 'css', 'fonts', 'assets', 'previewDpi', 'timeoutMs', 'maxPages', 'allowMissingGlyphs', 'signal']);
|
|
49
|
+
for (const key of Object.keys(options)) if (!supported.has(key)) invalid(`Unknown option: ${key}`);
|
|
50
|
+
const { html, css = '', fonts = [], assets = {}, previewDpi = 0, timeoutMs = 30000, maxPages = 1000, allowMissingGlyphs = false, signal } = options;
|
|
51
|
+
if (typeof html !== 'string' || !html.trim()) invalid('html must be a nonempty string.');
|
|
52
|
+
if (typeof css !== 'string') invalid('css must be a string.');
|
|
53
|
+
if (Buffer.byteLength(html) + Buffer.byteLength(css) > sourceLimit) invalid('HTML and CSS together must not exceed 4,000,000 UTF-8 bytes.');
|
|
54
|
+
if (!Number.isInteger(previewDpi) || (previewDpi !== 0 && (previewDpi < 36 || previewDpi > 300))) invalid('previewDpi must be 0 (disabled) or an integer from 36 to 300.');
|
|
55
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) invalid('timeoutMs must be a positive integer no greater than 2147483647.');
|
|
56
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1) invalid('maxPages must be a positive integer.');
|
|
57
|
+
if (typeof allowMissingGlyphs !== 'boolean') invalid('allowMissingGlyphs must be a boolean.');
|
|
58
|
+
if (signal !== undefined && !(signal instanceof AbortSignal)) invalid('signal must be an AbortSignal.');
|
|
59
|
+
if (!Array.isArray(fonts)) invalid('fonts must be an array of font byte arrays.');
|
|
60
|
+
if (!assets || typeof assets !== 'object' || Array.isArray(assets)) invalid('assets must map relative names to byte arrays.');
|
|
61
|
+
let assetBytes = 0;
|
|
62
|
+
function bytes(value, label) {
|
|
63
|
+
if (!(value instanceof Uint8Array) || !value.byteLength) invalid(`${label} must be a nonempty Uint8Array or Buffer.`);
|
|
64
|
+
assetBytes += value.byteLength;
|
|
65
|
+
if (assetBytes > assetLimit) invalid('Custom fonts and assets together must not exceed 64 MiB.');
|
|
66
|
+
return Uint8Array.from(value); // Snapshot caller-owned buffers before any await.
|
|
67
|
+
}
|
|
68
|
+
const customFonts = fonts.map((font, i) => ({ name: `fonts/custom-${i}.ttf`, data: bytes(font, `fonts[${i}]`) }));
|
|
69
|
+
const names = Object.keys(assets);
|
|
70
|
+
const fileNames = new Set(names);
|
|
71
|
+
const assetFiles = names.map(name => {
|
|
72
|
+
const segments = name.split('/');
|
|
73
|
+
if (!name || /[\\:\0]/.test(name) || segments.some(s => !s || s === '.' || s === '..')) invalid(`Invalid asset path: ${name}`);
|
|
74
|
+
for (let i = 1; i < segments.length; i++) if (fileNames.has(segments.slice(0, i).join('/'))) invalid(`Asset is both a file and a directory: ${name}`);
|
|
75
|
+
return { name: 'assets/' + name, data: bytes(assets[name], `assets[${name}]`) };
|
|
76
|
+
});
|
|
77
|
+
return { html, css: 'body { font-family: Inter; }\n' + css, customFonts, assetFiles, previewDpi, timeoutMs, maxPages, allowMissingGlyphs, signal };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function renderPdf(options) {
|
|
81
|
+
if (Number(process.versions.node.split('.')[0]) < 22) throw new FullbleedError('UNSUPPORTED_NODE', 'Fullbleed requires Node.js 22 or newer.');
|
|
82
|
+
const start = Date.now();
|
|
83
|
+
const job = normalize(options);
|
|
84
|
+
const aborted = () => new FullbleedError('ABORTED', 'PDF rendering was aborted.');
|
|
85
|
+
if (job.signal?.aborted) throw aborted();
|
|
86
|
+
const remaining = job.timeoutMs - (Date.now() - start);
|
|
87
|
+
if (remaining <= 0) throw new FullbleedError('TIMEOUT', `PDF rendering exceeded ${job.timeoutMs} ms.`);
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
let worker, timer, settled = false;
|
|
90
|
+
const finish = (error, result) => {
|
|
91
|
+
if (settled) return;
|
|
92
|
+
settled = true;
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
job.signal?.removeEventListener('abort', abort);
|
|
95
|
+
const termination = worker?.terminate();
|
|
96
|
+
if (termination) termination.catch(() => {});
|
|
97
|
+
if (error) reject(error); else resolve(result);
|
|
98
|
+
};
|
|
99
|
+
const abort = () => finish(aborted());
|
|
100
|
+
timer = setTimeout(() => finish(new FullbleedError('TIMEOUT', `PDF rendering exceeded ${job.timeoutMs} ms.`)), remaining);
|
|
101
|
+
job.signal?.addEventListener('abort', abort, { once: true });
|
|
102
|
+
if (job.signal?.aborted) return abort();
|
|
103
|
+
runtime().then(loaded => {
|
|
104
|
+
if (settled) return;
|
|
105
|
+
// A trusted inline bootstrap supports --input-type callers while letting
|
|
106
|
+
// Node inherit process flags through its own worker compatibility rules.
|
|
107
|
+
const entry = pathToFileURL(join(__dirname, 'worker.js')).href;
|
|
108
|
+
worker = new Worker(`import(${JSON.stringify(entry)})`, {
|
|
109
|
+
eval: true,
|
|
110
|
+
workerData: {
|
|
111
|
+
module: loaded.module, fonts: [...loaded.fonts, ...job.customFonts], assets: job.assetFiles,
|
|
112
|
+
html: job.html, css: job.css, previewDpi: job.previewDpi, maxPages: job.maxPages,
|
|
113
|
+
allowMissingGlyphs: job.allowMissingGlyphs,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
worker.once('error', cause => finish(new FullbleedError('WORKER_FAILED', cause.message, { cause })));
|
|
117
|
+
worker.once('exit', code => {
|
|
118
|
+
if (!settled) finish(new FullbleedError('WORKER_FAILED', `Rendering worker exited before producing a result (code ${code}).`));
|
|
119
|
+
});
|
|
120
|
+
worker.once('message', result => {
|
|
121
|
+
if (!result.ok) return finish(new FullbleedError(result.code, result.message));
|
|
122
|
+
finish(null, { pdf: Buffer.from(result.pdf), previews: result.previews.map(bytes => Buffer.from(bytes)),
|
|
123
|
+
pages: result.pages, missingGlyphs: result.missingGlyphs, engineVersion: pkg.fullbleed.engineVersion });
|
|
124
|
+
});
|
|
125
|
+
if (job.signal?.aborted) abort();
|
|
126
|
+
}).catch(cause => finish(cause instanceof FullbleedError ? cause : new FullbleedError('WORKER_FAILED', cause.message, { cause })));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
exports.renderPdf = renderPdf;
|
|
131
|
+
exports.FullbleedError = FullbleedError;
|
|
132
|
+
exports.engineVersion = pkg.fullbleed.engineVersion;
|
|
133
|
+
exports.version = pkg.version;
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import type { Buffer } from 'node:buffer';
|
|
3
|
+
|
|
4
|
+
export interface RenderOptions {
|
|
5
|
+
html: string;
|
|
6
|
+
css?: string;
|
|
7
|
+
/** Additional TrueType font files, as bytes. Use their family names in CSS. */
|
|
8
|
+
fonts?: readonly Uint8Array[];
|
|
9
|
+
/** Relative asset names; reference them as assets/name.svg in HTML/CSS. */
|
|
10
|
+
assets?: Readonly<Record<string, Uint8Array>>;
|
|
11
|
+
/** 0 disables previews (default); otherwise 36–300 DPI. */
|
|
12
|
+
previewDpi?: number;
|
|
13
|
+
/** Includes initial engine loading. Default: 30000 ms. */
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
/** Reject documents larger than this. Default: 1000. */
|
|
16
|
+
maxPages?: number;
|
|
17
|
+
/** Default false: missing glyphs reject the job instead of silently emitting them. */
|
|
18
|
+
allowMissingGlyphs?: boolean;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RenderResult {
|
|
23
|
+
pdf: Buffer;
|
|
24
|
+
previews: Buffer[];
|
|
25
|
+
pages: number;
|
|
26
|
+
missingGlyphs: number;
|
|
27
|
+
engineVersion: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class FullbleedError extends Error {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
constructor(code: string, message: string, options?: ErrorOptions);
|
|
33
|
+
}
|
|
34
|
+
export function renderPdf(options: RenderOptions): Promise<RenderResult>;
|
|
35
|
+
export const engineVersion: string;
|
|
36
|
+
export const version: string;
|
package/src/index.js
ADDED
package/src/worker.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
3
|
+
import { WASI, File, OpenFile, Directory, ConsoleStdout, PreopenDirectory } from '@bjorn3/browser_wasi_shim';
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const job = workerData;
|
|
7
|
+
const root = new PreopenDirectory('.', new Map());
|
|
8
|
+
const put = (path, data) => {
|
|
9
|
+
const parts = path.split('/');
|
|
10
|
+
let directory = root.dir;
|
|
11
|
+
for (const part of parts.slice(0, -1)) {
|
|
12
|
+
if (!directory.contents.has(part)) directory.contents.set(part, new Directory(new Map()));
|
|
13
|
+
directory = directory.contents.get(part);
|
|
14
|
+
}
|
|
15
|
+
directory.contents.set(parts.at(-1), new File(data));
|
|
16
|
+
};
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
put('input.html', encoder.encode(job.html));
|
|
19
|
+
put('style.css', encoder.encode(job.css));
|
|
20
|
+
for (const file of [...job.fonts, ...job.assets]) put(file.name, file.data);
|
|
21
|
+
let log = '';
|
|
22
|
+
const collect = line => { if (log.length < 4000) log += String(line).slice(0, 4000 - log.length) + '\n'; };
|
|
23
|
+
const wasi = new WASI(['fullbleed', String(job.maxPages), String(job.previewDpi), job.allowMissingGlyphs ? 'allow' : 'reject', ...job.fonts.map(f => f.name)], [], [
|
|
24
|
+
new OpenFile(new File([])), ConsoleStdout.lineBuffered(collect), ConsoleStdout.lineBuffered(collect), root,
|
|
25
|
+
], { debug: false });
|
|
26
|
+
const instance = await WebAssembly.instantiate(job.module, { wasi_snapshot_preview1: wasi.wasiImport });
|
|
27
|
+
const status = wasi.start(instance);
|
|
28
|
+
if (status !== 0) {
|
|
29
|
+
const code = ['PAGE_LIMIT', 'MISSING_GLYPHS', 'PREVIEW_FAILED'].find(c => log.includes(c + ':')) ?? 'RENDER_FAILED';
|
|
30
|
+
parentPort.postMessage({ ok: false, code, message: log.trim() || `The engine exited with code ${status}.` });
|
|
31
|
+
} else {
|
|
32
|
+
const read = name => {
|
|
33
|
+
const file = root.dir.contents.get(name);
|
|
34
|
+
if (!(file instanceof File)) throw new Error(`Missing engine output: ${name}`);
|
|
35
|
+
return Uint8Array.from(file.data);
|
|
36
|
+
};
|
|
37
|
+
const report = JSON.parse(new TextDecoder().decode(read('result.json')));
|
|
38
|
+
const pdf = read('document.pdf');
|
|
39
|
+
if (new TextDecoder().decode(pdf.slice(0, 5)) !== '%PDF-') throw new Error('Engine output is not a PDF.');
|
|
40
|
+
const previews = job.previewDpi ? Array.from({ length: report.pages }, (_, i) => read(`preview-${i + 1}.png`)) : [];
|
|
41
|
+
parentPort.postMessage({ ok: true, pdf, previews, ...report }, [pdf.buffer, ...previews.map(p => p.buffer)]);
|
|
42
|
+
}
|
|
43
|
+
} catch (cause) {
|
|
44
|
+
const code = cause instanceof WebAssembly.RuntimeError || cause instanceof RangeError ? 'ENGINE_LIMIT' : 'RENDER_FAILED';
|
|
45
|
+
parentPort.postMessage({ ok: false, code, message: cause.message || String(cause) });
|
|
46
|
+
}
|