@2018kguo/recto 0.1.0 → 0.3.0
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/dist/assets.d.ts +11 -6
- package/dist/assets.js +123 -42
- package/dist/document.d.ts +48 -2
- package/dist/document.js +109 -4
- package/dist/engine.d.ts +17 -9
- package/dist/engine.js +223 -45
- package/dist/errors.d.ts +4 -2
- package/dist/errors.js +4 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +13 -0
- package/dist/pool-worker.d.ts +14 -0
- package/dist/pool-worker.js +65 -0
- package/dist/pool.d.ts +36 -0
- package/dist/pool.js +287 -0
- package/dist/ranged.d.ts +10 -0
- package/dist/ranged.js +38 -11
- package/package.json +2 -1
- package/wasm/FoxitDingbats.pfb +0 -0
- package/wasm/FoxitFixed.pfb +0 -0
- package/wasm/FoxitFixedBold.pfb +0 -0
- package/wasm/FoxitFixedBoldItalic.pfb +0 -0
- package/wasm/FoxitFixedItalic.pfb +0 -0
- package/wasm/FoxitSans.pfb +0 -0
- package/wasm/FoxitSansBold.pfb +0 -0
- package/wasm/FoxitSansBoldItalic.pfb +0 -0
- package/wasm/FoxitSansItalic.pfb +0 -0
- package/wasm/FoxitSerif.pfb +0 -0
- package/wasm/FoxitSerifBold.pfb +0 -0
- package/wasm/FoxitSerifBoldItalic.pfb +0 -0
- package/wasm/FoxitSerifItalic.pfb +0 -0
- package/wasm/FoxitSymbol.pfb +0 -0
- package/wasm/THIRD_PARTY_NOTICES +2 -158
- package/wasm/cmaps.deflate +0 -0
- package/wasm/pins.d.mts +1 -1
- package/wasm/pins.mjs +19 -3
- package/wasm/recto-more.wasm +0 -0
- package/wasm/recto-rare.wasm +0 -0
- package/wasm/recto-render.wasm +0 -0
- package/wasm/recto.gen.d.mts +2 -1
- package/wasm/recto.gen.mjs +118 -12
- package/wasm/recto.types.d.mts +46 -1
- package/wasm/recto.wasm +0 -0
- package/wasm/cmaps.brotli +0 -0
- package/wasm/fonts.bin +0 -0
package/dist/assets.d.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { PINS } from "../wasm/pins.mjs";
|
|
2
2
|
export type AssetName = keyof typeof PINS;
|
|
3
|
+
export type WasmName = "recto.wasm" | "recto-render.wasm" | "recto-more.wasm" | "recto-rare.wasm";
|
|
4
|
+
/** The standard fonts, in the order the wasm asks for them (`font_index` in crates/recto/src/assets.rs). */
|
|
5
|
+
export declare const STANDARD_FONTS: readonly ["FoxitFixed.pfb", "FoxitFixedBold.pfb", "FoxitFixedBoldItalic.pfb", "FoxitFixedItalic.pfb", "FoxitSans.pfb", "FoxitSansBold.pfb", "FoxitSansBoldItalic.pfb", "FoxitSansItalic.pfb", "FoxitSerif.pfb", "FoxitSerifBold.pfb", "FoxitSerifBoldItalic.pfb", "FoxitSerifItalic.pfb", "FoxitSymbol.pfb", "FoxitDingbats.pfb"];
|
|
3
6
|
/** Whether an asset can be read at the moment the engine asks for it. */
|
|
4
7
|
export declare function canReadSync(): boolean;
|
|
5
8
|
/** `assetBase` as a directory URL, or null for the package's own files. */
|
|
6
9
|
export declare function assetBaseUrl(base: string | URL | undefined): URL | null;
|
|
7
|
-
/**
|
|
8
|
-
export
|
|
9
|
-
/**
|
|
10
|
+
/** A hash to use where there is no native one to call synchronously. A live instance's is quicker than this file's. */
|
|
11
|
+
export type SyncHash = (bytes: Uint8Array) => string | null;
|
|
12
|
+
/** The wasm, verified and then compiled. */
|
|
13
|
+
export declare function compileWasm(name: WasmName, base: URL | null): Promise<WebAssembly.Module>;
|
|
14
|
+
/** Fetches and verifies assets ahead of time; a thread that can't read them when needed has to. */
|
|
10
15
|
export declare function preloadAssets(base: URL | null, names: AssetName[]): Promise<void>;
|
|
11
16
|
/** An asset, read synchronously. Throws a PdfError rather than returning nothing. */
|
|
12
|
-
export declare function loadAssetSync(name: AssetName, base: URL | null): Uint8Array;
|
|
13
|
-
/**
|
|
14
|
-
export declare function
|
|
17
|
+
export declare function loadAssetSync(name: AssetName, base: URL | null, hash?: SyncHash): Uint8Array;
|
|
18
|
+
/** A file, read now and hashed before it's returned, and not kept: a part is compiled once. */
|
|
19
|
+
export declare function readCheckedSync(name: AssetName, base: URL | null, hash?: SyncHash): Uint8Array;
|
package/dist/assets.js
CHANGED
|
@@ -8,21 +8,72 @@
|
|
|
8
8
|
// are written out as `new URL("literal", import.meta.url)` because that is the form
|
|
9
9
|
// bundlers recognise and rewrite, emitting the file.
|
|
10
10
|
//
|
|
11
|
-
// Each file is pinned by SHA-256
|
|
12
|
-
// HTML fallback page must not read as a font.
|
|
11
|
+
// Each file is pinned by SHA-256 and hashed here, every byte, before it's used: the engine reads
|
|
12
|
+
// its tables as trusted, and a dev server's HTML fallback page must not read as a font. Nothing is
|
|
13
|
+
// left to `fetch`'s `integrity`, which a wrapper around `fetch` can drop, and which buffers the
|
|
14
|
+
// body anyway. pins.mjs is written by the build.
|
|
13
15
|
import { PINS } from "../wasm/pins.mjs";
|
|
14
16
|
import { PdfError } from "./errors.js";
|
|
15
17
|
import { sha256Hex } from "./sha256.js";
|
|
18
|
+
/** The standard fonts, in the order the wasm asks for them (`font_index` in crates/recto/src/assets.rs). */
|
|
19
|
+
export const STANDARD_FONTS = [
|
|
20
|
+
"FoxitFixed.pfb",
|
|
21
|
+
"FoxitFixedBold.pfb",
|
|
22
|
+
"FoxitFixedBoldItalic.pfb",
|
|
23
|
+
"FoxitFixedItalic.pfb",
|
|
24
|
+
"FoxitSans.pfb",
|
|
25
|
+
"FoxitSansBold.pfb",
|
|
26
|
+
"FoxitSansBoldItalic.pfb",
|
|
27
|
+
"FoxitSansItalic.pfb",
|
|
28
|
+
"FoxitSerif.pfb",
|
|
29
|
+
"FoxitSerifBold.pfb",
|
|
30
|
+
"FoxitSerifBoldItalic.pfb",
|
|
31
|
+
"FoxitSerifItalic.pfb",
|
|
32
|
+
"FoxitSymbol.pfb",
|
|
33
|
+
"FoxitDingbats.pfb",
|
|
34
|
+
];
|
|
16
35
|
function packagedUrl(name) {
|
|
17
36
|
switch (name) {
|
|
18
37
|
case "recto.wasm":
|
|
19
38
|
return new URL("../wasm/recto.wasm", import.meta.url);
|
|
20
|
-
case "
|
|
21
|
-
return new URL("../wasm/
|
|
39
|
+
case "recto-render.wasm":
|
|
40
|
+
return new URL("../wasm/recto-render.wasm", import.meta.url);
|
|
41
|
+
case "recto-more.wasm":
|
|
42
|
+
return new URL("../wasm/recto-more.wasm", import.meta.url);
|
|
43
|
+
case "recto-rare.wasm":
|
|
44
|
+
return new URL("../wasm/recto-rare.wasm", import.meta.url);
|
|
45
|
+
case "FoxitFixed.pfb":
|
|
46
|
+
return new URL("../wasm/FoxitFixed.pfb", import.meta.url);
|
|
47
|
+
case "FoxitFixedBold.pfb":
|
|
48
|
+
return new URL("../wasm/FoxitFixedBold.pfb", import.meta.url);
|
|
49
|
+
case "FoxitFixedBoldItalic.pfb":
|
|
50
|
+
return new URL("../wasm/FoxitFixedBoldItalic.pfb", import.meta.url);
|
|
51
|
+
case "FoxitFixedItalic.pfb":
|
|
52
|
+
return new URL("../wasm/FoxitFixedItalic.pfb", import.meta.url);
|
|
53
|
+
case "FoxitSans.pfb":
|
|
54
|
+
return new URL("../wasm/FoxitSans.pfb", import.meta.url);
|
|
55
|
+
case "FoxitSansBold.pfb":
|
|
56
|
+
return new URL("../wasm/FoxitSansBold.pfb", import.meta.url);
|
|
57
|
+
case "FoxitSansBoldItalic.pfb":
|
|
58
|
+
return new URL("../wasm/FoxitSansBoldItalic.pfb", import.meta.url);
|
|
59
|
+
case "FoxitSansItalic.pfb":
|
|
60
|
+
return new URL("../wasm/FoxitSansItalic.pfb", import.meta.url);
|
|
61
|
+
case "FoxitSerif.pfb":
|
|
62
|
+
return new URL("../wasm/FoxitSerif.pfb", import.meta.url);
|
|
63
|
+
case "FoxitSerifBold.pfb":
|
|
64
|
+
return new URL("../wasm/FoxitSerifBold.pfb", import.meta.url);
|
|
65
|
+
case "FoxitSerifBoldItalic.pfb":
|
|
66
|
+
return new URL("../wasm/FoxitSerifBoldItalic.pfb", import.meta.url);
|
|
67
|
+
case "FoxitSerifItalic.pfb":
|
|
68
|
+
return new URL("../wasm/FoxitSerifItalic.pfb", import.meta.url);
|
|
69
|
+
case "FoxitSymbol.pfb":
|
|
70
|
+
return new URL("../wasm/FoxitSymbol.pfb", import.meta.url);
|
|
71
|
+
case "FoxitDingbats.pfb":
|
|
72
|
+
return new URL("../wasm/FoxitDingbats.pfb", import.meta.url);
|
|
22
73
|
case "cjk-fallback.ttf":
|
|
23
74
|
return new URL("../wasm/cjk-fallback.ttf", import.meta.url);
|
|
24
|
-
case "cmaps.
|
|
25
|
-
return new URL("../wasm/cmaps.
|
|
75
|
+
case "cmaps.deflate":
|
|
76
|
+
return new URL("../wasm/cmaps.deflate", import.meta.url);
|
|
26
77
|
}
|
|
27
78
|
}
|
|
28
79
|
/** A stalled server must not block the thread for ever; the largest asset is 5 MB. */
|
|
@@ -71,11 +122,16 @@ function loadFailure(name, url, reason, cause) {
|
|
|
71
122
|
return new PdfError("missing-asset", `recto could not load ${name}${url ? ` from ${url.href}` : ""}: ${reason}. It ships in the package's wasm/ ` +
|
|
72
123
|
"directory. If a bundler or dev server moved the package without its files, copy them " +
|
|
73
124
|
"somewhere it serves and pass that directory as `assetBase` (with Vite's dev server, add " +
|
|
74
|
-
'optimizeDeps: { exclude: ["@2018kguo/recto"] } instead).', cause);
|
|
125
|
+
'optimizeDeps: { exclude: ["@2018kguo/recto"] } instead; a server bundle should keep the package external).', cause);
|
|
75
126
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
127
|
+
function notThisBuild(name, url, bytes) {
|
|
128
|
+
return loadFailure(name, url, `the ${bytes.byteLength} bytes there are not this build's file (a server's fallback page, or a different version of the package)`);
|
|
129
|
+
}
|
|
130
|
+
function checked(name, url, bytes, hash) {
|
|
131
|
+
const crypto = globalThis.process?.getBuiltinModule?.("node:crypto");
|
|
132
|
+
const hex = crypto ? crypto.createHash("sha256").update(bytes).digest("hex") : (hash?.(bytes) ?? sha256Hex(bytes));
|
|
133
|
+
if (hex !== PINS[name]) {
|
|
134
|
+
throw notThisBuild(name, url, bytes);
|
|
79
135
|
}
|
|
80
136
|
return bytes;
|
|
81
137
|
}
|
|
@@ -93,32 +149,52 @@ function readSync(url) {
|
|
|
93
149
|
}
|
|
94
150
|
return new Uint8Array(xhr.response);
|
|
95
151
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
152
|
+
/** `crypto.subtle` is only there in a secure context; a plain-http page hashes here. */
|
|
153
|
+
async function hashOf(bytes) {
|
|
154
|
+
const subtle = globalThis.crypto?.subtle;
|
|
155
|
+
if (!subtle) {
|
|
156
|
+
return sha256Hex(bytes);
|
|
157
|
+
}
|
|
158
|
+
const digest = new Uint8Array(await subtle.digest("SHA-256", bytes));
|
|
159
|
+
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
160
|
+
}
|
|
161
|
+
/** The only way bytes get here from a network without blocking: whole, and hashed before they're used. */
|
|
162
|
+
async function fetchBytes(name, url) {
|
|
163
|
+
const response = await fetch(url);
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
throw new Error(`HTTP ${response.status}`);
|
|
166
|
+
}
|
|
167
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
168
|
+
if ((await hashOf(bytes)) !== PINS[name]) {
|
|
169
|
+
throw notThisBuild(name, url, bytes);
|
|
170
|
+
}
|
|
171
|
+
return bytes;
|
|
172
|
+
}
|
|
173
|
+
/** The wasm, verified and then compiled. */
|
|
174
|
+
export async function compileWasm(name, base) {
|
|
98
175
|
let url = null;
|
|
99
176
|
let bytes;
|
|
100
177
|
try {
|
|
101
178
|
// Inside the try: under a blob: worker `import.meta.url` is no base for a URL.
|
|
102
|
-
url = assetUrl(
|
|
103
|
-
|
|
104
|
-
bytes = readFromDisk(url);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
const response = await fetch(url);
|
|
108
|
-
if (!response.ok) {
|
|
109
|
-
throw new Error(`HTTP ${response.status}`);
|
|
110
|
-
}
|
|
111
|
-
bytes = new Uint8Array(await response.arrayBuffer());
|
|
112
|
-
}
|
|
179
|
+
url = assetUrl(name, base);
|
|
180
|
+
bytes = (nodeFs() ? checked(name, url, readFromDisk(url)) : await fetchBytes(name, url));
|
|
113
181
|
}
|
|
114
182
|
catch (error) {
|
|
115
|
-
|
|
183
|
+
if (error instanceof PdfError) {
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
throw loadFailure(name, url, error instanceof Error ? error.message : String(error), error);
|
|
116
187
|
}
|
|
117
|
-
return
|
|
188
|
+
return await compiling(name, WebAssembly.compile(bytes));
|
|
189
|
+
}
|
|
190
|
+
function compiling(name, module) {
|
|
191
|
+
return module.catch((error) => {
|
|
192
|
+
throw new PdfError("missing-asset", `recto could not compile ${name}: ${String(error)}. A Content-Security-Policy must allow 'wasm-unsafe-eval'.`, error);
|
|
193
|
+
});
|
|
118
194
|
}
|
|
119
195
|
/** Verified assets by URL: hash and read each once per thread. */
|
|
120
196
|
const loaded = new Map();
|
|
121
|
-
/** Fetches assets ahead of time
|
|
197
|
+
/** Fetches and verifies assets ahead of time; a thread that can't read them when needed has to. */
|
|
122
198
|
export async function preloadAssets(base, names) {
|
|
123
199
|
await Promise.all(names.map(async (name) => {
|
|
124
200
|
let url = null;
|
|
@@ -128,28 +204,40 @@ export async function preloadAssets(base, names) {
|
|
|
128
204
|
if (loaded.has(url.href)) {
|
|
129
205
|
return;
|
|
130
206
|
}
|
|
131
|
-
|
|
132
|
-
if (!response.ok) {
|
|
133
|
-
throw new Error(`HTTP ${response.status}`);
|
|
134
|
-
}
|
|
135
|
-
bytes = new Uint8Array(await response.arrayBuffer());
|
|
207
|
+
bytes = nodeFs() ? checked(name, url, readFromDisk(url)) : await fetchBytes(name, url);
|
|
136
208
|
}
|
|
137
209
|
catch (error) {
|
|
210
|
+
if (error instanceof PdfError) {
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
138
213
|
throw loadFailure(name, url, error instanceof Error ? error.message : String(error), error);
|
|
139
214
|
}
|
|
140
|
-
loaded.set(url.href,
|
|
215
|
+
loaded.set(url.href, bytes);
|
|
141
216
|
}));
|
|
142
217
|
}
|
|
143
218
|
/** An asset, read synchronously. Throws a PdfError rather than returning nothing. */
|
|
144
|
-
export function loadAssetSync(name, base) {
|
|
219
|
+
export function loadAssetSync(name, base, hash) {
|
|
145
220
|
let url = null;
|
|
146
|
-
let bytes;
|
|
147
221
|
try {
|
|
148
222
|
url = assetUrl(name, base);
|
|
149
223
|
const cached = loaded.get(url.href);
|
|
150
224
|
if (cached) {
|
|
151
225
|
return cached;
|
|
152
226
|
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
throw loadFailure(name, url, error instanceof Error ? error.message : String(error), error);
|
|
230
|
+
}
|
|
231
|
+
const bytes = readCheckedSync(name, base, hash);
|
|
232
|
+
loaded.set(url.href, bytes);
|
|
233
|
+
return bytes;
|
|
234
|
+
}
|
|
235
|
+
/** A file, read now and hashed before it's returned, and not kept: a part is compiled once. */
|
|
236
|
+
export function readCheckedSync(name, base, hash) {
|
|
237
|
+
let url = null;
|
|
238
|
+
let bytes;
|
|
239
|
+
try {
|
|
240
|
+
url = assetUrl(name, base);
|
|
153
241
|
if (!canReadSync()) {
|
|
154
242
|
throw new Error("this thread can only use assets fetched up front, and it was left out of preloadAssets");
|
|
155
243
|
}
|
|
@@ -158,12 +246,5 @@ export function loadAssetSync(name, base) {
|
|
|
158
246
|
catch (error) {
|
|
159
247
|
throw loadFailure(name, url, error instanceof Error ? error.message : String(error), error);
|
|
160
248
|
}
|
|
161
|
-
|
|
162
|
-
return bytes;
|
|
163
|
-
}
|
|
164
|
-
/** Entry `index` of fonts.bin: u32 count, u32 offsets[count + 1], bytes (little-endian). */
|
|
165
|
-
export function fontFromBundle(bundle, index) {
|
|
166
|
-
const view = new DataView(bundle.buffer, bundle.byteOffset, bundle.byteLength);
|
|
167
|
-
const base = 4 + (view.getUint32(0, true) + 1) * 4;
|
|
168
|
-
return bundle.subarray(base + view.getUint32(4 + index * 4, true), base + view.getUint32(8 + index * 4, true));
|
|
249
|
+
return checked(name, url, bytes, hash);
|
|
169
250
|
}
|
package/dist/document.d.ts
CHANGED
|
@@ -171,6 +171,12 @@ export type PdfRenderOptions = {
|
|
|
171
171
|
/** Default 4096. May be Infinity, to fit the width alone. A render is capped at 16,384 px a side and 64 Mpx. */
|
|
172
172
|
maxHeight?: number;
|
|
173
173
|
};
|
|
174
|
+
export type PdfRenderAsyncOptions = PdfRenderOptions & {
|
|
175
|
+
/** Stops the render at the next pause: `aborted`, with the document still usable. */
|
|
176
|
+
signal?: AbortSignal;
|
|
177
|
+
/** About how long it runs before giving the thread back. Default 8. */
|
|
178
|
+
sliceMs?: number;
|
|
179
|
+
};
|
|
174
180
|
export type PdfBitmap = {
|
|
175
181
|
width: number;
|
|
176
182
|
height: number;
|
|
@@ -204,6 +210,18 @@ export interface PdfDocument {
|
|
|
204
210
|
readonly pageCount: number;
|
|
205
211
|
/** The document's bookmarks; empty if it has none. */
|
|
206
212
|
outline(): PdfOutlineItem[];
|
|
213
|
+
/** What each page is called, one string per page ("iv", "A-5"), or null if the file doesn't say. */
|
|
214
|
+
pageLabels(): string[] | null;
|
|
215
|
+
/**
|
|
216
|
+
* Which of the document's trees were cut short: `pages` means `pageCount` may be short, by a limit or
|
|
217
|
+
* because a damaged tree left no pages (an honestly empty document says false). `pageLabels` and `outline`
|
|
218
|
+
* mean those passed a limit, and may be wrong or incomplete. A page's own limits are `truncated` on its results.
|
|
219
|
+
*/
|
|
220
|
+
structureTruncated(): {
|
|
221
|
+
pages: boolean;
|
|
222
|
+
pageLabels: boolean;
|
|
223
|
+
outline: boolean;
|
|
224
|
+
};
|
|
207
225
|
pageInfo(index: number): PdfPageInfo;
|
|
208
226
|
text(index: number): PdfPageText;
|
|
209
227
|
/** `text`, `objects` and `annotations` from one interpretation of the page. */
|
|
@@ -220,16 +238,34 @@ export interface PdfDocument {
|
|
|
220
238
|
objects(index: number): PdfPageObjects;
|
|
221
239
|
annotations(index: number): PdfAnnotation[];
|
|
222
240
|
render(index: number, options: PdfRenderOptions): PdfBitmap;
|
|
241
|
+
/**
|
|
242
|
+
* The same pixels as `render`, giving the thread back between slices, and stoppable with a
|
|
243
|
+
* signal. The last step, rasterizing, is one piece: about a third of a heavy page's time.
|
|
244
|
+
*/
|
|
245
|
+
renderAsync(index: number, options: PdfRenderAsyncOptions): Promise<PdfBitmap>;
|
|
223
246
|
close(): void;
|
|
224
247
|
[Symbol.dispose](): void;
|
|
225
248
|
}
|
|
226
249
|
export type Instance = {
|
|
227
250
|
glue: ReturnType<typeof createGlue>;
|
|
251
|
+
/** Loads a part of the wasm, hashed first, or throws `missing-asset` having changed nothing. */
|
|
252
|
+
loadPart(name: "recto-render.wasm" | "recto-more.wasm" | "recto-rare.wasm"): void;
|
|
228
253
|
memory: WebAssembly.Memory;
|
|
254
|
+
/** Its memory when it was made: past this, something used it, and dropping it gives memory back. */
|
|
255
|
+
startBytes: number;
|
|
229
256
|
generation: number;
|
|
230
257
|
openDocuments: number;
|
|
231
|
-
/**
|
|
258
|
+
/** The engine's count of documents open, on any of its instances. */
|
|
259
|
+
documents: {
|
|
260
|
+
open: number;
|
|
261
|
+
};
|
|
262
|
+
/** Set once the instance trapped; nothing may call it again. */
|
|
232
263
|
dead: boolean;
|
|
264
|
+
/**
|
|
265
|
+
* Set once a part or asset failed to load inside a call. Nothing more runs on it, but a document's
|
|
266
|
+
* file is still whole there, so it reopens on another instance at its next call.
|
|
267
|
+
*/
|
|
268
|
+
abandoned: boolean;
|
|
233
269
|
/** An asset's load failure, rethrown by the call that needed the asset. */
|
|
234
270
|
failure: PdfError | undefined;
|
|
235
271
|
/** Called when a document on this instance closes. */
|
|
@@ -240,10 +276,19 @@ export declare function engineFailure(instance: Instance, error: unknown): unkno
|
|
|
240
276
|
export declare class RectoDocument implements PdfDocument {
|
|
241
277
|
#private;
|
|
242
278
|
readonly pageCount: number;
|
|
243
|
-
constructor(instance: Instance, doc: Glue.Document,
|
|
279
|
+
constructor(instance: Instance, doc: Glue.Document, reopen: (doc: Glue.Document, instance: Instance) => {
|
|
280
|
+
instance: Instance;
|
|
281
|
+
doc: Glue.Document;
|
|
282
|
+
}, fetchFailure?: () => PdfError | null);
|
|
244
283
|
pageInfo(index: number): PdfPageInfo;
|
|
245
284
|
text(index: number): PdfPageText;
|
|
246
285
|
objects(index: number): PdfPageObjects;
|
|
286
|
+
pageLabels(): string[] | null;
|
|
287
|
+
structureTruncated(): {
|
|
288
|
+
pages: boolean;
|
|
289
|
+
pageLabels: boolean;
|
|
290
|
+
outline: boolean;
|
|
291
|
+
};
|
|
247
292
|
outline(): PdfOutlineItem[];
|
|
248
293
|
annotations(index: number): PdfAnnotation[];
|
|
249
294
|
scan(index: number): {
|
|
@@ -256,6 +301,7 @@ export declare class RectoDocument implements PdfDocument {
|
|
|
256
301
|
annotations: PdfAnnotation[];
|
|
257
302
|
};
|
|
258
303
|
render(index: number, options: PdfRenderOptions): PdfBitmap;
|
|
304
|
+
renderAsync(index: number, options: PdfRenderAsyncOptions): Promise<PdfBitmap>;
|
|
259
305
|
close(): void;
|
|
260
306
|
[Symbol.dispose](): void;
|
|
261
307
|
}
|
package/dist/document.js
CHANGED
|
@@ -167,19 +167,35 @@ function toAnnotations(a) {
|
|
|
167
167
|
return out;
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
|
+
const DEFAULT_SLICE_MS = 8;
|
|
171
|
+
/** Back to the event loop, without the 4 ms a nested timer waits in a browser. */
|
|
172
|
+
function nextTask() {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
const { port1, port2 } = new MessageChannel();
|
|
175
|
+
port1.onmessage = () => {
|
|
176
|
+
port1.close();
|
|
177
|
+
resolve();
|
|
178
|
+
};
|
|
179
|
+
port2.postMessage(0);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
170
182
|
export class RectoDocument {
|
|
171
183
|
pageCount;
|
|
172
184
|
#closed = false;
|
|
173
185
|
#doc;
|
|
174
186
|
#instance;
|
|
187
|
+
/** The same file on another instance. */
|
|
188
|
+
#reopen;
|
|
175
189
|
/** A ranged read's failure, thrown by the call whose fetch it failed. */
|
|
176
190
|
#fetchFailure;
|
|
177
|
-
constructor(instance, doc, fetchFailure = () => null) {
|
|
191
|
+
constructor(instance, doc, reopen, fetchFailure = () => null) {
|
|
178
192
|
this.#instance = instance;
|
|
179
193
|
this.#doc = doc;
|
|
194
|
+
this.#reopen = reopen;
|
|
180
195
|
this.#fetchFailure = fetchFailure;
|
|
181
196
|
this.pageCount = doc.pageCount;
|
|
182
197
|
instance.openDocuments += 1;
|
|
198
|
+
instance.documents.open += 1;
|
|
183
199
|
}
|
|
184
200
|
#check(index) {
|
|
185
201
|
if (this.#closed) {
|
|
@@ -188,8 +204,11 @@ export class RectoDocument {
|
|
|
188
204
|
if (!(Number.isInteger(index) && index >= 0 && index < this.pageCount)) {
|
|
189
205
|
throw new RangeError(`page ${index} is out of range (0..${this.pageCount - 1})`);
|
|
190
206
|
}
|
|
191
|
-
|
|
192
|
-
|
|
207
|
+
this.#checkInstance();
|
|
208
|
+
}
|
|
209
|
+
#checkInstance() {
|
|
210
|
+
if (this.#instance.abandoned) {
|
|
211
|
+
this.#moveOffAbandoned();
|
|
193
212
|
}
|
|
194
213
|
if (this.#instance.dead) {
|
|
195
214
|
throw new PdfError("crash", "the engine crashed earlier on this thread; open the document again");
|
|
@@ -215,6 +234,26 @@ export class RectoDocument {
|
|
|
215
234
|
}
|
|
216
235
|
return result;
|
|
217
236
|
}
|
|
237
|
+
/** The call that failed told the caller; this one is a fresh start. */
|
|
238
|
+
#moveOffAbandoned() {
|
|
239
|
+
const old = this.#instance;
|
|
240
|
+
let moved;
|
|
241
|
+
try {
|
|
242
|
+
moved = this.#reopen(this.#doc, old);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
throw engineFailure(old, error);
|
|
246
|
+
}
|
|
247
|
+
old.openDocuments -= 1;
|
|
248
|
+
old.onClose?.();
|
|
249
|
+
this.#instance = moved.instance;
|
|
250
|
+
this.#doc = moved.doc;
|
|
251
|
+
moved.instance.openDocuments += 1;
|
|
252
|
+
}
|
|
253
|
+
/** Loaded ahead of the call, so that failing to get it leaves nothing half-done. */
|
|
254
|
+
#needsRender() {
|
|
255
|
+
this.#call(() => this.#instance.loadPart("recto-render.wasm"));
|
|
256
|
+
}
|
|
218
257
|
pageInfo(index) {
|
|
219
258
|
this.#check(index);
|
|
220
259
|
return this.#call(() => {
|
|
@@ -228,12 +267,29 @@ export class RectoDocument {
|
|
|
228
267
|
}
|
|
229
268
|
objects(index) {
|
|
230
269
|
this.#check(index);
|
|
270
|
+
this.#needsRender();
|
|
231
271
|
return this.#call(() => toObjects(this.#doc.objects(index)));
|
|
232
272
|
}
|
|
273
|
+
pageLabels() {
|
|
274
|
+
if (this.#closed) {
|
|
275
|
+
throw new PdfError("closed", "this document is closed");
|
|
276
|
+
}
|
|
277
|
+
this.#checkInstance();
|
|
278
|
+
return this.#call(() => this.#doc.pageLabels());
|
|
279
|
+
}
|
|
280
|
+
structureTruncated() {
|
|
281
|
+
if (this.#closed) {
|
|
282
|
+
throw new PdfError("closed", "this document is closed");
|
|
283
|
+
}
|
|
284
|
+
this.#checkInstance();
|
|
285
|
+
const [pages, pageLabels, outline] = this.#call(() => this.#doc.structureTruncated());
|
|
286
|
+
return { pages: pages === 1, pageLabels: pageLabels === 1, outline: outline === 1 };
|
|
287
|
+
}
|
|
233
288
|
outline() {
|
|
234
289
|
if (this.#closed) {
|
|
235
290
|
throw new PdfError("closed", "this document is closed");
|
|
236
291
|
}
|
|
292
|
+
this.#checkInstance();
|
|
237
293
|
return this.#call(() => {
|
|
238
294
|
const top = [];
|
|
239
295
|
// The open item at each level; an item's parent is the last one a level up.
|
|
@@ -252,6 +308,7 @@ export class RectoDocument {
|
|
|
252
308
|
}
|
|
253
309
|
scan(index) {
|
|
254
310
|
this.#check(index);
|
|
311
|
+
this.#needsRender();
|
|
255
312
|
return this.#call(() => consume(this.#doc.scan(index), (scan) => ({
|
|
256
313
|
text: toText(scan.takeText()),
|
|
257
314
|
objects: toObjects(scan.takeObjects()),
|
|
@@ -272,6 +329,7 @@ export class RectoDocument {
|
|
|
272
329
|
!(maxHeight > 0)) {
|
|
273
330
|
throw new RangeError(`render width must be in 1..${MAX_DIMENSION} and maxHeight above 0`);
|
|
274
331
|
}
|
|
332
|
+
this.#needsRender();
|
|
275
333
|
return this.#call(() => consume(this.#doc.render(index, options.width, maxHeight), (page) => ({
|
|
276
334
|
width: page.width,
|
|
277
335
|
height: page.height,
|
|
@@ -279,13 +337,60 @@ export class RectoDocument {
|
|
|
279
337
|
rgba: page.takeRgba(),
|
|
280
338
|
})));
|
|
281
339
|
}
|
|
340
|
+
async renderAsync(index, options) {
|
|
341
|
+
this.#check(index);
|
|
342
|
+
const maxHeight = options.maxHeight ?? DEFAULT_MAX_HEIGHT;
|
|
343
|
+
if (!(options.width > 0 && options.width <= MAX_DIMENSION) ||
|
|
344
|
+
!(maxHeight > 0)) {
|
|
345
|
+
throw new RangeError(`render width must be in 1..${MAX_DIMENSION} and maxHeight above 0`);
|
|
346
|
+
}
|
|
347
|
+
this.#needsRender();
|
|
348
|
+
const instance = this.#instance;
|
|
349
|
+
const sliceMs = options.sliceMs ?? DEFAULT_SLICE_MS;
|
|
350
|
+
const task = this.#call(() => this.#doc.renderTask(index, options.width, maxHeight));
|
|
351
|
+
const stopIfAsked = () => {
|
|
352
|
+
if (options.signal?.aborted) {
|
|
353
|
+
throw new PdfError("aborted", "the signal was aborted", options.signal.reason);
|
|
354
|
+
}
|
|
355
|
+
if (this.#closed) {
|
|
356
|
+
throw new PdfError("closed", "this document is closed");
|
|
357
|
+
}
|
|
358
|
+
if (instance.dead) {
|
|
359
|
+
throw new PdfError("crash", "the engine crashed while this render was paused; open the document again");
|
|
360
|
+
}
|
|
361
|
+
if (instance.abandoned) {
|
|
362
|
+
throw instance.failure ?? new PdfError("missing-asset", "a file the engine needed failed to load while this render was paused; render again");
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
try {
|
|
366
|
+
stopIfAsked();
|
|
367
|
+
while (!this.#call(() => task.step(sliceMs))) {
|
|
368
|
+
await nextTask();
|
|
369
|
+
stopIfAsked();
|
|
370
|
+
}
|
|
371
|
+
await nextTask();
|
|
372
|
+
stopIfAsked();
|
|
373
|
+
return this.#call(() => consume(task.finish(), (page) => ({
|
|
374
|
+
width: page.width,
|
|
375
|
+
height: page.height,
|
|
376
|
+
truncated: page.truncated,
|
|
377
|
+
rgba: page.takeRgba(),
|
|
378
|
+
})));
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
if (!instance.dead && !instance.abandoned) {
|
|
382
|
+
task.free();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
282
386
|
close() {
|
|
283
387
|
if (this.#closed) {
|
|
284
388
|
return;
|
|
285
389
|
}
|
|
286
390
|
this.#closed = true;
|
|
287
391
|
this.#instance.openDocuments -= 1;
|
|
288
|
-
|
|
392
|
+
this.#instance.documents.open -= 1;
|
|
393
|
+
if (!this.#instance.dead && !this.#instance.abandoned) {
|
|
289
394
|
this.#doc.free();
|
|
290
395
|
}
|
|
291
396
|
this.#instance.onClose?.();
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type AssetName } from "./assets.ts";
|
|
2
|
-
import { type PdfDocument } from "./document.ts";
|
|
1
|
+
import { type AssetName, type SyncHash } from "./assets.ts";
|
|
2
|
+
import { type Instance, type PdfDocument } from "./document.ts";
|
|
3
3
|
import { type PdfRangedDocument, type PdfRangedOpenOptions, type PdfRangeSource } from "./ranged.ts";
|
|
4
4
|
export type PdfEngineOptions = {
|
|
5
5
|
/**
|
|
@@ -8,16 +8,22 @@ export type PdfEngineOptions = {
|
|
|
8
8
|
* path or file: URL in Node. Defaults to the package's own wasm/ directory.
|
|
9
9
|
*/
|
|
10
10
|
assetBase?: string | URL;
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
11
|
+
/**
|
|
12
|
+
* Past this much linear memory, the instance is dropped when its last document closes, or replaced
|
|
13
|
+
* at the next open if one is still open. By default it's dropped a second after the last close, once
|
|
14
|
+
* anything has used it, and an open before then reuses it; past 512 MiB, at the close; and past 64 MiB
|
|
15
|
+
* with a document still open, the next open gets a new one.
|
|
16
|
+
*/
|
|
13
17
|
recycleAboveBytes?: number;
|
|
14
18
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* Files to fetch and verify while the engine is created, so that a missing one fails here and no later
|
|
20
|
+
* call waits for it. `recto-render.wasm` saves the first `render` that wait; all three parts check the
|
|
21
|
+
* whole engine at startup. The default is none, except where a thread can't read a file when it's needed
|
|
22
|
+
* (a page's main thread, a service worker): there it's all of them, about 9.6 MB. Name fewer to fetch less, and
|
|
23
|
+
* a call that needs a font or CMaps you left out fails with `missing-asset`. The parts are fetched there
|
|
24
|
+
* whatever you name, so it can always draw.
|
|
19
25
|
*/
|
|
20
|
-
preloadAssets?:
|
|
26
|
+
preloadAssets?: (AssetName | "cmaps.brotli" | "fonts.bin")[];
|
|
21
27
|
};
|
|
22
28
|
export type PdfOpenOptions = {
|
|
23
29
|
password?: string;
|
|
@@ -44,6 +50,8 @@ export interface PdfEngine {
|
|
|
44
50
|
openRanged(source: PdfRangeSource, options?: PdfRangedOpenOptions): Promise<PdfRangedDocument>;
|
|
45
51
|
stats(): PdfEngineStats;
|
|
46
52
|
}
|
|
53
|
+
/** An instance's own SHA-256, for an asset it needs: faster than JavaScript's in a cold browser. */
|
|
54
|
+
export declare function hashOn(instance: Pick<Instance, "dead" | "glue">): SyncHash;
|
|
47
55
|
export declare function createPdfEngine(options?: PdfEngineOptions): Promise<PdfEngine>;
|
|
48
56
|
/**
|
|
49
57
|
* Opens a PDF on this thread's shared engine:
|