fullbleed 0.1.5 → 0.3.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/README.md CHANGED
@@ -1,14 +1,17 @@
1
- # Fullbleed for Node.js
1
+ # Fullbleed for Node.js and browsers
2
2
 
3
- Generate PDFs from static HTML and CSS in Node.js. The package includes the
3
+ Generate PDFs from static HTML and CSS in Node.js or a browser worker. The package includes the
4
4
  Fullbleed Rust engine compiled to WebAssembly and four font faces, so installing
5
5
  it does not require Python, Rust, a browser, or system fonts. MIT licensed.
6
6
 
7
+ Import `fullbleed` for Node.js or `fullbleed/browser` for a web application.
8
+
7
9
  This is an optional integration around the published **Fullbleed 2.5.8** engine.
8
- The Node package has its own version, **0.1.5**. Normal text now selects a custom
9
- family's regular face even when its italic face is supplied first. Review saved
10
- PDF baselines when upgrading: affected documents can change appearance and line
11
- breaks. See the [font-family checks](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/font-families.md) and
10
+ The package has its own version, **0.3.0**, and adds a browser entry point for
11
+ rendering in a Web Worker. The Node API still supports worker and process
12
+ isolation. See the [browser guide](docs/browser.md),
13
+ [process isolation guide](docs/process-isolation.md),
14
+ [font-family checks](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/font-families.md), and
12
15
  [embedded-font checks](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/font-subsets.md) for the retained fixtures.
13
16
 
14
17
  ## Install and render
@@ -19,10 +22,12 @@ Use Node.js 22 or newer. Install the [npm package](https://www.npmjs.com/package
19
22
  npm install fullbleed
20
23
  ```
21
24
 
22
- For a version-pinned installation, use `npm install --save-exact fullbleed@0.1.5`.
23
- Release verification compares the npm tarball with the checked
24
- [GitHub release](https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.1.5).
25
- See the [installation verification records](https://github.com/fullbleed-engine/fullbleed-node/tree/main/verification).
25
+ For a version-pinned installation, use `npm install --save-exact fullbleed@0.3.0`.
26
+ Package archives and retained PDF/PNG evidence are attached to each
27
+ [GitHub release](https://github.com/fullbleed-engine/fullbleed-node/releases).
28
+ The [installation verification records](https://github.com/fullbleed-engine/fullbleed-node/tree/main/verification)
29
+ retain published-package checks by version, including output hashes and failure
30
+ recovery. Use the record for the version you are evaluating.
26
31
 
27
32
  Save this as `invoice.mjs` and run `node invoice.mjs`:
28
33
 
@@ -74,6 +79,32 @@ for the worker and bundled assets, and a standalone-server verification script.
74
79
  The route bounds rendering and returns private PDF attachments from fictional
75
80
  data; connect your own authenticated record lookup when adapting it.
76
81
 
82
+ ## Use it in a browser
83
+
84
+ Copy the browser runtime into your application's static directory:
85
+
86
+ ```sh
87
+ npx fullbleed-browser-assets public/fullbleed
88
+ ```
89
+
90
+ Then use the separate browser entry from a page served over HTTPS or localhost:
91
+
92
+ ```javascript
93
+ import { createRenderer } from 'fullbleed/browser';
94
+
95
+ const renderer = createRenderer({ assetBaseUrl: '/fullbleed/' });
96
+ const result = await renderer.renderPdf({
97
+ html: '<h1>Your document</h1>',
98
+ css: 'h1 { color: #175c52 }',
99
+ maxPages: 20,
100
+ });
101
+ const pdf = new Blob([result.pdf], { type: 'application/pdf' });
102
+ ```
103
+
104
+ The result uses browser byte arrays. The worker loads the shipped engine/fonts
105
+ from your site and renders locally, with cancellation, deadlines and structured
106
+ errors. See the [browser API, download example and hosting requirements](docs/browser.md).
107
+
77
108
  ## Fonts and assets
78
109
 
79
110
  Supply file bytes explicitly. Additional fonts are registered alongside the
@@ -103,6 +134,9 @@ font-substitution or visual-validation report.
103
134
 
104
135
  ## API
105
136
 
137
+ This section describes the Node entry. The browser entry returns browser byte
138
+ arrays through a configured renderer; see its [API guide](docs/browser.md).
139
+
106
140
  `await renderPdf(options)` returns `{ pdf, previews, pages, missingGlyphs,
107
141
  engineVersion }`. PDF and PNG values are Node `Buffer` objects. Previews are
108
142
  generated from the finalized PDF and are empty when disabled.
@@ -121,6 +155,7 @@ native engine.
121
155
  | `maxPages` | Reject larger documents. Default: 1000. |
122
156
  | `timeoutMs` | Deadline including initial engine loading. Default: 30000 ms. |
123
157
  | `signal` | An `AbortSignal` to cancel rendering. |
158
+ | `isolation` | `'worker'` (default) or `'process'` for a separate Node process per call. |
124
159
  | `allowMissingGlyphs` | Default `false`: reject engine-reported missing glyphs. |
125
160
 
126
161
  Rendering runs in a dedicated worker. Concurrent calls use separate workers and
@@ -129,6 +164,13 @@ render failure, timeout, or cancellation. Awaiting jobs in sequence therefore
129
164
  does not overlap their worker lifetimes. A timeout or abort starts termination;
130
165
  settlement includes the time needed to stop the worker.
131
166
 
167
+ Use `isolation: 'process'` when a render-process failure should become a rejected
168
+ request while the calling application stays running. This mode runs the engine
169
+ and its worker in a fresh Node process, and waits for process exit and IPC
170
+ disconnection before settling. It requires a host that permits child processes
171
+ and adds startup and memory overhead. It does not pool processes or retry jobs.
172
+ See the [guide and failure checks](docs/process-isolation.md).
173
+
132
174
  Bound concurrency in a server according to its available memory;
133
175
  each worker's WebAssembly memory has a 512 MiB ceiling. HTML and CSS together are
134
176
  limited to 4,000,000 UTF-8 bytes; supplied fonts and assets together to 64 MiB.
@@ -142,6 +184,7 @@ try {
142
184
  html: '<h1>Monthly statement</h1>',
143
185
  maxPages: 20,
144
186
  timeoutMs: 10_000,
187
+ isolation: 'process',
145
188
  signal: AbortSignal.timeout(8_000),
146
189
  });
147
190
  // Return result.pdf from your HTTP handler, or write it to your storage.
@@ -153,8 +196,9 @@ try {
153
196
 
154
197
  Error codes include `INVALID_INPUT`, `MISSING_GLYPHS`, `PAGE_LIMIT`, `TIMEOUT`,
155
198
  `ABORTED`, `ENGINE_LOAD_FAILED`, `ENGINE_LIMIT`, `RENDER_FAILED`,
156
- `PREVIEW_FAILED`, and `WORKER_FAILED`. Failed or cancelled calls do not return a
157
- partial PDF. Caller-owned asset buffers are copied when a call starts.
199
+ `PREVIEW_FAILED`, `WORKER_FAILED`, and `PROCESS_FAILED`. Process failures may also
200
+ include `exitCode` and `signal`. Failed or cancelled calls do not return a partial
201
+ PDF. Caller-owned asset buffers are copied when a call starts.
158
202
 
159
203
  This API covers ordinary document generation and previews. It does not expose
160
204
  PDF/A, PDF/UA, PDF/X, template overlays, or compiled VDP. Use the
@@ -183,6 +227,12 @@ npm run render:fonts
183
227
  npm run verify:fonts
184
228
  npm run render:families
185
229
  npm run verify:families
230
+ npm run verify:browser:prepare
231
+ python -m pip install -r tools/browser-requirements.txt
232
+ python -m playwright install chrome firefox webkit
233
+ python tools/check-browser.py --browser chrome --label local
234
+ python tools/check-browser.py --browser firefox --label local
235
+ python tools/check-browser.py --browser webkit --label local
186
236
  ```
187
237
 
188
238
  The build records engine metadata, licenses, and artifact hashes in `dist/`.
@@ -191,6 +241,8 @@ fonts, and image assets; compares native and WebAssembly PDF/PNG bytes; checks
191
241
  error recovery; and installs the packed tarball into a separate directory with
192
242
  spaces in its path. CI exercises Node 22, 24, and 26 on Windows, Linux, and macOS.
193
243
  Retained release evidence describes the verified fixtures and scope.
244
+ Browser CI uses the same packed artifact in Chrome, Firefox and Playwright WebKit,
245
+ including actual downloads, finalized previews, asset failures and recovery.
194
246
  The font check reads PDFs from the isolated tarball installation, checks the
195
247
  embedded TrueType programs against their source fonts, and compares two
196
248
  independent text readers. CI also compares text and native/PDFium pixels with
@@ -199,7 +251,8 @@ the previous public npm release.
199
251
  An intermittent process crash observed on Linux with Node 24.21.0 remains under
200
252
  investigation in [issue #7](https://github.com/fullbleed-engine/fullbleed-node/issues/7).
201
253
  Version 0.1.3 fixes worker shutdown timing, but does not establish that this crash
202
- is resolved. See the [runtime investigation and diagnostic](docs/runtime-diagnostics.md)
254
+ is resolved. Version 0.2.0 adds optional process fault containment; it does not
255
+ claim to fix the native crash. See the [runtime investigation and diagnostic](docs/runtime-diagnostics.md)
203
256
  for the observed scope, retained results, and a synthetic reproducer.
204
257
 
205
258
  ## License and support
@@ -0,0 +1,67 @@
1
+ {
2
+ "schema": "fullbleed.browser-assets.v1",
3
+ "packageVersion": "0.3.0",
4
+ "engineVersion": "2.5.8",
5
+ "files": {
6
+ "client.js": {
7
+ "source": "dist/browser/client.js",
8
+ "bytes": 12042,
9
+ "sha256": "9de6e9b58387b419bd9469d9ac462907cd1f17c787dc89c86ee5e2c96309c0bf"
10
+ },
11
+ "worker.js": {
12
+ "source": "dist/browser/worker.js",
13
+ "bytes": 52108,
14
+ "sha256": "732050b295547a95b2eef88926c64f442fe3cb399282bcfc3f7feb08393b35e5"
15
+ },
16
+ "engine.wasm": {
17
+ "source": "dist/engine.wasm",
18
+ "bytes": 5069661,
19
+ "sha256": "e95cc9132029b9c42ad684d898063b4efc043fa0b7b9fe2e7867f3eb3a17ef58"
20
+ },
21
+ "THIRD_PARTY_NOTICES.txt": {
22
+ "source": "dist/THIRD_PARTY_NOTICES.txt",
23
+ "bytes": 57660,
24
+ "sha256": "5475f50666d3af5df89048cc1ddc98d153cd813019afbc213be20e4f3838911f"
25
+ },
26
+ "LICENSE.txt": {
27
+ "source": "LICENSE",
28
+ "bytes": 1079,
29
+ "sha256": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9"
30
+ },
31
+ "fonts/BebasNeue-OFL.txt": {
32
+ "source": "assets/fonts/BebasNeue-OFL.txt",
33
+ "bytes": 4337,
34
+ "sha256": "72082f6cb4d04be2ecf7cc7d9e1e7d73787f0af8a5a278a47cade70c16b78341"
35
+ },
36
+ "fonts/BebasNeue-Regular.ttf": {
37
+ "source": "assets/fonts/BebasNeue-Regular.ttf",
38
+ "bytes": 61400,
39
+ "sha256": "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73"
40
+ },
41
+ "fonts/DMSerifDisplay-Italic.ttf": {
42
+ "source": "assets/fonts/DMSerifDisplay-Italic.ttf",
43
+ "bytes": 71208,
44
+ "sha256": "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45"
45
+ },
46
+ "fonts/DMSerifDisplay-OFL.txt": {
47
+ "source": "assets/fonts/DMSerifDisplay-OFL.txt",
48
+ "bytes": 4512,
49
+ "sha256": "aa49c876a28d8c0b0a3027d6c25b9c607eee8c0fb96165011d42a6ace1408fe1"
50
+ },
51
+ "fonts/DMSerifDisplay-Regular.ttf": {
52
+ "source": "assets/fonts/DMSerifDisplay-Regular.ttf",
53
+ "bytes": 76580,
54
+ "sha256": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616"
55
+ },
56
+ "fonts/Inter-OFL.txt": {
57
+ "source": "assets/fonts/Inter-OFL.txt",
58
+ "bytes": 4377,
59
+ "sha256": "5b9321a4298cfeb6b34354164a1c3afc3db114569984c502b9b35d988fd58c57"
60
+ },
61
+ "fonts/Inter-Variable.ttf": {
62
+ "source": "assets/fonts/Inter-Variable.ttf",
63
+ "bytes": 876576,
64
+ "sha256": "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031"
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,225 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __esm = (fn, res, err) => function __init() {
8
+ if (err) throw err[0];
9
+ try {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ } catch (e) {
12
+ throw err = [e], e;
13
+ }
14
+ };
15
+ var __commonJS = (cb, mod) => function __require() {
16
+ try {
17
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
+ } catch (e) {
19
+ throw mod = 0, e;
20
+ }
21
+ };
22
+ var __copyProps = (to, from, except, desc) => {
23
+ if (from && typeof from === "object" || typeof from === "function") {
24
+ for (let key of __getOwnPropNames(from))
25
+ if (!__hasOwnProp.call(to, key) && key !== except)
26
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
27
+ }
28
+ return to;
29
+ };
30
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
31
+ // If the importer is in node compatibility mode or this is not an ESM
32
+ // file that has been converted to a CommonJS file using a Babel-
33
+ // compatible transform (i.e. "__esModule" has not been set), then set
34
+ // "default" to the CommonJS "module.exports" for node compatibility.
35
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
36
+ mod
37
+ ));
38
+
39
+ // <define:__FULLBLEED_FONT_NAMES__>
40
+ var init_define_FULLBLEED_FONT_NAMES = __esm({
41
+ "<define:__FULLBLEED_FONT_NAMES__>"() {
42
+ }
43
+ });
44
+
45
+ // <define:__FULLBLEED_RUNTIME_FILES__>
46
+ var init_define_FULLBLEED_RUNTIME_FILES = __esm({
47
+ "<define:__FULLBLEED_RUNTIME_FILES__>"() {
48
+ }
49
+ });
50
+
51
+ // src/render-input.cjs
52
+ var require_render_input = __commonJS({
53
+ "src/render-input.cjs"(exports, module) {
54
+ "use strict";
55
+ init_define_FULLBLEED_FONT_NAMES();
56
+ init_define_FULLBLEED_RUNTIME_FILES();
57
+ var FullbleedError2 = class extends Error {
58
+ constructor(code, message, options) {
59
+ super(message, options);
60
+ this.name = "FullbleedError";
61
+ this.code = code;
62
+ }
63
+ };
64
+ var invalid2 = (message) => {
65
+ throw new FullbleedError2("INVALID_INPUT", message);
66
+ };
67
+ var sourceLimit = 4e6;
68
+ var assetLimit = 64 * 1024 * 1024;
69
+ function normalize(options, { browser = false } = {}) {
70
+ if (!options || typeof options !== "object" || Array.isArray(options)) invalid2("Pass an options object with an html string.");
71
+ const supported = /* @__PURE__ */ new Set(["html", "css", "fonts", "assets", "previewDpi", "timeoutMs", "maxPages", "allowMissingGlyphs", "signal"]);
72
+ if (!browser) supported.add("isolation");
73
+ for (const key of Object.keys(options)) if (!supported.has(key)) invalid2(`Unknown option: ${key}`);
74
+ const { html, css = "", fonts = [], assets = {}, previewDpi = 0, timeoutMs = 3e4, maxPages = 1e3, allowMissingGlyphs = false, signal, isolation = "worker" } = options;
75
+ if (typeof html !== "string" || !html.trim()) invalid2("html must be a nonempty string.");
76
+ if (typeof css !== "string") invalid2("css must be a string.");
77
+ if (new TextEncoder().encode(html).byteLength + new TextEncoder().encode(css).byteLength > sourceLimit) invalid2("HTML and CSS together must not exceed 4,000,000 UTF-8 bytes.");
78
+ if (!Number.isInteger(previewDpi) || previewDpi !== 0 && (previewDpi < 36 || previewDpi > 300)) invalid2("previewDpi must be 0 (disabled) or an integer from 36 to 300.");
79
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) invalid2("timeoutMs must be a positive integer no greater than 2147483647.");
80
+ if (!Number.isSafeInteger(maxPages) || maxPages < 1) invalid2("maxPages must be a positive integer.");
81
+ if (typeof allowMissingGlyphs !== "boolean") invalid2("allowMissingGlyphs must be a boolean.");
82
+ if (signal !== void 0 && !(signal instanceof AbortSignal)) invalid2("signal must be an AbortSignal.");
83
+ if (isolation !== "worker" && isolation !== "process") invalid2("isolation must be worker or process.");
84
+ if (!Array.isArray(fonts)) invalid2("fonts must be an array of font byte arrays.");
85
+ if (!assets || typeof assets !== "object" || Array.isArray(assets)) invalid2("assets must map relative names to byte arrays.");
86
+ let assetBytes = 0;
87
+ function bytes(value, label) {
88
+ if (!(value instanceof Uint8Array) || !value.byteLength) invalid2(`${label} must be a nonempty Uint8Array or Buffer.`);
89
+ assetBytes += value.byteLength;
90
+ if (assetBytes > assetLimit) invalid2("Custom fonts and assets together must not exceed 64 MiB.");
91
+ return Uint8Array.from(value);
92
+ }
93
+ const customFonts = fonts.map((font, i) => ({ name: `fonts/custom-${i}.ttf`, data: bytes(font, `fonts[${i}]`) }));
94
+ const names = Object.keys(assets);
95
+ const fileNames = new Set(names);
96
+ const assetFiles = names.map((name) => {
97
+ const segments = name.split("/");
98
+ if (!name || /[\\:\0]/.test(name) || segments.some((s) => !s || s === "." || s === "..")) invalid2(`Invalid asset path: ${name}`);
99
+ for (let i = 1; i < segments.length; i++) if (fileNames.has(segments.slice(0, i).join("/"))) invalid2(`Asset is both a file and a directory: ${name}`);
100
+ return { name: "assets/" + name, data: bytes(assets[name], `assets[${name}]`) };
101
+ });
102
+ return { html, css, customFonts, assetFiles, previewDpi, timeoutMs, maxPages, allowMissingGlyphs, signal, isolation };
103
+ }
104
+ module.exports = { FullbleedError: FullbleedError2, normalize };
105
+ }
106
+ });
107
+
108
+ // src/browser.js
109
+ init_define_FULLBLEED_FONT_NAMES();
110
+ init_define_FULLBLEED_RUNTIME_FILES();
111
+ var import_render_input = __toESM(require_render_input(), 1);
112
+ var version = "0.3.0";
113
+ var engineVersion = "2.5.8";
114
+ var FullbleedError = import_render_input.default.FullbleedError;
115
+ var invalid = (message) => {
116
+ throw new FullbleedError("INVALID_INPUT", message);
117
+ };
118
+ function createRenderer(options) {
119
+ if (!options || typeof options !== "object" || Array.isArray(options)) invalid("Pass an assetBaseUrl pointing to your hosted Fullbleed assets.");
120
+ for (const key of Object.keys(options)) if (key !== "assetBaseUrl") invalid(`Unknown renderer option: ${key}`);
121
+ if (typeof globalThis.Worker !== "function" || !globalThis.location || !globalThis.crypto?.subtle || !globalThis.WebAssembly) {
122
+ throw new FullbleedError("UNSUPPORTED_BROWSER", "Use a browser with Web Workers, WebAssembly and Web Crypto over HTTPS or localhost.");
123
+ }
124
+ if (typeof options.assetBaseUrl !== "string" && !(options.assetBaseUrl instanceof URL)) invalid("assetBaseUrl must be a URL or URL string.");
125
+ let base;
126
+ try {
127
+ base = new URL(options.assetBaseUrl, globalThis.location.href);
128
+ } catch {
129
+ invalid("assetBaseUrl is not a valid URL.");
130
+ }
131
+ if (!["http:", "https:"].includes(base.protocol) || base.origin !== globalThis.location.origin || base.username || base.password || base.search || base.hash || !base.pathname.endsWith("/")) {
132
+ invalid("assetBaseUrl must be a same-origin HTTP(S) directory URL ending in /, without credentials, a query or a fragment.");
133
+ }
134
+ const entry = new URL("worker.js", base);
135
+ entry.searchParams.set("v", version);
136
+ async function renderPdf(options2) {
137
+ const start = Date.now();
138
+ const job = import_render_input.default.normalize(options2, { browser: true });
139
+ const aborted = () => new FullbleedError("ABORTED", "PDF rendering was aborted.");
140
+ if (job.signal?.aborted) throw aborted();
141
+ const remaining = job.timeoutMs - (Date.now() - start);
142
+ if (remaining <= 0) throw new FullbleedError("TIMEOUT", `PDF rendering exceeded ${job.timeoutMs} ms.`);
143
+ return new Promise((resolve, reject) => {
144
+ let worker, timer, settled = false, ready = false;
145
+ const finish = (error, result) => {
146
+ if (settled) return;
147
+ settled = true;
148
+ clearTimeout(timer);
149
+ job.signal?.removeEventListener("abort", abort);
150
+ queueMicrotask(() => {
151
+ try {
152
+ worker?.terminate();
153
+ } catch (cause) {
154
+ reject(new FullbleedError("WORKER_FAILED", "Could not stop the render worker.", { cause }));
155
+ return;
156
+ }
157
+ if (error) reject(error);
158
+ else resolve(result);
159
+ });
160
+ };
161
+ const abort = () => finish(aborted());
162
+ timer = setTimeout(() => finish(new FullbleedError("TIMEOUT", `PDF rendering exceeded ${job.timeoutMs} ms.`)), remaining);
163
+ job.signal?.addEventListener("abort", abort, { once: true });
164
+ if (job.signal?.aborted) {
165
+ abort();
166
+ return;
167
+ }
168
+ try {
169
+ worker = new Worker(entry.href, { type: "module", name: "Fullbleed PDF renderer", credentials: "omit" });
170
+ worker.addEventListener("error", (event) => {
171
+ event.preventDefault();
172
+ finish(new FullbleedError("WORKER_FAILED", "The rendering worker could not run. Check the copied assets and the site content security policy."));
173
+ });
174
+ worker.addEventListener("messageerror", () => finish(new FullbleedError("WORKER_FAILED", "The rendering worker returned an unreadable response.")));
175
+ worker.addEventListener("message", ({ data }) => {
176
+ if (settled) return;
177
+ if (!ready) {
178
+ if (data?.type !== "ready" || data.protocol !== 1 || data.version !== version || data.engineVersion !== engineVersion) {
179
+ finish(new FullbleedError("VERSION_MISMATCH", "The hosted browser assets do not match this package. Copy the assets again and deploy them together."));
180
+ return;
181
+ }
182
+ ready = true;
183
+ try {
184
+ worker.postMessage({ type: "render", job: {
185
+ html: job.html,
186
+ css: "body { font-family: Inter; }\n" + job.css,
187
+ fonts: job.customFonts,
188
+ assets: job.assetFiles,
189
+ previewDpi: job.previewDpi,
190
+ maxPages: job.maxPages,
191
+ allowMissingGlyphs: job.allowMissingGlyphs
192
+ } });
193
+ } catch (cause) {
194
+ finish(new FullbleedError("WORKER_FAILED", "Could not send the document to the rendering worker.", { cause }));
195
+ }
196
+ return;
197
+ }
198
+ if (data?.type !== "result" || typeof data.ok !== "boolean") {
199
+ finish(new FullbleedError("WORKER_FAILED", "The rendering worker returned an invalid response."));
200
+ return;
201
+ }
202
+ if (!data.ok) {
203
+ finish(typeof data.code === "string" && typeof data.message === "string" ? new FullbleedError(data.code, data.message) : new FullbleedError("WORKER_FAILED", "The rendering worker returned an invalid error."));
204
+ return;
205
+ }
206
+ if (!(data.pdf instanceof Uint8Array) || new TextDecoder().decode(data.pdf.subarray(0, 5)) !== "%PDF-" || !Number.isSafeInteger(data.pages) || data.pages < 1 || data.pages > job.maxPages || !Array.isArray(data.previews) || data.previews.length !== (job.previewDpi ? data.pages : 0) || !data.previews.every((bytes) => bytes instanceof Uint8Array && [137, 80, 78, 71, 13, 10, 26, 10].every((value, i) => bytes[i] === value)) || !Number.isSafeInteger(data.missingGlyphs) || data.missingGlyphs < 0 || data.engineVersion !== engineVersion) {
207
+ finish(new FullbleedError("WORKER_FAILED", "The rendering worker returned an invalid result."));
208
+ return;
209
+ }
210
+ finish(null, { pdf: data.pdf, previews: data.previews, pages: data.pages, missingGlyphs: data.missingGlyphs, engineVersion });
211
+ });
212
+ if (job.signal?.aborted) abort();
213
+ } catch (cause) {
214
+ finish(new FullbleedError("WORKER_FAILED", "Could not start the rendering worker.", { cause }));
215
+ }
216
+ });
217
+ }
218
+ return Object.freeze({ renderPdf });
219
+ }
220
+ export {
221
+ FullbleedError,
222
+ createRenderer,
223
+ engineVersion,
224
+ version
225
+ };