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.
Files changed (114) hide show
  1. package/README.md +35 -6
  2. package/THIRD_PARTY_NOTICES.md +5 -2
  3. package/dist/api.d.ts +9 -1
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +25 -27
  6. package/dist/api.js.map +1 -1
  7. package/dist/browser.d.ts +1 -1
  8. package/dist/browser.d.ts.map +1 -1
  9. package/dist/browser.js +13 -1
  10. package/dist/browser.js.map +1 -1
  11. package/dist/node-worker.d.ts +2 -0
  12. package/dist/node-worker.d.ts.map +1 -0
  13. package/dist/node-worker.js +10 -0
  14. package/dist/node-worker.js.map +1 -0
  15. package/dist/node.d.ts +1 -1
  16. package/dist/node.d.ts.map +1 -1
  17. package/dist/node.js +18 -6
  18. package/dist/node.js.map +1 -1
  19. package/dist/render.d.ts +8 -0
  20. package/dist/render.d.ts.map +1 -0
  21. package/dist/render.js +28 -0
  22. package/dist/render.js.map +1 -0
  23. package/dist/wasm.d.ts +3 -0
  24. package/dist/wasm.d.ts.map +1 -0
  25. package/dist/wasm.js +18 -0
  26. package/dist/wasm.js.map +1 -0
  27. package/dist/worker-host.d.ts +16 -0
  28. package/dist/worker-host.d.ts.map +1 -0
  29. package/dist/worker-host.js +31 -0
  30. package/dist/worker-host.js.map +1 -0
  31. package/dist/worker-protocol.d.ts +22 -0
  32. package/dist/worker-protocol.d.ts.map +1 -0
  33. package/dist/worker-protocol.js +2 -0
  34. package/dist/worker-protocol.js.map +1 -0
  35. package/dist/worker-runtime.d.ts +8 -0
  36. package/dist/worker-runtime.d.ts.map +1 -0
  37. package/dist/worker-runtime.js +34 -0
  38. package/dist/worker-runtime.js.map +1 -0
  39. package/dist/worker.d.ts +2 -0
  40. package/dist/worker.d.ts.map +1 -0
  41. package/dist/worker.js +7 -0
  42. package/dist/worker.js.map +1 -0
  43. package/docs/layout-regressions.md +65 -0
  44. package/docs/pagination-regressions.md +7 -6
  45. package/package.json +5 -2
  46. package/src/api.ts +33 -22
  47. package/src/browser.ts +13 -3
  48. package/src/node-worker.ts +9 -0
  49. package/src/node.ts +13 -4
  50. package/src/render.ts +28 -0
  51. package/src/wasm.ts +17 -0
  52. package/src/worker-host.ts +45 -0
  53. package/src/worker-protocol.ts +10 -0
  54. package/src/worker-runtime.ts +35 -0
  55. package/src/worker.ts +12 -0
  56. package/vendor/README.md +35 -0
  57. package/vendor/blitz-dom/Cargo.toml +113 -0
  58. package/vendor/blitz-dom/LICENSE-APACHE +176 -0
  59. package/vendor/blitz-dom/LICENSE-MIT +23 -0
  60. package/vendor/blitz-dom/assets/default.css +1142 -0
  61. package/vendor/blitz-dom/assets/moz-bullet-font.otf +0 -0
  62. package/vendor/blitz-dom/src/accessibility.rs +255 -0
  63. package/vendor/blitz-dom/src/config.rs +97 -0
  64. package/vendor/blitz-dom/src/cssom.rs +971 -0
  65. package/vendor/blitz-dom/src/debug.rs +206 -0
  66. package/vendor/blitz-dom/src/document.rs +3395 -0
  67. package/vendor/blitz-dom/src/events/driver.rs +427 -0
  68. package/vendor/blitz-dom/src/events/focus.rs +40 -0
  69. package/vendor/blitz-dom/src/events/ime.rs +31 -0
  70. package/vendor/blitz-dom/src/events/keyboard.rs +174 -0
  71. package/vendor/blitz-dom/src/events/mod.rs +315 -0
  72. package/vendor/blitz-dom/src/events/pointer.rs +849 -0
  73. package/vendor/blitz-dom/src/font_metrics.rs +177 -0
  74. package/vendor/blitz-dom/src/form.rs +463 -0
  75. package/vendor/blitz-dom/src/html.rs +38 -0
  76. package/vendor/blitz-dom/src/iframe.rs +186 -0
  77. package/vendor/blitz-dom/src/layout/construct.rs +1328 -0
  78. package/vendor/blitz-dom/src/layout/damage.rs +642 -0
  79. package/vendor/blitz-dom/src/layout/inline.rs +1160 -0
  80. package/vendor/blitz-dom/src/layout/list.rs +237 -0
  81. package/vendor/blitz-dom/src/layout/mod.rs +969 -0
  82. package/vendor/blitz-dom/src/layout/paint_tree.rs +462 -0
  83. package/vendor/blitz-dom/src/layout/replaced.rs +439 -0
  84. package/vendor/blitz-dom/src/layout/table.rs +967 -0
  85. package/vendor/blitz-dom/src/layout/text_transform.rs +979 -0
  86. package/vendor/blitz-dom/src/layout/writing_mode.rs +437 -0
  87. package/vendor/blitz-dom/src/lib.rs +160 -0
  88. package/vendor/blitz-dom/src/mutator.rs +1818 -0
  89. package/vendor/blitz-dom/src/net.rs +649 -0
  90. package/vendor/blitz-dom/src/node/attributes.rs +60 -0
  91. package/vendor/blitz-dom/src/node/custom_widget.rs +178 -0
  92. package/vendor/blitz-dom/src/node/element.rs +1106 -0
  93. package/vendor/blitz-dom/src/node/mod.rs +30 -0
  94. package/vendor/blitz-dom/src/node/node.rs +2036 -0
  95. package/vendor/blitz-dom/src/node/scrollbar.rs +237 -0
  96. package/vendor/blitz-dom/src/node/serialize.rs +225 -0
  97. package/vendor/blitz-dom/src/node/stylo_data.rs +142 -0
  98. package/vendor/blitz-dom/src/node/svg.rs +265 -0
  99. package/vendor/blitz-dom/src/node/text.rs +804 -0
  100. package/vendor/blitz-dom/src/query_selector.rs +275 -0
  101. package/vendor/blitz-dom/src/resolve.rs +474 -0
  102. package/vendor/blitz-dom/src/resolved_style.rs +797 -0
  103. package/vendor/blitz-dom/src/scrolling.rs +780 -0
  104. package/vendor/blitz-dom/src/selection.rs +122 -0
  105. package/vendor/blitz-dom/src/stylo.rs +1518 -0
  106. package/vendor/blitz-dom/src/stylo_device.rs +88 -0
  107. package/vendor/blitz-dom/src/stylo_to_cursor_icon.rs +49 -0
  108. package/vendor/blitz-dom/src/stylo_to_kurbo.rs +129 -0
  109. package/vendor/blitz-dom/src/stylo_to_parley.rs +655 -0
  110. package/vendor/blitz-dom/src/traversal.rs +600 -0
  111. package/vendor/blitz-dom/src/tree.rs +136 -0
  112. package/vendor/blitz-dom/src/url.rs +63 -0
  113. package/vendor/blitz-dom/src/util.rs +256 -0
  114. 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. This confirms a browser
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 zero-width case now exports, but its column widths still differ from the
77
- browser. Its test deliberately does not claim that all twelve cells are visible.
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",
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": false,
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, { render_pdf, engine_version, RenderJob } from "../wasm/blitz_pdf.js";
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 function createApi(defaultSource: () => Promise<WasmSource> | WasmSource,
7
- prepare: (source: WasmSource) => Promise<WasmSource> = async source => source) {
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
- const source = await prepare(options.wasm ?? await defaultSource());
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
- if (typeof html !== "string") throw new TypeError("html must be a string");
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
- if (!assetResolver) return render_pdf(html, normalized);
27
- const load = createAssetLoader(assetResolver);
28
- const job = new RenderJob(html, normalized);
29
- try {
30
- for (;;) {
31
- const urls = job.pending_assets();
32
- if (urls.length === 0) return job.finish();
33
- const data = await load(urls);
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"] }