@2018kguo/recto 0.1.0 → 0.5.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 +144 -2
- package/dist/document.js +163 -14
- package/dist/engine.d.ts +24 -9
- package/dist/engine.js +236 -49
- 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 +15 -0
- package/dist/pool-worker.js +65 -0
- package/dist/pool.d.ts +42 -0
- package/dist/pool.js +295 -0
- package/dist/ranged.d.ts +44 -9
- package/dist/ranged.js +571 -86
- 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 +213 -19
- package/wasm/recto.types.d.mts +83 -4
- package/wasm/recto.wasm +0 -0
- package/wasm/cmaps.brotli +0 -0
- package/wasm/fonts.bin +0 -0
package/dist/document.js
CHANGED
|
@@ -45,6 +45,37 @@ export const ANNOT_SUBTYPE = {
|
|
|
45
45
|
};
|
|
46
46
|
const DEFAULT_MAX_HEIGHT = 4096;
|
|
47
47
|
const MAX_DIMENSION = 16_384;
|
|
48
|
+
/** With a region, the whole page's bitmap may be up to this wide. */
|
|
49
|
+
const MAX_REGION_DIMENSION = 262_144;
|
|
50
|
+
/** `width`, `maxHeight` and the region as the engine takes them (NaNs for none), or a RangeError. */
|
|
51
|
+
function renderArgs(options) {
|
|
52
|
+
const maxHeight = options.maxHeight ?? DEFAULT_MAX_HEIGHT;
|
|
53
|
+
const { region } = options;
|
|
54
|
+
const most = region ? MAX_REGION_DIMENSION : MAX_DIMENSION;
|
|
55
|
+
if (!(options.width > 0 && options.width <= most) || !(maxHeight > 0)) {
|
|
56
|
+
throw new RangeError(region
|
|
57
|
+
? `with a region, render width must be in 1..${MAX_REGION_DIMENSION} and maxHeight above 0`
|
|
58
|
+
: `render width must be in 1..${MAX_DIMENSION} and maxHeight above 0`);
|
|
59
|
+
}
|
|
60
|
+
if (!region) {
|
|
61
|
+
return [options.width, maxHeight, Number.NaN, Number.NaN, Number.NaN, Number.NaN];
|
|
62
|
+
}
|
|
63
|
+
const { x, y, width, height } = region;
|
|
64
|
+
if (![x, y, width, height].every((v) => typeof v === "number" && Number.isFinite(v)) || !(width > 0 && height > 0)) {
|
|
65
|
+
throw new RangeError("a render's region needs finite x and y, and width and height above 0");
|
|
66
|
+
}
|
|
67
|
+
return [options.width, maxHeight, x, y, width, height];
|
|
68
|
+
}
|
|
69
|
+
/** A rendered page as `render` returns it: with a region, where it is on the page. */
|
|
70
|
+
function bitmapOf(page, region) {
|
|
71
|
+
const bitmap = {
|
|
72
|
+
width: page.width,
|
|
73
|
+
height: page.height,
|
|
74
|
+
truncated: page.truncated,
|
|
75
|
+
rgba: page.takeRgba(),
|
|
76
|
+
};
|
|
77
|
+
return region ? { ...bitmap, x: page.x, y: page.y, pageWidth: page.pageWidth, pageHeight: page.pageHeight } : bitmap;
|
|
78
|
+
}
|
|
48
79
|
/** What a call into the instance threw, as what the caller should see. */
|
|
49
80
|
export function engineFailure(instance, error) {
|
|
50
81
|
if (error instanceof PdfError) {
|
|
@@ -167,19 +198,60 @@ function toAnnotations(a) {
|
|
|
167
198
|
return out;
|
|
168
199
|
});
|
|
169
200
|
}
|
|
201
|
+
function toLink([annotation, x, y, width, height, kind, text, page, view, destX, destY, zoom, rx, ry, rw, rh, name]) {
|
|
202
|
+
const number = (v) => (Number.isNaN(v) ? null : v);
|
|
203
|
+
const target = kind === "uri"
|
|
204
|
+
? { kind, uri: text }
|
|
205
|
+
: kind === "page"
|
|
206
|
+
? {
|
|
207
|
+
kind,
|
|
208
|
+
destination: {
|
|
209
|
+
page,
|
|
210
|
+
view,
|
|
211
|
+
x: number(destX),
|
|
212
|
+
y: number(destY),
|
|
213
|
+
zoom: number(zoom),
|
|
214
|
+
box: Number.isNaN(rx) ? null : { x: rx, y: ry, width: rw, height: rh },
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
: kind === "remote"
|
|
218
|
+
? { kind, file: text, name, page: page < 0 ? null : page }
|
|
219
|
+
: kind === "launch"
|
|
220
|
+
? { kind, file: text }
|
|
221
|
+
: kind === "action"
|
|
222
|
+
? { kind, action: text, name }
|
|
223
|
+
: { kind: "none" };
|
|
224
|
+
return { annotation, box: { x, y, width, height }, target };
|
|
225
|
+
}
|
|
226
|
+
const DEFAULT_SLICE_MS = 8;
|
|
227
|
+
/** Back to the event loop, without the 4 ms a nested timer waits in a browser. */
|
|
228
|
+
function nextTask() {
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
const { port1, port2 } = new MessageChannel();
|
|
231
|
+
port1.onmessage = () => {
|
|
232
|
+
port1.close();
|
|
233
|
+
resolve();
|
|
234
|
+
};
|
|
235
|
+
port2.postMessage(0);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
170
238
|
export class RectoDocument {
|
|
171
239
|
pageCount;
|
|
172
240
|
#closed = false;
|
|
173
241
|
#doc;
|
|
174
242
|
#instance;
|
|
243
|
+
/** The same file on another instance. */
|
|
244
|
+
#reopen;
|
|
175
245
|
/** A ranged read's failure, thrown by the call whose fetch it failed. */
|
|
176
246
|
#fetchFailure;
|
|
177
|
-
constructor(instance, doc, fetchFailure = () => null) {
|
|
247
|
+
constructor(instance, doc, reopen, fetchFailure = () => null) {
|
|
178
248
|
this.#instance = instance;
|
|
179
249
|
this.#doc = doc;
|
|
250
|
+
this.#reopen = reopen;
|
|
180
251
|
this.#fetchFailure = fetchFailure;
|
|
181
252
|
this.pageCount = doc.pageCount;
|
|
182
253
|
instance.openDocuments += 1;
|
|
254
|
+
instance.documents.open += 1;
|
|
183
255
|
}
|
|
184
256
|
#check(index) {
|
|
185
257
|
if (this.#closed) {
|
|
@@ -188,8 +260,11 @@ export class RectoDocument {
|
|
|
188
260
|
if (!(Number.isInteger(index) && index >= 0 && index < this.pageCount)) {
|
|
189
261
|
throw new RangeError(`page ${index} is out of range (0..${this.pageCount - 1})`);
|
|
190
262
|
}
|
|
191
|
-
|
|
192
|
-
|
|
263
|
+
this.#checkInstance();
|
|
264
|
+
}
|
|
265
|
+
#checkInstance() {
|
|
266
|
+
if (this.#instance.abandoned) {
|
|
267
|
+
this.#moveOffAbandoned();
|
|
193
268
|
}
|
|
194
269
|
if (this.#instance.dead) {
|
|
195
270
|
throw new PdfError("crash", "the engine crashed earlier on this thread; open the document again");
|
|
@@ -215,6 +290,26 @@ export class RectoDocument {
|
|
|
215
290
|
}
|
|
216
291
|
return result;
|
|
217
292
|
}
|
|
293
|
+
/** The call that failed told the caller; this one is a fresh start. */
|
|
294
|
+
#moveOffAbandoned() {
|
|
295
|
+
const old = this.#instance;
|
|
296
|
+
let moved;
|
|
297
|
+
try {
|
|
298
|
+
moved = this.#reopen(this.#doc, old);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
throw engineFailure(old, error);
|
|
302
|
+
}
|
|
303
|
+
old.openDocuments -= 1;
|
|
304
|
+
old.onClose?.();
|
|
305
|
+
this.#instance = moved.instance;
|
|
306
|
+
this.#doc = moved.doc;
|
|
307
|
+
moved.instance.openDocuments += 1;
|
|
308
|
+
}
|
|
309
|
+
/** Loaded ahead of the call, so that failing to get it leaves nothing half-done. */
|
|
310
|
+
#needsRender() {
|
|
311
|
+
this.#call(() => this.#instance.loadPart("recto-render.wasm"));
|
|
312
|
+
}
|
|
218
313
|
pageInfo(index) {
|
|
219
314
|
this.#check(index);
|
|
220
315
|
return this.#call(() => {
|
|
@@ -228,12 +323,29 @@ export class RectoDocument {
|
|
|
228
323
|
}
|
|
229
324
|
objects(index) {
|
|
230
325
|
this.#check(index);
|
|
326
|
+
this.#needsRender();
|
|
231
327
|
return this.#call(() => toObjects(this.#doc.objects(index)));
|
|
232
328
|
}
|
|
329
|
+
pageLabels() {
|
|
330
|
+
if (this.#closed) {
|
|
331
|
+
throw new PdfError("closed", "this document is closed");
|
|
332
|
+
}
|
|
333
|
+
this.#checkInstance();
|
|
334
|
+
return this.#call(() => this.#doc.pageLabels());
|
|
335
|
+
}
|
|
336
|
+
structureTruncated() {
|
|
337
|
+
if (this.#closed) {
|
|
338
|
+
throw new PdfError("closed", "this document is closed");
|
|
339
|
+
}
|
|
340
|
+
this.#checkInstance();
|
|
341
|
+
const [pages, pageLabels, outline] = this.#call(() => this.#doc.structureTruncated());
|
|
342
|
+
return { pages: pages === 1, pageLabels: pageLabels === 1, outline: outline === 1 };
|
|
343
|
+
}
|
|
233
344
|
outline() {
|
|
234
345
|
if (this.#closed) {
|
|
235
346
|
throw new PdfError("closed", "this document is closed");
|
|
236
347
|
}
|
|
348
|
+
this.#checkInstance();
|
|
237
349
|
return this.#call(() => {
|
|
238
350
|
const top = [];
|
|
239
351
|
// The open item at each level; an item's parent is the last one a level up.
|
|
@@ -250,8 +362,15 @@ export class RectoDocument {
|
|
|
250
362
|
this.#check(index);
|
|
251
363
|
return this.#call(() => toAnnotations(this.#doc.annotations(index)));
|
|
252
364
|
}
|
|
365
|
+
links(index) {
|
|
366
|
+
this.#check(index);
|
|
367
|
+
// A viewer's call: its code is in drawing's part, so text alone downloads none of it.
|
|
368
|
+
this.#needsRender();
|
|
369
|
+
return this.#call(() => this.#doc.links(index).map(toLink));
|
|
370
|
+
}
|
|
253
371
|
scan(index) {
|
|
254
372
|
this.#check(index);
|
|
373
|
+
this.#needsRender();
|
|
255
374
|
return this.#call(() => consume(this.#doc.scan(index), (scan) => ({
|
|
256
375
|
text: toText(scan.takeText()),
|
|
257
376
|
objects: toObjects(scan.takeObjects()),
|
|
@@ -267,17 +386,46 @@ export class RectoDocument {
|
|
|
267
386
|
}
|
|
268
387
|
render(index, options) {
|
|
269
388
|
this.#check(index);
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
389
|
+
const args = renderArgs(options);
|
|
390
|
+
this.#needsRender();
|
|
391
|
+
return this.#call(() => consume(this.#doc.render(index, ...args), (page) => bitmapOf(page, options.region !== undefined)));
|
|
392
|
+
}
|
|
393
|
+
async renderAsync(index, options) {
|
|
394
|
+
this.#check(index);
|
|
395
|
+
const args = renderArgs(options);
|
|
396
|
+
this.#needsRender();
|
|
397
|
+
const instance = this.#instance;
|
|
398
|
+
const sliceMs = options.sliceMs ?? DEFAULT_SLICE_MS;
|
|
399
|
+
const task = this.#call(() => this.#doc.renderTask(index, ...args));
|
|
400
|
+
const stopIfAsked = () => {
|
|
401
|
+
if (options.signal?.aborted) {
|
|
402
|
+
throw new PdfError("aborted", "the signal was aborted", options.signal.reason);
|
|
403
|
+
}
|
|
404
|
+
if (this.#closed) {
|
|
405
|
+
throw new PdfError("closed", "this document is closed");
|
|
406
|
+
}
|
|
407
|
+
if (instance.dead) {
|
|
408
|
+
throw new PdfError("crash", "the engine crashed while this render was paused; open the document again");
|
|
409
|
+
}
|
|
410
|
+
if (instance.abandoned) {
|
|
411
|
+
throw instance.failure ?? new PdfError("missing-asset", "a file the engine needed failed to load while this render was paused; render again");
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
try {
|
|
415
|
+
stopIfAsked();
|
|
416
|
+
while (!this.#call(() => task.step(sliceMs))) {
|
|
417
|
+
await nextTask();
|
|
418
|
+
stopIfAsked();
|
|
419
|
+
}
|
|
420
|
+
await nextTask();
|
|
421
|
+
stopIfAsked();
|
|
422
|
+
return this.#call(() => consume(task.finish(), (page) => bitmapOf(page, options.region !== undefined)));
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
if (!instance.dead && !instance.abandoned) {
|
|
426
|
+
task.free();
|
|
427
|
+
}
|
|
274
428
|
}
|
|
275
|
-
return this.#call(() => consume(this.#doc.render(index, options.width, maxHeight), (page) => ({
|
|
276
|
-
width: page.width,
|
|
277
|
-
height: page.height,
|
|
278
|
-
truncated: page.truncated,
|
|
279
|
-
rgba: page.takeRgba(),
|
|
280
|
-
})));
|
|
281
429
|
}
|
|
282
430
|
close() {
|
|
283
431
|
if (this.#closed) {
|
|
@@ -285,7 +433,8 @@ export class RectoDocument {
|
|
|
285
433
|
}
|
|
286
434
|
this.#closed = true;
|
|
287
435
|
this.#instance.openDocuments -= 1;
|
|
288
|
-
|
|
436
|
+
this.#instance.documents.open -= 1;
|
|
437
|
+
if (!this.#instance.dead && !this.#instance.abandoned) {
|
|
289
438
|
this.#doc.free();
|
|
290
439
|
}
|
|
291
440
|
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,29 @@ 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
|
+
* The most bytes one stream may decode to, and one image hold at a byte a component. Past it, that
|
|
20
|
+
* stream or image is left out and the page's results say `truncated`, so a few bytes that inflate to
|
|
21
|
+
* gigabytes can't take the instance's memory. Default 768 MiB (a 600 dpi gray scan of an A0 page is
|
|
22
|
+
* 558 MB); `Infinity` for none.
|
|
23
|
+
*/
|
|
24
|
+
maxDecodedBytes?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Files to fetch and verify while the engine is created, so that a missing one fails here and no later
|
|
27
|
+
* call waits for it. `recto-render.wasm` saves the first `render` that wait; all three parts check the
|
|
28
|
+
* whole engine at startup. The default is none, except where a thread can't read a file when it's needed
|
|
29
|
+
* (a page's main thread, a service worker): there it's all of them, about 9.6 MB. Name fewer to fetch less, and
|
|
30
|
+
* a call that needs a font or CMaps you left out fails with `missing-asset`. The parts are fetched there
|
|
31
|
+
* whatever you name, so it can always draw.
|
|
19
32
|
*/
|
|
20
|
-
preloadAssets?:
|
|
33
|
+
preloadAssets?: (AssetName | "cmaps.brotli" | "fonts.bin")[];
|
|
21
34
|
};
|
|
22
35
|
export type PdfOpenOptions = {
|
|
23
36
|
password?: string;
|
|
@@ -44,6 +57,8 @@ export interface PdfEngine {
|
|
|
44
57
|
openRanged(source: PdfRangeSource, options?: PdfRangedOpenOptions): Promise<PdfRangedDocument>;
|
|
45
58
|
stats(): PdfEngineStats;
|
|
46
59
|
}
|
|
60
|
+
/** An instance's own SHA-256, for an asset it needs: faster than JavaScript's in a cold browser. */
|
|
61
|
+
export declare function hashOn(instance: Pick<Instance, "dead" | "glue">): SyncHash;
|
|
47
62
|
export declare function createPdfEngine(options?: PdfEngineOptions): Promise<PdfEngine>;
|
|
48
63
|
/**
|
|
49
64
|
* Opens a PDF on this thread's shared engine:
|