@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.
Files changed (44) hide show
  1. package/dist/assets.d.ts +11 -6
  2. package/dist/assets.js +123 -42
  3. package/dist/document.d.ts +144 -2
  4. package/dist/document.js +163 -14
  5. package/dist/engine.d.ts +24 -9
  6. package/dist/engine.js +236 -49
  7. package/dist/errors.d.ts +4 -2
  8. package/dist/errors.js +4 -1
  9. package/dist/index.d.ts +12 -1
  10. package/dist/index.js +13 -0
  11. package/dist/pool-worker.d.ts +15 -0
  12. package/dist/pool-worker.js +65 -0
  13. package/dist/pool.d.ts +42 -0
  14. package/dist/pool.js +295 -0
  15. package/dist/ranged.d.ts +44 -9
  16. package/dist/ranged.js +571 -86
  17. package/package.json +2 -1
  18. package/wasm/FoxitDingbats.pfb +0 -0
  19. package/wasm/FoxitFixed.pfb +0 -0
  20. package/wasm/FoxitFixedBold.pfb +0 -0
  21. package/wasm/FoxitFixedBoldItalic.pfb +0 -0
  22. package/wasm/FoxitFixedItalic.pfb +0 -0
  23. package/wasm/FoxitSans.pfb +0 -0
  24. package/wasm/FoxitSansBold.pfb +0 -0
  25. package/wasm/FoxitSansBoldItalic.pfb +0 -0
  26. package/wasm/FoxitSansItalic.pfb +0 -0
  27. package/wasm/FoxitSerif.pfb +0 -0
  28. package/wasm/FoxitSerifBold.pfb +0 -0
  29. package/wasm/FoxitSerifBoldItalic.pfb +0 -0
  30. package/wasm/FoxitSerifItalic.pfb +0 -0
  31. package/wasm/FoxitSymbol.pfb +0 -0
  32. package/wasm/THIRD_PARTY_NOTICES +2 -158
  33. package/wasm/cmaps.deflate +0 -0
  34. package/wasm/pins.d.mts +1 -1
  35. package/wasm/pins.mjs +19 -3
  36. package/wasm/recto-more.wasm +0 -0
  37. package/wasm/recto-rare.wasm +0 -0
  38. package/wasm/recto-render.wasm +0 -0
  39. package/wasm/recto.gen.d.mts +2 -1
  40. package/wasm/recto.gen.mjs +213 -19
  41. package/wasm/recto.types.d.mts +83 -4
  42. package/wasm/recto.wasm +0 -0
  43. package/wasm/cmaps.brotli +0 -0
  44. 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
- if (this.#instance.failure) {
192
- throw this.#instance.failure;
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 maxHeight = options.maxHeight ?? DEFAULT_MAX_HEIGHT;
271
- if (!(options.width > 0 && options.width <= MAX_DIMENSION) ||
272
- !(maxHeight > 0)) {
273
- throw new RangeError(`render width must be in 1..${MAX_DIMENSION} and maxHeight above 0`);
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
- if (!this.#instance.dead) {
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
- /** Past this much linear memory, the instance is dropped when its last document closes, or
12
- * replaced at the next open if one is still open. Default 512 MiB. */
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
- * Where a thread can't read a file synchronously (a page's main thread, a service worker), the
16
- * assets are fetched while the engine is created: by default all of them, 5.6 MB. Name fewer to
17
- * fetch less, and a document that needs one you left out fails with `missing-asset`. Ignored
18
- * where assets can be read when needed.
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?: Exclude<AssetName, "recto.wasm">[];
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: