fullbleed 0.2.0 → 0.3.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/README.md +65 -8
- package/dist/THIRD_PARTY_NOTICES.txt +2 -2
- package/dist/browser/asset-manifest.json +67 -0
- package/dist/browser/client.js +225 -0
- package/dist/browser/worker.js +1477 -0
- package/dist/build.json +20 -8
- package/dist/engine.wasm +0 -0
- package/package.json +18 -6
- package/src/browser-worker.js +39 -0
- package/src/browser.d.ts +37 -0
- package/src/browser.js +103 -0
- package/src/copy-browser-assets.cjs +43 -0
- package/src/engine-runner.js +46 -0
- package/src/index.cjs +1 -45
- package/src/render-input.cjs +51 -0
- package/src/worker.js +3 -43
package/README.md
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
0.1
|
|
7
|
+
Import `fullbleed` for Node.js or `fullbleed/browser` for a web application.
|
|
8
|
+
|
|
9
|
+
This is an optional integration around the published **Fullbleed 2.5.10** engine.
|
|
10
|
+
The package has its own version, **0.3.1**. This patch fixes inline text overlap
|
|
11
|
+
and spacing in wrapped paragraphs, and keeps tracked labels at their intended
|
|
12
|
+
width. Affected PDFs can change line breaks,
|
|
13
|
+
pagination, and hashes; review saved baselines when upgrading.
|
|
14
|
+
The browser entry renders in a Web Worker, and the Node API supports worker and
|
|
15
|
+
process isolation. See the [browser guide](docs/browser.md),
|
|
16
|
+
[inline wrapping checks](docs/inline-wrapping.md),
|
|
17
|
+
[process isolation guide](docs/process-isolation.md),
|
|
11
18
|
[font-family checks](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/font-families.md), and
|
|
12
19
|
[embedded-font checks](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/font-subsets.md) for the retained fixtures.
|
|
13
20
|
|
|
@@ -19,10 +26,12 @@ Use Node.js 22 or newer. Install the [npm package](https://www.npmjs.com/package
|
|
|
19
26
|
npm install fullbleed
|
|
20
27
|
```
|
|
21
28
|
|
|
22
|
-
For a version-pinned installation, use `npm install --save-exact fullbleed@0.
|
|
29
|
+
For a version-pinned installation, use `npm install --save-exact fullbleed@0.3.1`.
|
|
23
30
|
Package archives and retained PDF/PNG evidence are attached to each
|
|
24
31
|
[GitHub release](https://github.com/fullbleed-engine/fullbleed-node/releases).
|
|
25
|
-
|
|
32
|
+
The [installation verification records](https://github.com/fullbleed-engine/fullbleed-node/tree/main/verification)
|
|
33
|
+
retain published-package checks by version, including output hashes and failure
|
|
34
|
+
recovery. Use the record for the version you are evaluating.
|
|
26
35
|
|
|
27
36
|
Save this as `invoice.mjs` and run `node invoice.mjs`:
|
|
28
37
|
|
|
@@ -74,6 +83,39 @@ for the worker and bundled assets, and a standalone-server verification script.
|
|
|
74
83
|
The route bounds rendering and returns private PDF attachments from fictional
|
|
75
84
|
data; connect your own authenticated record lookup when adapting it.
|
|
76
85
|
|
|
86
|
+
## Use it in a browser
|
|
87
|
+
|
|
88
|
+
For React, use the [React and TypeScript starter](examples/react) with automatic
|
|
89
|
+
previews, editable templates, cancellation, and a reusable component-lifecycle hook.
|
|
90
|
+
|
|
91
|
+
Start with the [complete browser application](examples/browser): editable HTML/CSS,
|
|
92
|
+
invoice and report designs, previews, cancellation, and PDF downloads.
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
Copy the browser runtime into your application's static directory:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
npx fullbleed-browser-assets public/fullbleed
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then use the separate browser entry from a page served over HTTPS or localhost:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
import { createRenderer } from 'fullbleed/browser';
|
|
105
|
+
|
|
106
|
+
const renderer = createRenderer({ assetBaseUrl: '/fullbleed/' });
|
|
107
|
+
const result = await renderer.renderPdf({
|
|
108
|
+
html: '<h1>Your document</h1>',
|
|
109
|
+
css: 'h1 { color: #175c52 }',
|
|
110
|
+
maxPages: 20,
|
|
111
|
+
});
|
|
112
|
+
const pdf = new Blob([result.pdf], { type: 'application/pdf' });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The result uses browser byte arrays. The worker loads the shipped engine/fonts
|
|
116
|
+
from your site and renders locally, with cancellation, deadlines and structured
|
|
117
|
+
errors. See the [browser API, download example and hosting requirements](docs/browser.md).
|
|
118
|
+
|
|
77
119
|
## Fonts and assets
|
|
78
120
|
|
|
79
121
|
Supply file bytes explicitly. Additional fonts are registered alongside the
|
|
@@ -101,8 +143,15 @@ Use registered font families in CSS. The engine can substitute an unavailable
|
|
|
101
143
|
family. Its missing-glyph report checks resolved fonts and is not an exhaustive
|
|
102
144
|
font-substitution or visual-validation report.
|
|
103
145
|
|
|
146
|
+
Use bundled or explicitly registered fonts for PNG previews. PDFs using the
|
|
147
|
+
unembedded built-in Helvetica or Times faces rely on PDF-reader fonts; the
|
|
148
|
+
Fullbleed preview can omit their text. See the [retained inline checks](docs/inline-wrapping.md).
|
|
149
|
+
|
|
104
150
|
## API
|
|
105
151
|
|
|
152
|
+
This section describes the Node entry. The browser entry returns browser byte
|
|
153
|
+
arrays through a configured renderer; see its [API guide](docs/browser.md).
|
|
154
|
+
|
|
106
155
|
`await renderPdf(options)` returns `{ pdf, previews, pages, missingGlyphs,
|
|
107
156
|
engineVersion }`. PDF and PNG values are Node `Buffer` objects. Previews are
|
|
108
157
|
generated from the finalized PDF and are empty when disabled.
|
|
@@ -193,6 +242,12 @@ npm run render:fonts
|
|
|
193
242
|
npm run verify:fonts
|
|
194
243
|
npm run render:families
|
|
195
244
|
npm run verify:families
|
|
245
|
+
npm run verify:browser:prepare
|
|
246
|
+
python -m pip install -r tools/browser-requirements.txt
|
|
247
|
+
python -m playwright install chrome firefox webkit
|
|
248
|
+
python tools/check-browser.py --browser chrome --label local
|
|
249
|
+
python tools/check-browser.py --browser firefox --label local
|
|
250
|
+
python tools/check-browser.py --browser webkit --label local
|
|
196
251
|
```
|
|
197
252
|
|
|
198
253
|
The build records engine metadata, licenses, and artifact hashes in `dist/`.
|
|
@@ -201,6 +256,8 @@ fonts, and image assets; compares native and WebAssembly PDF/PNG bytes; checks
|
|
|
201
256
|
error recovery; and installs the packed tarball into a separate directory with
|
|
202
257
|
spaces in its path. CI exercises Node 22, 24, and 26 on Windows, Linux, and macOS.
|
|
203
258
|
Retained release evidence describes the verified fixtures and scope.
|
|
259
|
+
Browser CI uses the same packed artifact in Chrome, Firefox and Playwright WebKit,
|
|
260
|
+
including actual downloads, finalized previews, asset failures and recovery.
|
|
204
261
|
The font check reads PDFs from the isolated tarball installation, checks the
|
|
205
262
|
embedded TrueType programs against their source fonts, and compares two
|
|
206
263
|
independent text readers. CI also compares text and native/PDFium pixels with
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
fullbleed 2.5.
|
|
1
|
+
fullbleed 2.5.10 / LICENSE
|
|
2
2
|
|
|
3
3
|
MIT License
|
|
4
4
|
|
|
@@ -23,7 +23,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
23
23
|
SOFTWARE.
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
fullbleed 2.5.
|
|
26
|
+
fullbleed 2.5.10 / THIRD_PARTY_LICENSES.md
|
|
27
27
|
|
|
28
28
|
<!-- SPDX-License-Identifier: MIT -->
|
|
29
29
|
# Third-Party Licenses
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "fullbleed.browser-assets.v1",
|
|
3
|
+
"packageVersion": "0.3.1",
|
|
4
|
+
"engineVersion": "2.5.10",
|
|
5
|
+
"files": {
|
|
6
|
+
"client.js": {
|
|
7
|
+
"source": "dist/browser/client.js",
|
|
8
|
+
"bytes": 12043,
|
|
9
|
+
"sha256": "60ea647aa3818c61a313f6a3c8ab8f269667fddc6ab85465d30c61408da0b765"
|
|
10
|
+
},
|
|
11
|
+
"worker.js": {
|
|
12
|
+
"source": "dist/browser/worker.js",
|
|
13
|
+
"bytes": 52109,
|
|
14
|
+
"sha256": "32734ecd472fe4c329869ccf58078e8dbb0269621f5b37b8a72116daace65a99"
|
|
15
|
+
},
|
|
16
|
+
"engine.wasm": {
|
|
17
|
+
"source": "dist/engine.wasm",
|
|
18
|
+
"bytes": 5075752,
|
|
19
|
+
"sha256": "4fcbecb954f6a5a64ea840795e74d719cc045d5ba35d5ffa730fcb46b3ce8b3c"
|
|
20
|
+
},
|
|
21
|
+
"THIRD_PARTY_NOTICES.txt": {
|
|
22
|
+
"source": "dist/THIRD_PARTY_NOTICES.txt",
|
|
23
|
+
"bytes": 57662,
|
|
24
|
+
"sha256": "d41ecdf3735cc3eb030949311ba830a503f339ddb0108fb0b2e452186bab354f"
|
|
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.1";
|
|
113
|
+
var engineVersion = "2.5.10";
|
|
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
|
+
};
|