blitz-pdf 0.1.3 → 0.1.5
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 +35 -6
- package/THIRD_PARTY_NOTICES.md +5 -2
- package/dist/api.d.ts +9 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +25 -27
- package/dist/api.js.map +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +13 -1
- package/dist/browser.js.map +1 -1
- package/dist/node-worker.d.ts +2 -0
- package/dist/node-worker.d.ts.map +1 -0
- package/dist/node-worker.js +10 -0
- package/dist/node-worker.js.map +1 -0
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +18 -6
- package/dist/node.js.map +1 -1
- package/dist/render.d.ts +8 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +28 -0
- package/dist/render.js.map +1 -0
- package/dist/wasm.d.ts +3 -0
- package/dist/wasm.d.ts.map +1 -0
- package/dist/wasm.js +18 -0
- package/dist/wasm.js.map +1 -0
- package/dist/worker-host.d.ts +16 -0
- package/dist/worker-host.d.ts.map +1 -0
- package/dist/worker-host.js +31 -0
- package/dist/worker-host.js.map +1 -0
- package/dist/worker-protocol.d.ts +22 -0
- package/dist/worker-protocol.d.ts.map +1 -0
- package/dist/worker-protocol.js +2 -0
- package/dist/worker-protocol.js.map +1 -0
- package/dist/worker-runtime.d.ts +8 -0
- package/dist/worker-runtime.d.ts.map +1 -0
- package/dist/worker-runtime.js +34 -0
- package/dist/worker-runtime.js.map +1 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +7 -0
- package/dist/worker.js.map +1 -0
- package/docs/layout-regressions.md +65 -0
- package/docs/pagination-regressions.md +7 -6
- package/package.json +5 -2
- package/src/api.ts +33 -22
- package/src/browser.ts +13 -3
- package/src/node-worker.ts +9 -0
- package/src/node.ts +13 -4
- package/src/render.ts +28 -0
- package/src/wasm.ts +17 -0
- package/src/worker-host.ts +45 -0
- package/src/worker-protocol.ts +10 -0
- package/src/worker-runtime.ts +35 -0
- package/src/worker.ts +12 -0
- package/vendor/README.md +35 -0
- package/vendor/blitz-dom/Cargo.toml +113 -0
- package/vendor/blitz-dom/LICENSE-APACHE +176 -0
- package/vendor/blitz-dom/LICENSE-MIT +23 -0
- package/vendor/blitz-dom/assets/default.css +1142 -0
- package/vendor/blitz-dom/assets/moz-bullet-font.otf +0 -0
- package/vendor/blitz-dom/src/accessibility.rs +255 -0
- package/vendor/blitz-dom/src/config.rs +97 -0
- package/vendor/blitz-dom/src/cssom.rs +971 -0
- package/vendor/blitz-dom/src/debug.rs +206 -0
- package/vendor/blitz-dom/src/document.rs +3395 -0
- package/vendor/blitz-dom/src/events/driver.rs +427 -0
- package/vendor/blitz-dom/src/events/focus.rs +40 -0
- package/vendor/blitz-dom/src/events/ime.rs +31 -0
- package/vendor/blitz-dom/src/events/keyboard.rs +174 -0
- package/vendor/blitz-dom/src/events/mod.rs +315 -0
- package/vendor/blitz-dom/src/events/pointer.rs +849 -0
- package/vendor/blitz-dom/src/font_metrics.rs +177 -0
- package/vendor/blitz-dom/src/form.rs +463 -0
- package/vendor/blitz-dom/src/html.rs +38 -0
- package/vendor/blitz-dom/src/iframe.rs +186 -0
- package/vendor/blitz-dom/src/layout/construct.rs +1328 -0
- package/vendor/blitz-dom/src/layout/damage.rs +642 -0
- package/vendor/blitz-dom/src/layout/inline.rs +1160 -0
- package/vendor/blitz-dom/src/layout/list.rs +237 -0
- package/vendor/blitz-dom/src/layout/mod.rs +969 -0
- package/vendor/blitz-dom/src/layout/paint_tree.rs +462 -0
- package/vendor/blitz-dom/src/layout/replaced.rs +439 -0
- package/vendor/blitz-dom/src/layout/table.rs +967 -0
- package/vendor/blitz-dom/src/layout/text_transform.rs +979 -0
- package/vendor/blitz-dom/src/layout/writing_mode.rs +437 -0
- package/vendor/blitz-dom/src/lib.rs +160 -0
- package/vendor/blitz-dom/src/mutator.rs +1818 -0
- package/vendor/blitz-dom/src/net.rs +649 -0
- package/vendor/blitz-dom/src/node/attributes.rs +60 -0
- package/vendor/blitz-dom/src/node/custom_widget.rs +178 -0
- package/vendor/blitz-dom/src/node/element.rs +1106 -0
- package/vendor/blitz-dom/src/node/mod.rs +30 -0
- package/vendor/blitz-dom/src/node/node.rs +2036 -0
- package/vendor/blitz-dom/src/node/scrollbar.rs +237 -0
- package/vendor/blitz-dom/src/node/serialize.rs +225 -0
- package/vendor/blitz-dom/src/node/stylo_data.rs +142 -0
- package/vendor/blitz-dom/src/node/svg.rs +265 -0
- package/vendor/blitz-dom/src/node/text.rs +804 -0
- package/vendor/blitz-dom/src/query_selector.rs +275 -0
- package/vendor/blitz-dom/src/resolve.rs +474 -0
- package/vendor/blitz-dom/src/resolved_style.rs +797 -0
- package/vendor/blitz-dom/src/scrolling.rs +780 -0
- package/vendor/blitz-dom/src/selection.rs +122 -0
- package/vendor/blitz-dom/src/stylo.rs +1518 -0
- package/vendor/blitz-dom/src/stylo_device.rs +88 -0
- package/vendor/blitz-dom/src/stylo_to_cursor_icon.rs +49 -0
- package/vendor/blitz-dom/src/stylo_to_kurbo.rs +129 -0
- package/vendor/blitz-dom/src/stylo_to_parley.rs +655 -0
- package/vendor/blitz-dom/src/traversal.rs +600 -0
- package/vendor/blitz-dom/src/tree.rs +136 -0
- package/vendor/blitz-dom/src/url.rs +63 -0
- package/vendor/blitz-dom/src/util.rs +256 -0
- package/wasm/blitz_pdf_bg.wasm +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Report margins and spacing
|
|
2
|
+
|
|
3
|
+
`tests/layout.test.ts` exercises the public `renderPdf` API against rebuilt
|
|
4
|
+
WASM. The underlying changes are in the vendored Blitz DOM crate; their
|
|
5
|
+
upstream revision and modified files are listed in `vendor/README.md`.
|
|
6
|
+
|
|
7
|
+
## Fractional geometry
|
|
8
|
+
|
|
9
|
+
Blitz normally rounds layout boxes to integer screen pixels. PDF physical
|
|
10
|
+
lengths need fractional coordinates: a 25 mm offset is about 94.488 CSS pixels,
|
|
11
|
+
not 94. Truncating the content viewport also moves right-aligned and
|
|
12
|
+
percentage-sized content. Body layout and header/footer templates now use the
|
|
13
|
+
exact CSS viewport and preserve box coordinates through painting and pagination.
|
|
14
|
+
Tests allow Stylo's 1/60-pixel length quantization.
|
|
15
|
+
|
|
16
|
+
## Table line heights and empty paragraphs
|
|
17
|
+
|
|
18
|
+
The root text style of an inline formatting context must use baseline
|
|
19
|
+
alignment. A table cell's `vertical-align: middle` belongs to placement inside
|
|
20
|
+
the row; passing it to the text builder inflated the cell's own lines too.
|
|
21
|
+
Tests compare ordinary blocks with cells containing small labels, values, and
|
|
22
|
+
trailing breaks, and retain descendant superscript and cell alignment coverage.
|
|
23
|
+
|
|
24
|
+
An empty paragraph containing a span must allow margins to collapse through it.
|
|
25
|
+
The previous empty-text shortcut reported a non-collapsible box, adding an
|
|
26
|
+
extra paragraph margin. It also bypassed explicit heights. Empty inline
|
|
27
|
+
contexts now follow normal sizing and collapse calculations; explicit
|
|
28
|
+
height, min-height, padding, borders, and real blank lines remain effective.
|
|
29
|
+
|
|
30
|
+
## Table widths and generated wrappers
|
|
31
|
+
|
|
32
|
+
The report's information table uses `max-width: 0` on its cells. That constraint
|
|
33
|
+
affects intrinsic measurement, while the final cells must fill their allocated
|
|
34
|
+
columns. Previously it also clamped the final content width to zero, producing
|
|
35
|
+
character-by-character wrapping. Tests cover auto/fixed tables and both inline
|
|
36
|
+
and block cell contents, including the full text of height-limited tables.
|
|
37
|
+
|
|
38
|
+
The report body uses `display: table` with ordinary headings, paragraphs, and
|
|
39
|
+
nested tables as children. These require anonymous rows and cells under
|
|
40
|
+
[CSS 2.1 section 17.2.1](https://www.w3.org/TR/CSS2/tables.html#anonymous-boxes).
|
|
41
|
+
Blitz previously discarded those children. Layout construction now wraps
|
|
42
|
+
consecutive improper children while preserving the author's DOM for selectors.
|
|
43
|
+
Tests compare implicit and explicit wrappers, whitespace, hidden siblings,
|
|
44
|
+
row inheritance, CSS page-size changes, and pagination with nested content.
|
|
45
|
+
|
|
46
|
+
## Project reproduction
|
|
47
|
+
|
|
48
|
+
The original offer template was expanded with its project's template compiler,
|
|
49
|
+
synthetic offer data, the unmodified DIN stylesheet, and the project's Noto Sans
|
|
50
|
+
font files. The report configuration defaults to `style_din5008`; its export
|
|
51
|
+
call passes `styleFile`, `fontFamily`, and A4 options, without `styleOverwrite`.
|
|
52
|
+
The stylesheet supplies the 27 mm top and 41 mm bottom page margins.
|
|
53
|
+
|
|
54
|
+
The resulting one-page report was rendered with Poppler and inspected, including
|
|
55
|
+
the address block, information table, greeting, line item, totals, and footer
|
|
56
|
+
text. Chromium DOM measurements verified column distribution and margin
|
|
57
|
+
behavior with the same HTML. Against the supplied Chromium PDF, the six shared
|
|
58
|
+
information-table labels have the same 21 pt baseline interval; their absolute
|
|
59
|
+
baselines differ by 0.118 pt. The delivery heading and greeting differ by
|
|
60
|
+
0.137 pt and 0.125 pt respectively, and the body left edge by 0.003 pt.
|
|
61
|
+
|
|
62
|
+
The supplied business PDFs contain different
|
|
63
|
+
data, and normal font metrics can vary across Chromium platforms; this is
|
|
64
|
+
coverage of the project template and layout regressions, not a pixel-equality
|
|
65
|
+
claim for different documents.
|
|
@@ -58,12 +58,10 @@ width of about 283.46 pixels, cell widths of about 141.73 pixels and a total
|
|
|
58
58
|
height of 123 pixels. Changing `max-width` to `none` produces the same
|
|
59
59
|
dimensions for these equal cell contents.
|
|
60
60
|
|
|
61
|
-
Blitz's final cell layout instead has width 2 pixels (zero content width plus
|
|
61
|
+
Unpatched Blitz's final cell layout instead has width 2 pixels (zero content width plus
|
|
62
62
|
padding), height 78 pixels, and four 19-pixel lines for `Text`. Its table grid
|
|
63
63
|
adapts cells to grid items while retaining `max_size.width`; the inline layout
|
|
64
|
-
therefore receives a zero-width constraint.
|
|
65
|
-
compatibility difference. It does not establish a general correction to the
|
|
66
|
-
CSS table-width algorithm, which remains a separate upstream task.
|
|
64
|
+
therefore receives a zero-width constraint.
|
|
67
65
|
|
|
68
66
|
The resulting tall table exceeds its container's height. Pagination previously
|
|
69
67
|
used line and row ranges beyond the overflow clip; the document end inside a
|
|
@@ -73,8 +71,11 @@ glyph bounds with the scene's nested clips. PDF text emission uses the same
|
|
|
73
71
|
clip bounds to omit fully hidden runs. Regression coverage includes both
|
|
74
72
|
`max-width` values, nested `hidden`/`clip` containers and clipped oversized ink.
|
|
75
73
|
|
|
76
|
-
The
|
|
77
|
-
|
|
74
|
+
The subsequent [layout patch](layout-regressions.md) separates the intrinsic
|
|
75
|
+
width constraint from the final allocated column width. The zero-width case
|
|
76
|
+
now renders all twelve cells, and the test requires that content for both
|
|
77
|
+
`max-width` values. This fixes the reproduction without replacing the entire
|
|
78
|
+
table width algorithm.
|
|
78
79
|
|
|
79
80
|
## Diagnostic distinctions
|
|
80
81
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitz-pdf",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Render HTML and CSS to PDF with Blitz and WebAssembly in Node.js, Bun, and browsers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=20"
|
|
17
17
|
},
|
|
18
|
-
"sideEffects":
|
|
18
|
+
"sideEffects": [
|
|
19
|
+
"./dist/worker.js",
|
|
20
|
+
"./dist/node-worker.js"
|
|
21
|
+
],
|
|
19
22
|
"main": "./dist/node.js",
|
|
20
23
|
"module": "./dist/browser.js",
|
|
21
24
|
"types": "./dist/node.d.ts",
|
package/src/api.ts
CHANGED
|
@@ -1,44 +1,55 @@
|
|
|
1
|
-
import loadWasm, {
|
|
1
|
+
import loadWasm, { engine_version } from "../wasm/blitz_pdf.js";
|
|
2
2
|
import { createAssetLoader } from "./assets.js";
|
|
3
3
|
import { normalizeOptions } from "./options.js";
|
|
4
|
+
import { render, validateHtml } from "./render.js";
|
|
5
|
+
import { compileWasm } from "./wasm.js";
|
|
6
|
+
import { renderInWorker, type SpawnWorker } from "./worker-host.js";
|
|
4
7
|
import type { InitOptions, PdfOptions, WasmSource } from "./types.js";
|
|
5
8
|
|
|
6
|
-
export
|
|
7
|
-
|
|
9
|
+
export interface ApiConfig {
|
|
10
|
+
defaultSource: () => Promise<WasmSource> | WasmSource;
|
|
11
|
+
prepare?: (source: WasmSource) => Promise<WasmSource>;
|
|
12
|
+
spawnWorker: SpawnWorker;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createApi({ defaultSource, prepare = async source => source, spawnWorker }: ApiConfig) {
|
|
16
|
+
let compiled: Promise<WebAssembly.Module> | undefined;
|
|
8
17
|
let ready: Promise<void> | undefined;
|
|
9
18
|
|
|
19
|
+
function compile(source?: WasmSource): Promise<WebAssembly.Module> {
|
|
20
|
+
if (compiled) return compiled;
|
|
21
|
+
compiled = (async () => compileWasm(await prepare(source ?? await defaultSource())))()
|
|
22
|
+
.catch((error: unknown) => { compiled = undefined; throw error; });
|
|
23
|
+
return compiled;
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
/** Initialize once. Concurrent callers share initialization; failed loads can be retried. */
|
|
11
27
|
function init(options: InitOptions = {}): Promise<void> {
|
|
12
28
|
if (ready) return ready;
|
|
13
29
|
ready = (async () => {
|
|
14
|
-
|
|
15
|
-
await loadWasm({ module_or_path: source });
|
|
30
|
+
await loadWasm({ module_or_path: await compile(options.wasm) });
|
|
16
31
|
})().catch((error: unknown) => { ready = undefined; throw error; });
|
|
17
32
|
return ready;
|
|
18
33
|
}
|
|
19
34
|
|
|
35
|
+
/** The compiled WASM module, compiled once and shared with every worker. It can be posted to your own workers. */
|
|
36
|
+
function getWasmModule(): Promise<WebAssembly.Module> { return compile(); }
|
|
37
|
+
|
|
20
38
|
/** Render HTML/CSS to a vector PDF/A-3u with embedded fonts and selectable text. */
|
|
21
39
|
async function renderPdf(html: string, options: PdfOptions = {}): Promise<Uint8Array> {
|
|
22
|
-
|
|
23
|
-
if (new TextEncoder().encode(html).byteLength > 16 * 1024 * 1024) throw new RangeError("HTML exceeds the 16 MiB limit");
|
|
40
|
+
validateHtml(html);
|
|
24
41
|
const { assetResolver, ...normalized } = normalizeOptions(options);
|
|
25
42
|
await init();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for (let i = 0; i < urls.length; i++) job.provide_asset(urls[i]!, data[i]!);
|
|
35
|
-
}
|
|
36
|
-
} finally {
|
|
37
|
-
// In-flight JS loaders never access this job, including after a sibling rejects.
|
|
38
|
-
job.free();
|
|
39
|
-
}
|
|
43
|
+
return render(html, normalized, assetResolver && createAssetLoader(assetResolver));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Like `renderPdf`, but layout and serialization run in a new worker. An `assets` function still runs on the calling thread. */
|
|
47
|
+
async function renderPdfInWorker(html: string, options: PdfOptions = {}): Promise<Uint8Array> {
|
|
48
|
+
validateHtml(html);
|
|
49
|
+
const { assetResolver, ...normalized } = normalizeOptions(options);
|
|
50
|
+
return renderInWorker(spawnWorker, await compile(), html, normalized, assetResolver);
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
async function getEngineVersion(): Promise<string> { await init(); return engine_version(); }
|
|
43
|
-
return { init, renderPdf, getEngineVersion };
|
|
54
|
+
return { init, renderPdf, renderPdfInWorker, getWasmModule, getEngineVersion };
|
|
44
55
|
}
|
package/src/browser.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { createApi } from "./api.js";
|
|
2
2
|
|
|
3
3
|
export type { AssetData, AssetMap, AssetResolver, AttachmentRelationship, BinaryData, InitOptions, Length, PageFormat, PageNumberOptions, PdfAttachment, PdfDate, PdfMetadata, PdfOptions, PdfXmpProperty, PdfXmpSchema, WasmSource } from "./types.js";
|
|
4
|
-
export const { init, renderPdf, getEngineVersion } = createApi(
|
|
5
|
-
() => new URL("../wasm/blitz_pdf_bg.wasm", import.meta.url),
|
|
6
|
-
|
|
4
|
+
export const { init, renderPdf, renderPdfInWorker, getWasmModule, getEngineVersion } = createApi({
|
|
5
|
+
defaultSource: () => new URL("../wasm/blitz_pdf_bg.wasm", import.meta.url),
|
|
6
|
+
spawnWorker: events => {
|
|
7
|
+
const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
|
|
8
|
+
worker.onmessage = event => events.message(event.data);
|
|
9
|
+
worker.onerror = event => {
|
|
10
|
+
event.preventDefault();
|
|
11
|
+
events.error(new Error(`Render worker failed: ${event.message || "unknown error"}`));
|
|
12
|
+
};
|
|
13
|
+
worker.onmessageerror = () => events.error(new Error("Render worker sent a message that could not be deserialized"));
|
|
14
|
+
return { post: (message, transfer) => worker.postMessage(message, transfer), terminate: () => worker.terminate() };
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { serve } from "./worker-runtime.js";
|
|
3
|
+
|
|
4
|
+
const port = parentPort;
|
|
5
|
+
if (!port) throw new Error("blitz-pdf node-worker must run in a worker thread");
|
|
6
|
+
serve({
|
|
7
|
+
post: (message, transfer) => port.postMessage(message, transfer),
|
|
8
|
+
onMessage: handler => { port.on("message", handler); },
|
|
9
|
+
});
|
package/src/node.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
2
3
|
import { createApi } from "./api.js";
|
|
3
4
|
|
|
4
5
|
export type { AssetData, AssetMap, AssetResolver, AttachmentRelationship, BinaryData, InitOptions, Length, PageFormat, PageNumberOptions, PdfAttachment, PdfDate, PdfMetadata, PdfOptions, PdfXmpProperty, PdfXmpSchema, WasmSource } from "./types.js";
|
|
5
|
-
export const { init, renderPdf, getEngineVersion } = createApi(
|
|
6
|
-
() => readFile(new URL("../wasm/blitz_pdf_bg.wasm", import.meta.url)),
|
|
7
|
-
async source => {
|
|
6
|
+
export const { init, renderPdf, renderPdfInWorker, getWasmModule, getEngineVersion } = createApi({
|
|
7
|
+
defaultSource: () => readFile(new URL("../wasm/blitz_pdf_bg.wasm", import.meta.url)),
|
|
8
|
+
prepare: async source => {
|
|
8
9
|
if (source instanceof URL && source.protocol === "file:") return readFile(source);
|
|
9
10
|
if (typeof source === "string" && source.startsWith("file:")) return readFile(new URL(source));
|
|
10
11
|
return source;
|
|
11
12
|
},
|
|
12
|
-
|
|
13
|
+
spawnWorker: events => {
|
|
14
|
+
const worker = new Worker(new URL("./node-worker.js", import.meta.url));
|
|
15
|
+
worker.on("message", events.message);
|
|
16
|
+
worker.on("error", events.error);
|
|
17
|
+
worker.on("messageerror", events.error);
|
|
18
|
+
worker.on("exit", code => events.error(new Error(`Render worker exited with code ${code}`)));
|
|
19
|
+
return { post: (message, transfer) => worker.postMessage(message, transfer), terminate: () => void worker.terminate() };
|
|
20
|
+
},
|
|
21
|
+
});
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { render_pdf, RenderJob } from "../wasm/blitz_pdf.js";
|
|
2
|
+
import type { normalizeOptions } from "./options.js";
|
|
3
|
+
|
|
4
|
+
/** Normalized options without the resolver function, so they can be posted to a worker. */
|
|
5
|
+
export type RenderOptions = Omit<ReturnType<typeof normalizeOptions>, "assetResolver">;
|
|
6
|
+
export type LoadAssets = (urls: string[]) => Promise<Uint8Array[]>;
|
|
7
|
+
|
|
8
|
+
export function validateHtml(html: unknown): asserts html is string {
|
|
9
|
+
if (typeof html !== "string") throw new TypeError("html must be a string");
|
|
10
|
+
if (new TextEncoder().encode(html).byteLength > 16 * 1024 * 1024) throw new RangeError("HTML exceeds the 16 MiB limit");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Requires an initialized WASM instance in the current thread. */
|
|
14
|
+
export async function render(html: string, options: RenderOptions, load?: LoadAssets): Promise<Uint8Array> {
|
|
15
|
+
if (!load) return render_pdf(html, options);
|
|
16
|
+
const job = new RenderJob(html, options);
|
|
17
|
+
try {
|
|
18
|
+
for (;;) {
|
|
19
|
+
const urls = job.pending_assets();
|
|
20
|
+
if (urls.length === 0) return job.finish();
|
|
21
|
+
const data = await load(urls);
|
|
22
|
+
for (let i = 0; i < urls.length; i++) job.provide_asset(urls[i]!, data[i]!);
|
|
23
|
+
}
|
|
24
|
+
} finally {
|
|
25
|
+
// In-flight JS loaders never access this job, including after a sibling rejects.
|
|
26
|
+
job.free();
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/wasm.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WasmSource } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export async function compileWasm(source: WasmSource): Promise<WebAssembly.Module> {
|
|
4
|
+
if (source instanceof WebAssembly.Module) return source;
|
|
5
|
+
if (typeof source === "string" || source instanceof URL || (typeof Request === "function" && source instanceof Request)) {
|
|
6
|
+
source = await fetch(source);
|
|
7
|
+
}
|
|
8
|
+
if (typeof Response === "function" && source instanceof Response) {
|
|
9
|
+
if (!source.ok) throw new Error(`Failed to fetch WASM: ${source.status} ${source.statusText} fetching '${source.url}'`);
|
|
10
|
+
// Streaming compilation rejects other MIME types, so fall back to buffering instead.
|
|
11
|
+
if (typeof WebAssembly.compileStreaming === "function" && source.headers.get("Content-Type") === "application/wasm") {
|
|
12
|
+
return WebAssembly.compileStreaming(source);
|
|
13
|
+
}
|
|
14
|
+
return WebAssembly.compile(await source.arrayBuffer());
|
|
15
|
+
}
|
|
16
|
+
return WebAssembly.compile(source as BufferSource);
|
|
17
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createAssetLoader } from "./assets.js";
|
|
2
|
+
import type { RenderOptions } from "./render.js";
|
|
3
|
+
import type { AssetResolver } from "./types.js";
|
|
4
|
+
import type { HostMessage, WorkerMessage } from "./worker-protocol.js";
|
|
5
|
+
|
|
6
|
+
export interface WorkerEvents {
|
|
7
|
+
message(message: WorkerMessage): void;
|
|
8
|
+
/** Crashes, unreadable messages, or an exit before the render settled. */
|
|
9
|
+
error(error: unknown): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WorkerHandle {
|
|
13
|
+
post(message: HostMessage, transfer: ArrayBuffer[]): void;
|
|
14
|
+
terminate(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SpawnWorker = (events: WorkerEvents) => WorkerHandle;
|
|
18
|
+
|
|
19
|
+
/** Renders in a fresh worker that instantiates the precompiled module, then terminates it. */
|
|
20
|
+
export function renderInWorker(spawn: SpawnWorker, module: WebAssembly.Module, html: string,
|
|
21
|
+
options: RenderOptions, resolver: AssetResolver | undefined): Promise<Uint8Array> {
|
|
22
|
+
return new Promise<Uint8Array>((resolve, reject) => {
|
|
23
|
+
const load = resolver && createAssetLoader(resolver);
|
|
24
|
+
let settled = false;
|
|
25
|
+
const settle = (callback: () => void) => {
|
|
26
|
+
if (settled) return;
|
|
27
|
+
settled = true;
|
|
28
|
+
worker.terminate();
|
|
29
|
+
callback();
|
|
30
|
+
};
|
|
31
|
+
const worker: WorkerHandle = spawn({
|
|
32
|
+
message(message) {
|
|
33
|
+
if (message.type === "done") settle(() => resolve(message.pdf));
|
|
34
|
+
else if (message.type === "error") settle(() => reject(message.error));
|
|
35
|
+
else if (!load) settle(() => reject(new Error("Render worker requested assets without a resolver")));
|
|
36
|
+
else load(message.urls).then(
|
|
37
|
+
data => { if (!settled) worker.post({ type: "assets", data }, data.map(bytes => bytes.buffer as ArrayBuffer)); },
|
|
38
|
+
(error: unknown) => settle(() => reject(error)),
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
error(error) { settle(() => reject(error)); },
|
|
42
|
+
});
|
|
43
|
+
worker.post({ type: "render", module, html, options, external: load !== undefined }, []);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RenderOptions } from "./render.js";
|
|
2
|
+
|
|
3
|
+
export type HostMessage =
|
|
4
|
+
| { type: "render"; module: WebAssembly.Module; html: string; options: RenderOptions; external: boolean }
|
|
5
|
+
| { type: "assets"; data: Uint8Array[] };
|
|
6
|
+
|
|
7
|
+
export type WorkerMessage =
|
|
8
|
+
| { type: "assets"; urls: string[] }
|
|
9
|
+
| { type: "done"; pdf: Uint8Array }
|
|
10
|
+
| { type: "error"; error: unknown };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { initSync } from "../wasm/blitz_pdf.js";
|
|
2
|
+
import { render } from "./render.js";
|
|
3
|
+
import type { HostMessage, WorkerMessage } from "./worker-protocol.js";
|
|
4
|
+
|
|
5
|
+
export interface WorkerEndpoint {
|
|
6
|
+
post(message: WorkerMessage, transfer: ArrayBuffer[]): void;
|
|
7
|
+
onMessage(handler: (message: HostMessage) => void): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Worker side of `renderPdfInWorker`: one render per worker; asset bytes come from the host. */
|
|
11
|
+
export function serve(endpoint: WorkerEndpoint): void {
|
|
12
|
+
let receiveAssets: ((data: Uint8Array[]) => void) | undefined;
|
|
13
|
+
endpoint.onMessage(async message => {
|
|
14
|
+
if (message.type === "assets") {
|
|
15
|
+
const receive = receiveAssets;
|
|
16
|
+
receiveAssets = undefined;
|
|
17
|
+
receive?.(message.data);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
initSync({ module: message.module });
|
|
22
|
+
const load = message.external
|
|
23
|
+
? (urls: string[]) => new Promise<Uint8Array[]>(resolve => {
|
|
24
|
+
receiveAssets = resolve;
|
|
25
|
+
endpoint.post({ type: "assets", urls }, []);
|
|
26
|
+
})
|
|
27
|
+
: undefined;
|
|
28
|
+
const pdf = await render(message.html, message.options, load);
|
|
29
|
+
endpoint.post({ type: "done", pdf }, [pdf.buffer as ArrayBuffer]);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
try { endpoint.post({ type: "error", error }, []); }
|
|
32
|
+
catch { endpoint.post({ type: "error", error: new Error(String(error)) }, []); }
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { serve } from "./worker-runtime.js";
|
|
2
|
+
|
|
3
|
+
interface DedicatedWorkerScope {
|
|
4
|
+
postMessage(message: unknown, transfer: Transferable[]): void;
|
|
5
|
+
onmessage: ((event: MessageEvent) => void) | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const scope = globalThis as unknown as DedicatedWorkerScope;
|
|
9
|
+
serve({
|
|
10
|
+
post: (message, transfer) => scope.postMessage(message, transfer),
|
|
11
|
+
onMessage: handler => { scope.onmessage = event => handler(event.data); },
|
|
12
|
+
});
|
package/vendor/README.md
CHANGED
|
@@ -40,3 +40,38 @@ No invoice-specific behavior is added to the engine or public API.
|
|
|
40
40
|
|
|
41
41
|
When upgrading Krilla, review this patch and run `tests/document.test.ts`, the
|
|
42
42
|
Node/browser checks, and `npm run test:pdfa` against the rebuilt WASM.
|
|
43
|
+
|
|
44
|
+
# Blitz print layout patch
|
|
45
|
+
|
|
46
|
+
`blitz-dom/` contains `packages/blitz-dom` from Blitz revision
|
|
47
|
+
`e72650a6e67941852fddf025295cfee30c39ea32`, with its MIT/Apache-2.0 licenses.
|
|
48
|
+
Its standalone manifest expands the upstream workspace dependencies and pins
|
|
49
|
+
the sibling Blitz crates to the same revision.
|
|
50
|
+
|
|
51
|
+
- `config.rs`, `document.rs`, `stylo_device.rs`, `iframe.rs`, `resolve.rs`, and
|
|
52
|
+
`layout/writing_mode.rs`: provide an optional fractional CSS viewport and
|
|
53
|
+
optional layout rounding. Defaults preserve the upstream window behavior.
|
|
54
|
+
The PDF body and margin templates disable rounding and supply the exact
|
|
55
|
+
printable dimensions, including after CSS page setup changes them. Layout,
|
|
56
|
+
transforms, painting, and pagination share the resulting geometry.
|
|
57
|
+
- `layout/construct.rs`: use baseline alignment for the root of an inline
|
|
58
|
+
formatting context. A table cell's `vertical-align: middle` aligns the cell
|
|
59
|
+
content within its row; applying it again to its own text inflated every line.
|
|
60
|
+
Descendant inline alignment and the cell's placement remain intact.
|
|
61
|
+
- `layout/inline.rs`: let empty inline contexts compute their size and margin
|
|
62
|
+
collapse normally. The empty-text shortcut prevented margins from collapsing
|
|
63
|
+
through paragraphs containing empty spans and discarded explicit heights.
|
|
64
|
+
- `layout/table.rs`: keep `max-width` as an intrinsic measurement constraint,
|
|
65
|
+
while allowing the final cell layout to use the width allocated by the grid.
|
|
66
|
+
Generate anonymous row/cell wrappers around improper table children, including
|
|
67
|
+
block content in a `display: table` container. `layout/construct.rs` creates
|
|
68
|
+
the wrappers without altering the DOM; `layout/damage.rs` retains their roles
|
|
69
|
+
and inherited styles when refreshing them. Ownership tracking frees them on
|
|
70
|
+
reconstruction.
|
|
71
|
+
|
|
72
|
+
When upgrading Blitz, review these changes against upstream and run
|
|
73
|
+
`tests/layout.test.ts`, `tests/pagination.test.ts`, and `tests/reports.test.ts`
|
|
74
|
+
against rebuilt WASM. Layout tests check physical margins, fractional viewport
|
|
75
|
+
widths, header/footer geometry, table line spacing, descendant alignment, empty
|
|
76
|
+
paragraphs, explicit empty-box dimensions, exact page-boundary fits, constrained
|
|
77
|
+
table columns, anonymous cells, and inheritance after CSS page-size changes.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "blitz-dom"
|
|
3
|
+
description = "Blitz DOM implementation"
|
|
4
|
+
documentation = "https://docs.rs/blitz-dom"
|
|
5
|
+
version = "0.3.0-beta.3"
|
|
6
|
+
license = "MIT OR Apache-2.0"
|
|
7
|
+
homepage = "https://github.com/dioxuslabs/blitz"
|
|
8
|
+
repository = "https://github.com/dioxuslabs/blitz"
|
|
9
|
+
categories = ["gui"]
|
|
10
|
+
edition = "2024"
|
|
11
|
+
rust-version = "1.91.0"
|
|
12
|
+
|
|
13
|
+
[features]
|
|
14
|
+
default = [
|
|
15
|
+
"svg",
|
|
16
|
+
"woff",
|
|
17
|
+
"accessibility",
|
|
18
|
+
"system-fonts",
|
|
19
|
+
"file-input",
|
|
20
|
+
"custom-widget",
|
|
21
|
+
"text-transform-icu"
|
|
22
|
+
]
|
|
23
|
+
custom-widget = ["dep:anyrender", "accessibility"]
|
|
24
|
+
tracing = ["dep:tracing"]
|
|
25
|
+
svg = ["dep:usvg", "dep:svgtypes"]
|
|
26
|
+
# WOFF decoding (using the "wuff" crate which is pure Rust)
|
|
27
|
+
woff = ["dep:wuff"]
|
|
28
|
+
accessibility = ["accesskit"]
|
|
29
|
+
system-fonts = ["parley/system"]
|
|
30
|
+
# complex-scripts: enable dictionary-based line-breaking
|
|
31
|
+
complex-scripts = ["parley/complex-scripts"]
|
|
32
|
+
# text-transform-icu: language-aware, Unicode-complete case mapping for `text-transform`
|
|
33
|
+
# (otherwise falls back to the standard library's case mapping)
|
|
34
|
+
text-transform-icu = ["dep:icu_casemap", "dep:writeable"]
|
|
35
|
+
autofocus = []
|
|
36
|
+
floats = ["taffy/float_layout", "stylo_taffy/floats"]
|
|
37
|
+
# CSS vertical `writing-mode` support (layout runs in each box's writing-mode frame; see layout/writing_mode.rs)
|
|
38
|
+
writing-mode = ["stylo_taffy/writing-mode"]
|
|
39
|
+
file-input = []
|
|
40
|
+
# Overlay-scrollbar thumb interaction (drag + hover tracking). Normally
|
|
41
|
+
# enabled through `blitz-paint/scrollbars`, which paints the thumbs.
|
|
42
|
+
scrollbars = []
|
|
43
|
+
parallel-construct = []
|
|
44
|
+
log-phase-times = ["debug_timer/enable"]
|
|
45
|
+
|
|
46
|
+
[dependencies]
|
|
47
|
+
# Blitz dependencies
|
|
48
|
+
blitz-traits = { version = "=0.3.0-beta.3", default-features = false, git = "https://github.com/DioxusLabs/blitz", rev = "e72650a6e67941852fddf025295cfee30c39ea32" }
|
|
49
|
+
stylo_taffy = { version = "=0.3.0-beta.3", default-features = false, git = "https://github.com/DioxusLabs/blitz", rev = "e72650a6e67941852fddf025295cfee30c39ea32", features = ["default"] }
|
|
50
|
+
debug_timer = { version = "0.1.2", git = "https://github.com/DioxusLabs/blitz", rev = "e72650a6e67941852fddf025295cfee30c39ea32" }
|
|
51
|
+
|
|
52
|
+
# Servo dependencies
|
|
53
|
+
style = { version = "0.22.0", package = "stylo" }
|
|
54
|
+
selectors = { version = "0.41.0", package = "selectors" }
|
|
55
|
+
cssparser = { version = "0.38.0" }
|
|
56
|
+
style_config = { version = "0.22.0", package = "stylo_static_prefs" }
|
|
57
|
+
style_traits = { version = "0.22.0", package = "stylo_traits" }
|
|
58
|
+
style_dom = { version = "0.22.0", package = "stylo_dom" }
|
|
59
|
+
app_units = { version = "0.7.5" }
|
|
60
|
+
euclid = { version = "0.22" }
|
|
61
|
+
atomic_refcell = { version = "0.1.13" }
|
|
62
|
+
markup5ever = { version = "0.40.0" }
|
|
63
|
+
smallvec = { version = "1" }
|
|
64
|
+
thin-vec = { version = "0.2" }
|
|
65
|
+
|
|
66
|
+
# DioxusLabs dependencies
|
|
67
|
+
taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "6c2106e3df79493d304f954dc2cd4a4a382981d3", default-features = false, features = ["std", "flexbox", "flexbox_balance", "grid", "block_layout", "content_size", "calc", "detailed_layout_info"] }
|
|
68
|
+
anyrender = { version = "0.14.0", optional = true }
|
|
69
|
+
|
|
70
|
+
# Linebender/Fontations dependencies
|
|
71
|
+
accesskit = { version = "0.25", optional = true }
|
|
72
|
+
parley = { version = "0.12", default-features = false, features = ["std"] }
|
|
73
|
+
skrifa = { version = "0.44", default-features = false, features = ["std"] }
|
|
74
|
+
linebender_resource_handle = { version = "0.1" }
|
|
75
|
+
color = { version = "0.3" }
|
|
76
|
+
kurbo = { version = "0.13.1" }
|
|
77
|
+
|
|
78
|
+
# Unicode
|
|
79
|
+
icu_casemap = { version = "2.3", default-features = false, features = ["compiled_data"], optional = true }
|
|
80
|
+
icu_locale_core = { version = "2.3", default-features = false }
|
|
81
|
+
icu_properties = { version = "2.3", default-features = false, features = ["compiled_data"] }
|
|
82
|
+
icu_segmenter = { version = "2.3", default-features = false, features = ["compiled_data"] }
|
|
83
|
+
writeable = { version = "0.6", default-features = false, optional = true }
|
|
84
|
+
|
|
85
|
+
# Other dependencies
|
|
86
|
+
slotmap = { version = "1.0.7" }
|
|
87
|
+
bitflags = { version = "2.8.0" }
|
|
88
|
+
tracing = { version = "0.1.40", optional = true }
|
|
89
|
+
rayon = { version = "1" }
|
|
90
|
+
|
|
91
|
+
# Media & Decoding
|
|
92
|
+
image = { version = "0.25.6", default-features = false }
|
|
93
|
+
usvg = { version = "0.48.1", optional = true }
|
|
94
|
+
svgtypes = { version = "0.16.1", optional = true }
|
|
95
|
+
wuff = { version = "0.2", optional = true }
|
|
96
|
+
html-escape = { version = "0.2.13" }
|
|
97
|
+
percent-encoding = { version = "2.3.1" }
|
|
98
|
+
|
|
99
|
+
# IO & Networking
|
|
100
|
+
url = { version = "2.5.0" }
|
|
101
|
+
|
|
102
|
+
# Input
|
|
103
|
+
keyboard-types = { version = "0.7" }
|
|
104
|
+
cursor-icon = { version = "1" }
|
|
105
|
+
thread_local = { version = "1" }
|
|
106
|
+
|
|
107
|
+
# WASM
|
|
108
|
+
web-time = { version = "1" }
|
|
109
|
+
|
|
110
|
+
# HACK: Blitz doesn't need to depend on objc2 directly. But this feature flag is necessary
|
|
111
|
+
# to prevent debug builds from panicking.
|
|
112
|
+
[target.'cfg(any(target_vendor = "apple"))'.dependencies]
|
|
113
|
+
objc2 = { version = "0.6", features = ["disable-encoding-assertions"] }
|