blitz-pdf 0.1.6 → 0.1.8

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 (88) hide show
  1. package/README.md +66 -11
  2. package/THIRD_PARTY_NOTICES.md +19 -1
  3. package/dist/api.d.ts +2 -1
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +31 -6
  6. package/dist/api.js.map +1 -1
  7. package/dist/browser.d.ts +2 -1
  8. package/dist/browser.d.ts.map +1 -1
  9. package/dist/browser.js +1 -1
  10. package/dist/browser.js.map +1 -1
  11. package/dist/node.d.ts +2 -1
  12. package/dist/node.d.ts.map +1 -1
  13. package/dist/node.js +6 -2
  14. package/dist/node.js.map +1 -1
  15. package/dist/types.d.ts +16 -1
  16. package/dist/types.d.ts.map +1 -1
  17. package/dist/worker-host.d.ts +10 -5
  18. package/dist/worker-host.d.ts.map +1 -1
  19. package/dist/worker-host.js +149 -28
  20. package/dist/worker-host.js.map +1 -1
  21. package/dist/worker-protocol.d.ts +5 -0
  22. package/dist/worker-protocol.d.ts.map +1 -1
  23. package/dist/worker-runtime.d.ts +1 -1
  24. package/dist/worker-runtime.d.ts.map +1 -1
  25. package/dist/worker-runtime.js +22 -6
  26. package/dist/worker-runtime.js.map +1 -1
  27. package/licenses/Noto-OFL.txt +94 -0
  28. package/licenses/NotoCJK-OFL.txt +92 -0
  29. package/licenses/NotoEmoji-OFL.txt +93 -0
  30. package/licenses/RUST_DEPENDENCIES.txt +34 -40
  31. package/licenses/Unifont-OFL.txt +90 -0
  32. package/licenses/font-sources.json +106 -0
  33. package/package.json +1 -1
  34. package/src/api.ts +32 -7
  35. package/src/browser.ts +2 -1
  36. package/src/node.ts +7 -2
  37. package/src/types.ts +17 -1
  38. package/src/worker-host.ts +148 -29
  39. package/src/worker-protocol.ts +5 -5
  40. package/src/worker-runtime.ts +20 -6
  41. package/vendor/README.md +38 -5
  42. package/vendor/blitz-dom/Cargo.toml +6 -6
  43. package/vendor/blitz-dom/src/accessibility.rs +175 -2
  44. package/vendor/blitz-dom/src/config.rs +8 -10
  45. package/vendor/blitz-dom/src/document.rs +66 -3
  46. package/vendor/blitz-dom/src/events/keyboard.rs +2 -6
  47. package/vendor/blitz-dom/src/layout/construct.rs +5 -300
  48. package/vendor/blitz-dom/src/layout/inline/build.rs +309 -0
  49. package/vendor/blitz-dom/src/layout/{inline.rs → inline/layout.rs} +1 -1
  50. package/vendor/blitz-dom/src/layout/inline/mod.rs +4 -0
  51. package/vendor/blitz-dom/src/layout/mod.rs +3 -1
  52. package/vendor/blitz-dom/src/layout/replaced.rs +13 -4
  53. package/vendor/blitz-dom/src/mutator.rs +47 -0
  54. package/vendor/blitz-dom/src/node/element.rs +17 -0
  55. package/vendor/blitz-dom/src/node/mod.rs +1 -0
  56. package/vendor/blitz-dom/src/node/node.rs +46 -5
  57. package/vendor/blitz-dom/src/node/text.rs +11 -11
  58. package/vendor/blitz-dom/src/resolve.rs +2 -2
  59. package/vendor/blitz-dom/src/stylo.rs +30 -22
  60. package/vendor/blitz-dom/src/util.rs +1 -1
  61. package/vendor/blitz-paint/Cargo.toml +4 -4
  62. package/vendor/fontique/Cargo.toml +194 -0
  63. package/vendor/fontique/LICENSE-APACHE +201 -0
  64. package/vendor/fontique/LICENSE-MIT +25 -0
  65. package/vendor/fontique/README.md +54 -0
  66. package/vendor/fontique/examples/generic_families.rs +37 -0
  67. package/vendor/fontique/src/attributes.rs +42 -0
  68. package/vendor/fontique/src/backend/android.rs +218 -0
  69. package/vendor/fontique/src/backend/coretext.rs +206 -0
  70. package/vendor/fontique/src/backend/dwrite.rs +395 -0
  71. package/vendor/fontique/src/backend/fontconfig.rs +844 -0
  72. package/vendor/fontique/src/backend/mod.rs +75 -0
  73. package/vendor/fontique/src/charmap.rs +127 -0
  74. package/vendor/fontique/src/collection/mod.rs +1016 -0
  75. package/vendor/fontique/src/collection/query.rs +392 -0
  76. package/vendor/fontique/src/fallback.rs +293 -0
  77. package/vendor/fontique/src/family.rs +111 -0
  78. package/vendor/fontique/src/family_name.rs +153 -0
  79. package/vendor/fontique/src/font.rs +562 -0
  80. package/vendor/fontique/src/generic.rs +36 -0
  81. package/vendor/fontique/src/impl_fontconfig.rs +159 -0
  82. package/vendor/fontique/src/lib.rs +87 -0
  83. package/vendor/fontique/src/matching.rs +483 -0
  84. package/vendor/fontique/src/scan.rs +312 -0
  85. package/vendor/fontique/src/script.rs +271 -0
  86. package/vendor/fontique/src/source.rs +108 -0
  87. package/vendor/fontique/src/source_cache.rs +254 -0
  88. package/wasm/blitz_pdf_bg.wasm +0 -0
@@ -0,0 +1,90 @@
1
+ The SIL Open Font License version 1.1 is copied below, and is also
2
+ available with a FAQ at http://scripts.sil.org/OFL.
3
+
4
+
5
+ -----------------------------------------------------------
6
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
7
+ -----------------------------------------------------------
8
+
9
+ PREAMBLE
10
+ The goals of the Open Font License (OFL) are to stimulate worldwide
11
+ development of collaborative font projects, to support the font creation
12
+ efforts of academic and linguistic communities, and to provide a free and
13
+ open framework in which fonts may be shared and improved in partnership
14
+ with others.
15
+
16
+ The OFL allows the licensed fonts to be used, studied, modified and
17
+ redistributed freely as long as they are not sold by themselves. The
18
+ fonts, including any derivative works, can be bundled, embedded,
19
+ redistributed and/or sold with any software provided that any reserved
20
+ names are not used by derivative works. The fonts and derivatives,
21
+ however, cannot be released under any other type of license. The
22
+ requirement for fonts to remain under this license does not apply
23
+ to any document created using the fonts or their derivatives.
24
+
25
+ DEFINITIONS
26
+ "Font Software" refers to the set of files released by the Copyright
27
+ Holder(s) under this license and clearly marked as such. This may
28
+ include source files, build scripts and documentation.
29
+
30
+ "Reserved Font Name" refers to any names specified as such after the
31
+ copyright statement(s).
32
+
33
+ "Original Version" refers to the collection of Font Software components as
34
+ distributed by the Copyright Holder(s).
35
+
36
+ "Modified Version" refers to any derivative made by adding to, deleting,
37
+ or substituting -- in part or in whole -- any of the components of the
38
+ Original Version, by changing formats or by porting the Font Software to a
39
+ new environment.
40
+
41
+ "Author" refers to any designer, engineer, programmer, technical
42
+ writer or other person who contributed to the Font Software.
43
+
44
+ PERMISSION & CONDITIONS
45
+ Permission is hereby granted, free of charge, to any person obtaining
46
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
47
+ redistribute, and sell modified and unmodified copies of the Font
48
+ Software, subject to the following conditions:
49
+
50
+ 1) Neither the Font Software nor any of its individual components,
51
+ in Original or Modified Versions, may be sold by itself.
52
+
53
+ 2) Original or Modified Versions of the Font Software may be bundled,
54
+ redistributed and/or sold with any software, provided that each copy
55
+ contains the above copyright notice and this license. These can be
56
+ included either as stand-alone text files, human-readable headers or
57
+ in the appropriate machine-readable metadata fields within text or
58
+ binary files as long as those fields can be easily viewed by the user.
59
+
60
+ 3) No Modified Version of the Font Software may use the Reserved Font
61
+ Name(s) unless explicit written permission is granted by the corresponding
62
+ Copyright Holder. This restriction only applies to the primary font name as
63
+ presented to the users.
64
+
65
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
66
+ Software shall not be used to promote, endorse or advertise any
67
+ Modified Version, except to acknowledge the contribution(s) of the
68
+ Copyright Holder(s) and the Author(s) or with their explicit written
69
+ permission.
70
+
71
+ 5) The Font Software, modified or unmodified, in part or in whole,
72
+ must be distributed entirely under this license, and must not be
73
+ distributed under any other license. The requirement for fonts to
74
+ remain under this license does not apply to any document created
75
+ using the Font Software.
76
+
77
+ TERMINATION
78
+ This license becomes null and void if any of the above conditions are
79
+ not met.
80
+
81
+ DISCLAIMER
82
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
83
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
84
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
85
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
86
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
87
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
88
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
89
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
90
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,106 @@
1
+ [
2
+ {
3
+ "family": "Noto Sans Tamil",
4
+ "source": "https://raw.githubusercontent.com/notofonts/notofonts.github.io/93fac8abebe6c7fed0c5b7d2456d8fa484fa146c/fonts/NotoSansTamil/full/ttf/NotoSansTamil-Regular.ttf",
5
+ "sourceSha256": "0afbc221964b6048c6d771c525be474d21b288a621dce0fafedd695cc5c98e4e",
6
+ "sha256": "e967d7cc71373113bc7330cca8bbceee43d75499c9cfe2c77d0a163cc5c9c56c",
7
+ "license": "licenses/Noto-OFL.txt",
8
+ "copyright": "Copyright 2022 The Noto Project Authors (https://github.com/notofonts/tamil)",
9
+ "path": "tests/fixtures/fonts/NotoSansTamil-Regular.woff2"
10
+ },
11
+ {
12
+ "family": "Noto Emoji",
13
+ "source": "https://raw.githubusercontent.com/google/fonts/8b0a1d0f5983c89bc2b93f1b5fb55f9e252744b5/ofl/notoemoji/NotoEmoji%5Bwght%5D.ttf",
14
+ "sourceSha256": "de6c18832938afc99caf132b39d6a30a19bac7f2e812e28db2535b4608d27551",
15
+ "sha256": "12f3b7927fe6381c6fd45bf0bd635e6858348171a129145ef40dc4774790f8b5",
16
+ "license": "licenses/NotoEmoji-OFL.txt",
17
+ "copyright": "Copyright 2013 Google LLC",
18
+ "instance": {
19
+ "wght": 400
20
+ },
21
+ "path": "assets/fonts/NotoEmoji-Regular.woff2"
22
+ },
23
+ {
24
+ "family": "Unifont",
25
+ "source": "https://unifoundry.com/pub/unifont/unifont-18.0.01/font-builds/unifont-18.0.01.otf",
26
+ "sourceSha256": "88d0a14d4aa9a96419720b39ba5da921a59560d76d4ec7d523acc06ed85cd3c2",
27
+ "sha256": "b3afcc1a7d3088985b29709c9f63bb76f730dd17c506e6974cb31f8568048dc4",
28
+ "license": "licenses/Unifont-OFL.txt",
29
+ "copyright": "Copyright © 1998-2026 Roman Czyborra, Paul Hardy, Qianqian Fang, Andrew Miller, Johnnie Weaver, David Corbett, Ælla Chiana Moskopp, Rebecca Bettencourt, Ho-Seok Ee, et al.",
30
+ "path": "assets/fonts/unifont-18.0.01.woff2",
31
+ "conversion": "CFF straight-line contours converted losslessly to TrueType outlines, with reversed contour winding; WOFF2 compressed without subsetting. See scripts/build-unicode-fallbacks.py."
32
+ },
33
+ {
34
+ "family": "Unifont Upper",
35
+ "source": "https://unifoundry.com/pub/unifont/unifont-18.0.01/font-builds/unifont_upper-18.0.01.otf",
36
+ "sourceSha256": "472201e45a050bff4c7658208b8c73e34ebce3764b287a5a46c7774038ef9391",
37
+ "sha256": "6b1456aedb95104af8b384783b31c060c91d5b772e0859b057e5592913977343",
38
+ "license": "licenses/Unifont-OFL.txt",
39
+ "copyright": "Copyright © 1998-2026 Roman Czyborra, Paul Hardy, Qianqian Fang, Andrew Miller, Johnnie Weaver, David Corbett, Ælla Chiana Moskopp, Rebecca Bettencourt, Ho-Seok Ee, et al.",
40
+ "path": "assets/fonts/unifont_upper-18.0.01.woff2",
41
+ "conversion": "CFF straight-line contours converted losslessly to TrueType outlines, with reversed contour winding; WOFF2 compressed without subsetting. See scripts/build-unicode-fallbacks.py."
42
+ },
43
+ {
44
+ "family": "Noto Sans",
45
+ "source": "https://raw.githubusercontent.com/notofonts/notofonts.github.io/93fac8abebe6c7fed0c5b7d2456d8fa484fa146c/fonts/NotoSans/full/ttf/NotoSans-Regular.ttf",
46
+ "sourceSha256": "f5f552c8c5edb61fe6efb824baf4d4de47b1a8689ab4925ff43f7bd6a4ebece5",
47
+ "sha256": "5e69b5d9791d13fcf54b306bd7db7210691981363e8366844b61d08bf53b35fc",
48
+ "copyright": "Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)",
49
+ "license": "licenses/Noto-OFL.txt",
50
+ "path": "assets/fonts/NotoSans-Regular.woff2"
51
+ },
52
+ {
53
+ "family": "Noto Sans",
54
+ "source": "https://raw.githubusercontent.com/notofonts/notofonts.github.io/93fac8abebe6c7fed0c5b7d2456d8fa484fa146c/fonts/NotoSans/full/ttf/NotoSans-Bold.ttf",
55
+ "sourceSha256": "3a08a47daa00cade516425c15c57615aef2fd418ec9811a7b9f465088f92cc05",
56
+ "sha256": "682022f287c5e97ab0fcf000b4dcefe468287cf0e474faafdf6b521dca549ecf",
57
+ "copyright": "Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)",
58
+ "license": "licenses/Noto-OFL.txt",
59
+ "path": "assets/fonts/NotoSans-Bold.woff2"
60
+ },
61
+ {
62
+ "family": "Noto Sans",
63
+ "source": "https://raw.githubusercontent.com/notofonts/notofonts.github.io/93fac8abebe6c7fed0c5b7d2456d8fa484fa146c/fonts/NotoSans/full/ttf/NotoSans-Italic.ttf",
64
+ "sourceSha256": "126522ae1bb9cd92120287fc47dfc74ef981e73931d93e52c565fb7e09b2d74a",
65
+ "sha256": "d4650f9a6687babedc858c2bab9b66f3af0482edf913773ec467d1c2e9c0180c",
66
+ "copyright": "Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)",
67
+ "license": "licenses/Noto-OFL.txt",
68
+ "path": "assets/fonts/NotoSans-Italic.woff2"
69
+ },
70
+ {
71
+ "family": "Noto Sans",
72
+ "source": "https://raw.githubusercontent.com/notofonts/notofonts.github.io/93fac8abebe6c7fed0c5b7d2456d8fa484fa146c/fonts/NotoSans/full/ttf/NotoSans-BoldItalic.ttf",
73
+ "sourceSha256": "2e34b41a4b9c234b1be7dff6d06cba18811ecb694b41350873edf0ec16a0f0fa",
74
+ "sha256": "2b74d12f0c0ab38db4149e3fedb039eba6e72cce7ec094ab904fbb7a99d48bfb",
75
+ "copyright": "Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)",
76
+ "license": "licenses/Noto-OFL.txt",
77
+ "path": "assets/fonts/NotoSans-BoldItalic.woff2"
78
+ },
79
+ {
80
+ "family": "Noto Sans Mono",
81
+ "source": "https://raw.githubusercontent.com/notofonts/notofonts.github.io/93fac8abebe6c7fed0c5b7d2456d8fa484fa146c/fonts/NotoSansMono/unhinted/ttf/NotoSansMono-Regular.ttf",
82
+ "sourceSha256": "87f8ce0522a6c99b743ee5fc75b4073cfdd575639119672828b7b9944b65b4f4",
83
+ "sha256": "b97922da952ad3f7ee7280c83d9929456bed8d4fd832dddef01e595422508891",
84
+ "copyright": "Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)",
85
+ "license": "licenses/Noto-OFL.txt",
86
+ "path": "assets/fonts/NotoSansMono-Regular.woff2"
87
+ },
88
+ {
89
+ "family": "Noto Sans CJK JP",
90
+ "source": "https://raw.githubusercontent.com/notofonts/noto-cjk/Sans2.004/Sans/OTF/Japanese/NotoSansCJKjp-Regular.otf",
91
+ "sourceSha256": "68a3fc98800b2a27b371f2fb79991daf3633bd89309d4ffaa6946fd587f375b5",
92
+ "sha256": "e24935ccb42dde87a7fc8f10929fc9c214fb6daa90637eb6c65db81eb3daea43",
93
+ "copyright": "© 2014-2021 Adobe (http://www.adobe.com/).",
94
+ "license": "licenses/NotoCJK-OFL.txt",
95
+ "path": "assets/fonts/NotoSansCJKjp-Regular.woff2"
96
+ },
97
+ {
98
+ "family": "Noto Sans CJK JP",
99
+ "source": "https://raw.githubusercontent.com/notofonts/noto-cjk/Sans2.004/Sans/OTF/Japanese/NotoSansCJKjp-Bold.otf",
100
+ "sourceSha256": "e53dcb0dcb2922e45d01aae1ebd2f382bb81d4229b18b6b883bd170678af1f76",
101
+ "sha256": "39b13feaef2327a3fce9b12f4c67bc5c069b0a97e8e11e03b062260f6c2cdbbf",
102
+ "copyright": "© 2014-2021 Adobe (http://www.adobe.com/).",
103
+ "license": "licenses/NotoCJK-OFL.txt",
104
+ "path": "assets/fonts/NotoSansCJKjp-Bold.woff2"
105
+ }
106
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitz-pdf",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",
package/src/api.ts CHANGED
@@ -3,8 +3,8 @@ import { createAssetLoader } from "./assets.js";
3
3
  import { normalizeOptions } from "./options.js";
4
4
  import { render, validateHtml } from "./render.js";
5
5
  import { compileWasm } from "./wasm.js";
6
- import { renderInWorker, type SpawnWorker } from "./worker-host.js";
7
- import type { InitOptions, PdfOptions, WasmSource } from "./types.js";
6
+ import { createWorkerPool, type SpawnWorker } from "./worker-host.js";
7
+ import type { InitOptions, PdfOptions, PdfWorkerPool, PdfWorkerPoolOptions, WasmSource } from "./types.js";
8
8
 
9
9
  export interface ApiConfig {
10
10
  defaultSource: () => Promise<WasmSource> | WasmSource;
@@ -15,6 +15,7 @@ export interface ApiConfig {
15
15
  export function createApi({ defaultSource, prepare = async source => source, spawnWorker }: ApiConfig) {
16
16
  let compiled: Promise<WebAssembly.Module> | undefined;
17
17
  let ready: Promise<void> | undefined;
18
+ let defaultPool: PdfWorkerPool | undefined;
18
19
 
19
20
  function compile(source?: WasmSource): Promise<WebAssembly.Module> {
20
21
  if (compiled) return compiled;
@@ -43,13 +44,37 @@ export function createApi({ defaultSource, prepare = async source => source, spa
43
44
  return render(html, normalized, assetResolver && createAssetLoader(assetResolver));
44
45
  }
45
46
 
46
- /** Like `renderPdf`, but layout and serialization run in a new worker. An `assets` function still runs on the calling thread. */
47
+ /** Create an owned pool, with independent concurrency and shutdown. No workers start until needed. */
48
+ function createPdfWorkerPool(options: PdfWorkerPoolOptions = {}): PdfWorkerPool {
49
+ const pool = createWorkerPool(spawnWorker, options);
50
+ // Include compilation in accepted work so close also drains renders waiting for WASM.
51
+ const pending = new Set<Promise<Uint8Array>>();
52
+ let closing: Promise<void> | undefined;
53
+ return {
54
+ renderPdf(html: string, options: PdfOptions = {}): Promise<Uint8Array> {
55
+ if (closing) return Promise.reject(new Error("PDF worker pool is closed"));
56
+ const work = (async () => {
57
+ validateHtml(html);
58
+ const { assetResolver, ...normalized } = normalizeOptions(options);
59
+ return pool.render(await compile(), html, normalized, assetResolver);
60
+ })();
61
+ pending.add(work);
62
+ void work.then(() => pending.delete(work), () => pending.delete(work));
63
+ return work;
64
+ },
65
+ close(): Promise<void> {
66
+ closing ??= Promise.allSettled([...pending]).then(() => pool.close());
67
+ return closing;
68
+ },
69
+ };
70
+ }
71
+
72
+ /** Render in the shared bounded pool. An assets function still runs on the calling thread. */
47
73
  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);
74
+ defaultPool ??= createPdfWorkerPool();
75
+ return defaultPool.renderPdf(html, options);
51
76
  }
52
77
 
53
78
  async function getEngineVersion(): Promise<string> { await init(); return engine_version(); }
54
- return { init, renderPdf, renderPdfInWorker, getWasmModule, getEngineVersion };
79
+ return { init, renderPdf, renderPdfInWorker, createPdfWorkerPool, getWasmModule, getEngineVersion };
55
80
  }
package/src/browser.ts CHANGED
@@ -1,7 +1,8 @@
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, renderPdfInWorker, getWasmModule, getEngineVersion } = createApi({
4
+ export type { PdfWorkerPool, PdfWorkerPoolOptions } from "./types.js";
5
+ export const { init, renderPdf, renderPdfInWorker, createPdfWorkerPool, getWasmModule, getEngineVersion } = createApi({
5
6
  defaultSource: () => new URL("../wasm/blitz_pdf_bg.wasm", import.meta.url),
6
7
  spawnWorker: events => {
7
8
  const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
package/src/node.ts CHANGED
@@ -3,7 +3,8 @@ import { Worker } from "node:worker_threads";
3
3
  import { createApi } from "./api.js";
4
4
 
5
5
  export type { AssetData, AssetMap, AssetResolver, AttachmentRelationship, BinaryData, InitOptions, Length, PageFormat, PageNumberOptions, PdfAttachment, PdfDate, PdfMetadata, PdfOptions, PdfXmpProperty, PdfXmpSchema, WasmSource } from "./types.js";
6
- export const { init, renderPdf, renderPdfInWorker, getWasmModule, getEngineVersion } = createApi({
6
+ export type { PdfWorkerPool, PdfWorkerPoolOptions } from "./types.js";
7
+ export const { init, renderPdf, renderPdfInWorker, createPdfWorkerPool, getWasmModule, getEngineVersion } = createApi({
7
8
  defaultSource: () => readFile(new URL("../wasm/blitz_pdf_bg.wasm", import.meta.url)),
8
9
  prepare: async source => {
9
10
  if (source instanceof URL && source.protocol === "file:") return readFile(source);
@@ -16,6 +17,10 @@ export const { init, renderPdf, renderPdfInWorker, getWasmModule, getEngineVersi
16
17
  worker.on("error", events.error);
17
18
  worker.on("messageerror", events.error);
18
19
  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
+ return {
21
+ post: (message, transfer) => worker.postMessage(message, transfer),
22
+ terminate: async () => { await worker.terminate(); },
23
+ ref: () => { worker.ref(); }, unref: () => { worker.unref(); },
24
+ };
20
25
  },
21
26
  });
package/src/types.ts CHANGED
@@ -105,7 +105,8 @@ export interface PdfOptions {
105
105
  baseUrl?: string;
106
106
  /** Resource map or async loader, called once per absolute URL per render. Data URLs are handled internally. */
107
107
  assets?: AssetMap | AssetResolver;
108
- /** Additional static TTF, OTF, WOFF, or WOFF2 fonts, using their internal family names. */
108
+ /** Additional static TTF, OTF, WOFF, or WOFF2 fonts, using their internal family names.
109
+ * Automatically used as fallbacks when the requested font lacks a glyph. */
109
110
  fonts?: readonly BinaryData[];
110
111
  /** PDF document title. */
111
112
  title?: string;
@@ -122,3 +123,18 @@ export interface PdfOptions {
122
123
  /** Reject larger documents. Default: 100; maximum: 1000. */
123
124
  maxPages?: number;
124
125
  }
126
+ /** Limits for a pool of reusable, independent PDF engines. */
127
+ export interface PdfWorkerPoolOptions {
128
+ /** Maximum simultaneous renders. Default: 4. */
129
+ maxWorkers?: number;
130
+ /** Release an unused worker after this many milliseconds. Default: 30000; 0 disables reuse. */
131
+ idleTimeoutMs?: number;
132
+ /** Recreate a worker after this many documents to bound retained engine memory. Default: 100. */
133
+ maxJobsPerWorker?: number;
134
+ }
135
+
136
+ export interface PdfWorkerPool {
137
+ renderPdf(html: string, options?: PdfOptions): Promise<Uint8Array>;
138
+ /** Stop accepting work, finish accepted renders, and release every worker. Idempotent. */
139
+ close(): Promise<void>;
140
+ }
@@ -1,45 +1,164 @@
1
1
  import { createAssetLoader } from "./assets.js";
2
2
  import type { RenderOptions } from "./render.js";
3
- import type { AssetResolver } from "./types.js";
3
+ import type { AssetResolver, PdfWorkerPoolOptions } from "./types.js";
4
4
  import type { HostMessage, WorkerMessage } from "./worker-protocol.js";
5
5
 
6
6
  export interface WorkerEvents {
7
7
  message(message: WorkerMessage): void;
8
- /** Crashes, unreadable messages, or an exit before the render settled. */
8
+ /** Crashes, unreadable messages, or an unexpected exit, including while idle. */
9
9
  error(error: unknown): void;
10
10
  }
11
11
 
12
12
  export interface WorkerHandle {
13
13
  post(message: HostMessage, transfer: ArrayBuffer[]): void;
14
- terminate(): void;
14
+ terminate(): void | Promise<void>;
15
+ ref?(): void;
16
+ unref?(): void;
15
17
  }
16
18
 
17
19
  export type SpawnWorker = (events: WorkerEvents) => WorkerHandle;
18
20
 
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
- });
21
+ interface Job {
22
+ id: number;
23
+ message: Extract<HostMessage, { type: "render" }>;
24
+ load: ReturnType<typeof createAssetLoader> | undefined;
25
+ resolve(pdf: Uint8Array): void;
26
+ reject(error: unknown): void;
27
+ }
28
+
29
+ interface Slot {
30
+ worker?: WorkerHandle;
31
+ job?: Job;
32
+ timer?: ReturnType<typeof setTimeout>;
33
+ completed: number;
34
+ retired: boolean;
35
+ }
36
+
37
+ function integer(value: number, min: number, max: number, name: string): number {
38
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
39
+ throw new RangeError(`${name} must be an integer from ${min} to ${max}`);
40
+ }
41
+ return value;
42
+ }
43
+
44
+ /** Jobs have their own resolver and document; only the initialized engine survives. */
45
+ export function createWorkerPool(spawn: SpawnWorker, options: PdfWorkerPoolOptions = {}) {
46
+ const maxWorkers = integer(options.maxWorkers ?? 4, 1, Number.MAX_SAFE_INTEGER, "maxWorkers");
47
+ const idleTimeoutMs = integer(options.idleTimeoutMs ?? 30_000, 0, 2_147_483_647, "idleTimeoutMs");
48
+ const maxJobs = integer(options.maxJobsPerWorker ?? 100, 1, Number.MAX_SAFE_INTEGER, "maxJobsPerWorker");
49
+ const slots = new Set<Slot>();
50
+ const queue: Job[] = [];
51
+ const terminating = new Set<Promise<void>>();
52
+ let nextId = 0;
53
+ let closing: Promise<void> | undefined;
54
+ let resolveClosed: (() => void) | undefined;
55
+ let pumping = false;
56
+
57
+ function checkClosed() {
58
+ if (closing && queue.length === 0 && slots.size === 0 && terminating.size === 0) resolveClosed?.();
59
+ }
60
+
61
+ function retire(slot: Slot) {
62
+ if (slot.retired) return;
63
+ slot.retired = true;
64
+ clearTimeout(slot.timer);
65
+ slots.delete(slot);
66
+ // Observe asynchronous termination before close resolves; late exit events are ignored.
67
+ const done = Promise.resolve().then(() => slot.worker?.terminate()).then(() => {}, () => {});
68
+ terminating.add(done);
69
+ void done.then(() => { terminating.delete(done); checkClosed(); });
70
+ }
71
+
72
+ function fail(slot: Slot, error: unknown) {
73
+ if (slot.retired) return;
74
+ const job = slot.job;
75
+ delete slot.job;
76
+ retire(slot);
77
+ job?.reject(error);
78
+ pump();
79
+ }
80
+
81
+ function release(slot: Slot) {
82
+ slot.completed++;
83
+ if (slot.completed >= maxJobs || idleTimeoutMs === 0 || (closing && queue.length === 0)) {
84
+ retire(slot);
85
+ } else if (queue.length === 0) {
86
+ slot.worker?.unref?.();
87
+ slot.timer = setTimeout(() => { retire(slot); checkClosed(); }, idleTimeoutMs);
88
+ // A warm worker and its eviction timer must not keep a CLI alive.
89
+ slot.timer.unref?.();
90
+ }
91
+ pump();
92
+ }
93
+
94
+ function message(slot: Slot, value: WorkerMessage) {
95
+ const job = slot.job;
96
+ if (slot.retired || !job || value.id !== job.id) return;
97
+ if (value.type === "done") {
98
+ delete slot.job;
99
+ release(slot);
100
+ job.resolve(value.pdf);
101
+ } else if (value.type === "error") {
102
+ fail(slot, value.error);
103
+ } else if (!job.load) {
104
+ fail(slot, new Error("Render worker requested assets without a resolver"));
105
+ } else {
106
+ void job.load(value.urls).then(data => {
107
+ // Resolver promises can finish after a crash or retirement. Never send them to another job.
108
+ if (slot.retired || slot.job !== job) return;
109
+ try {
110
+ slot.worker!.post({ type: "assets", id: job.id, data }, data.map(bytes => bytes.buffer as ArrayBuffer));
111
+ } catch (error) { fail(slot, error); }
112
+ }, error => { if (!slot.retired && slot.job === job) fail(slot, error); });
113
+ }
114
+ }
115
+
116
+ function pump() {
117
+ if (pumping) return;
118
+ pumping = true;
119
+ try {
120
+ while (queue.length > 0) {
121
+ let slot = [...slots].find(candidate => !candidate.job);
122
+ if (!slot && slots.size >= maxWorkers) break;
123
+ const job = queue.shift()!;
124
+ if (!slot) {
125
+ slot = { completed: 0, retired: false, job };
126
+ slots.add(slot);
127
+ const created = slot;
128
+ try {
129
+ created.worker = spawn({ message: value => message(created, value), error: error => fail(created, error) });
130
+ } catch (error) { fail(created, error); continue; }
131
+ } else {
132
+ clearTimeout(slot.timer);
133
+ delete slot.timer;
134
+ slot.job = job;
135
+ }
136
+ try {
137
+ slot.worker!.ref?.();
138
+ slot.worker!.post(job.message, []);
139
+ } catch (error) { fail(slot, error); }
140
+ }
141
+ if (closing && queue.length === 0) for (const slot of slots) if (!slot.job) retire(slot);
142
+ } finally { pumping = false; }
143
+ checkClosed();
144
+ }
145
+
146
+ return {
147
+ render(module: WebAssembly.Module, html: string, options: RenderOptions, resolver?: AssetResolver): Promise<Uint8Array> {
148
+ if (closing) return Promise.reject(new Error("PDF worker pool is closed"));
149
+ return new Promise((resolve, reject) => {
150
+ const id = ++nextId;
151
+ queue.push({ id, message: { type: "render", id, module, html, options, external: resolver !== undefined },
152
+ load: resolver ? createAssetLoader(resolver) : undefined, resolve, reject });
153
+ pump();
154
+ });
155
+ },
156
+ close(): Promise<void> {
157
+ if (!closing) {
158
+ closing = new Promise(resolve => { resolveClosed = resolve; });
159
+ pump();
160
+ }
161
+ return closing;
162
+ },
163
+ };
45
164
  }
@@ -1,10 +1,10 @@
1
1
  import type { RenderOptions } from "./render.js";
2
2
 
3
3
  export type HostMessage =
4
- | { type: "render"; module: WebAssembly.Module; html: string; options: RenderOptions; external: boolean }
5
- | { type: "assets"; data: Uint8Array[] };
4
+ | { type: "render"; id: number; module: WebAssembly.Module; html: string; options: RenderOptions; external: boolean }
5
+ | { type: "assets"; id: number; data: Uint8Array[] };
6
6
 
7
7
  export type WorkerMessage =
8
- | { type: "assets"; urls: string[] }
9
- | { type: "done"; pdf: Uint8Array }
10
- | { type: "error"; error: unknown };
8
+ | { type: "assets"; id: number; urls: string[] }
9
+ | { type: "done"; id: number; pdf: Uint8Array }
10
+ | { type: "error"; id: number; error: unknown };
@@ -7,29 +7,43 @@ export interface WorkerEndpoint {
7
7
  onMessage(handler: (message: HostMessage) => void): void;
8
8
  }
9
9
 
10
- /** Worker side of `renderPdfInWorker`: one render per worker; asset bytes come from the host. */
10
+ /** One document at a time; retain the initialized engine and its bundled-font cache. */
11
11
  export function serve(endpoint: WorkerEndpoint): void {
12
+ let initialized = false;
13
+ let activeId: number | undefined;
12
14
  let receiveAssets: ((data: Uint8Array[]) => void) | undefined;
13
15
  endpoint.onMessage(async message => {
14
16
  if (message.type === "assets") {
17
+ if (message.id !== activeId) return;
15
18
  const receive = receiveAssets;
16
19
  receiveAssets = undefined;
17
20
  receive?.(message.data);
18
21
  return;
19
22
  }
23
+ if (activeId !== undefined) {
24
+ endpoint.post({ type: "error", id: message.id, error: new Error("Worker already has an active document") }, []);
25
+ return;
26
+ }
27
+ activeId = message.id;
20
28
  try {
21
- initSync({ module: message.module });
29
+ if (!initialized) {
30
+ initSync({ module: message.module });
31
+ initialized = true;
32
+ }
22
33
  const load = message.external
23
34
  ? (urls: string[]) => new Promise<Uint8Array[]>(resolve => {
24
35
  receiveAssets = resolve;
25
- endpoint.post({ type: "assets", urls }, []);
36
+ endpoint.post({ type: "assets", id: message.id, urls }, []);
26
37
  })
27
38
  : undefined;
28
39
  const pdf = await render(message.html, message.options, load);
29
- endpoint.post({ type: "done", pdf }, [pdf.buffer as ArrayBuffer]);
40
+ endpoint.post({ type: "done", id: message.id, pdf }, [pdf.buffer as ArrayBuffer]);
30
41
  } catch (error) {
31
- try { endpoint.post({ type: "error", error }, []); }
32
- catch { endpoint.post({ type: "error", error: new Error(String(error)) }, []); }
42
+ try { endpoint.post({ type: "error", id: message.id, error }, []); }
43
+ catch { endpoint.post({ type: "error", id: message.id, error: new Error(String(error)) }, []); }
44
+ } finally {
45
+ activeId = undefined;
46
+ receiveAssets = undefined;
33
47
  }
34
48
  });
35
49
  }