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.
- package/README.md +66 -11
- package/THIRD_PARTY_NOTICES.md +19 -1
- package/dist/api.d.ts +2 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +31 -6
- package/dist/api.js.map +1 -1
- package/dist/browser.d.ts +2 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +6 -2
- package/dist/node.js.map +1 -1
- package/dist/types.d.ts +16 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-host.d.ts +10 -5
- package/dist/worker-host.d.ts.map +1 -1
- package/dist/worker-host.js +149 -28
- package/dist/worker-host.js.map +1 -1
- package/dist/worker-protocol.d.ts +5 -0
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-runtime.d.ts +1 -1
- package/dist/worker-runtime.d.ts.map +1 -1
- package/dist/worker-runtime.js +22 -6
- package/dist/worker-runtime.js.map +1 -1
- package/licenses/Noto-OFL.txt +94 -0
- package/licenses/NotoCJK-OFL.txt +92 -0
- package/licenses/NotoEmoji-OFL.txt +93 -0
- package/licenses/RUST_DEPENDENCIES.txt +34 -40
- package/licenses/Unifont-OFL.txt +90 -0
- package/licenses/font-sources.json +106 -0
- package/package.json +1 -1
- package/src/api.ts +32 -7
- package/src/browser.ts +2 -1
- package/src/node.ts +7 -2
- package/src/types.ts +17 -1
- package/src/worker-host.ts +148 -29
- package/src/worker-protocol.ts +5 -5
- package/src/worker-runtime.ts +20 -6
- package/vendor/README.md +38 -5
- package/vendor/blitz-dom/Cargo.toml +6 -6
- package/vendor/blitz-dom/src/accessibility.rs +175 -2
- package/vendor/blitz-dom/src/config.rs +8 -10
- package/vendor/blitz-dom/src/document.rs +66 -3
- package/vendor/blitz-dom/src/events/keyboard.rs +2 -6
- package/vendor/blitz-dom/src/layout/construct.rs +5 -300
- package/vendor/blitz-dom/src/layout/inline/build.rs +309 -0
- package/vendor/blitz-dom/src/layout/{inline.rs → inline/layout.rs} +1 -1
- package/vendor/blitz-dom/src/layout/inline/mod.rs +4 -0
- package/vendor/blitz-dom/src/layout/mod.rs +3 -1
- package/vendor/blitz-dom/src/layout/replaced.rs +13 -4
- package/vendor/blitz-dom/src/mutator.rs +47 -0
- package/vendor/blitz-dom/src/node/element.rs +17 -0
- package/vendor/blitz-dom/src/node/mod.rs +1 -0
- package/vendor/blitz-dom/src/node/node.rs +46 -5
- package/vendor/blitz-dom/src/node/text.rs +11 -11
- package/vendor/blitz-dom/src/resolve.rs +2 -2
- package/vendor/blitz-dom/src/stylo.rs +30 -22
- package/vendor/blitz-dom/src/util.rs +1 -1
- package/vendor/blitz-paint/Cargo.toml +4 -4
- package/vendor/fontique/Cargo.toml +194 -0
- package/vendor/fontique/LICENSE-APACHE +201 -0
- package/vendor/fontique/LICENSE-MIT +25 -0
- package/vendor/fontique/README.md +54 -0
- package/vendor/fontique/examples/generic_families.rs +37 -0
- package/vendor/fontique/src/attributes.rs +42 -0
- package/vendor/fontique/src/backend/android.rs +218 -0
- package/vendor/fontique/src/backend/coretext.rs +206 -0
- package/vendor/fontique/src/backend/dwrite.rs +395 -0
- package/vendor/fontique/src/backend/fontconfig.rs +844 -0
- package/vendor/fontique/src/backend/mod.rs +75 -0
- package/vendor/fontique/src/charmap.rs +127 -0
- package/vendor/fontique/src/collection/mod.rs +1016 -0
- package/vendor/fontique/src/collection/query.rs +392 -0
- package/vendor/fontique/src/fallback.rs +293 -0
- package/vendor/fontique/src/family.rs +111 -0
- package/vendor/fontique/src/family_name.rs +153 -0
- package/vendor/fontique/src/font.rs +562 -0
- package/vendor/fontique/src/generic.rs +36 -0
- package/vendor/fontique/src/impl_fontconfig.rs +159 -0
- package/vendor/fontique/src/lib.rs +87 -0
- package/vendor/fontique/src/matching.rs +483 -0
- package/vendor/fontique/src/scan.rs +312 -0
- package/vendor/fontique/src/script.rs +271 -0
- package/vendor/fontique/src/source.rs +108 -0
- package/vendor/fontique/src/source_cache.rs +254 -0
- 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
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 {
|
|
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
|
-
/**
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
+
}
|
package/src/worker-host.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
}
|
package/src/worker-protocol.ts
CHANGED
|
@@ -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 };
|
package/src/worker-runtime.ts
CHANGED
|
@@ -7,29 +7,43 @@ export interface WorkerEndpoint {
|
|
|
7
7
|
onMessage(handler: (message: HostMessage) => void): void;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
/**
|
|
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
|
-
|
|
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
|
}
|