@2018kguo/recto 0.3.0 → 0.5.1

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.js CHANGED
@@ -124,6 +124,22 @@ function loadFailure(name, url, reason, cause) {
124
124
  "somewhere it serves and pass that directory as `assetBase` (with Vite's dev server, add " +
125
125
  'optimizeDeps: { exclude: ["@2018kguo/recto"] } instead; a server bundle should keep the package external).', cause);
126
126
  }
127
+ /**
128
+ * `read` of an asset's URL, sync or async, with any failure but a PdfError reported as that asset's
129
+ * `missing-asset`. Resolving the URL is inside it: under a blob: worker `import.meta.url` is no base.
130
+ */
131
+ function located(name, base, read) {
132
+ let url = null;
133
+ const failure = (error) => error instanceof PdfError ? error : loadFailure(name, url, error instanceof Error ? error.message : String(error), error);
134
+ try {
135
+ url = assetUrl(name, base);
136
+ const result = read(url);
137
+ return (result instanceof Promise ? result.catch((error) => Promise.reject(failure(error))) : result);
138
+ }
139
+ catch (error) {
140
+ throw failure(error);
141
+ }
142
+ }
127
143
  function notThisBuild(name, url, bytes) {
128
144
  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
145
  }
@@ -172,19 +188,7 @@ async function fetchBytes(name, url) {
172
188
  }
173
189
  /** The wasm, verified and then compiled. */
174
190
  export async function compileWasm(name, base) {
175
- let url = null;
176
- let bytes;
177
- try {
178
- // Inside the try: under a blob: worker `import.meta.url` is no base for a URL.
179
- url = assetUrl(name, base);
180
- bytes = (nodeFs() ? checked(name, url, readFromDisk(url)) : await fetchBytes(name, url));
181
- }
182
- catch (error) {
183
- if (error instanceof PdfError) {
184
- throw error;
185
- }
186
- throw loadFailure(name, url, error instanceof Error ? error.message : String(error), error);
187
- }
191
+ const bytes = await located(name, base, async (url) => (nodeFs() ? checked(name, url, readFromDisk(url)) : await fetchBytes(name, url)));
188
192
  return await compiling(name, WebAssembly.compile(bytes));
189
193
  }
190
194
  function compiling(name, module) {
@@ -196,55 +200,31 @@ function compiling(name, module) {
196
200
  const loaded = new Map();
197
201
  /** Fetches and verifies assets ahead of time; a thread that can't read them when needed has to. */
198
202
  export async function preloadAssets(base, names) {
199
- await Promise.all(names.map(async (name) => {
200
- let url = null;
201
- let bytes;
202
- try {
203
- url = assetUrl(name, base);
204
- if (loaded.has(url.href)) {
205
- return;
206
- }
207
- bytes = nodeFs() ? checked(name, url, readFromDisk(url)) : await fetchBytes(name, url);
208
- }
209
- catch (error) {
210
- if (error instanceof PdfError) {
211
- throw error;
212
- }
213
- throw loadFailure(name, url, error instanceof Error ? error.message : String(error), error);
203
+ await Promise.all(names.map((name) => located(name, base, async (url) => {
204
+ if (!loaded.has(url.href)) {
205
+ loaded.set(url.href, nodeFs() ? checked(name, url, readFromDisk(url)) : await fetchBytes(name, url));
214
206
  }
215
- loaded.set(url.href, bytes);
216
- }));
207
+ })));
217
208
  }
218
209
  /** An asset, read synchronously. Throws a PdfError rather than returning nothing. */
219
210
  export function loadAssetSync(name, base, hash) {
220
- let url = null;
221
- try {
222
- url = assetUrl(name, base);
223
- const cached = loaded.get(url.href);
224
- if (cached) {
225
- return cached;
211
+ return located(name, base, (url) => {
212
+ let bytes = loaded.get(url.href);
213
+ if (!bytes) {
214
+ bytes = readCheckedSync(name, base, hash);
215
+ loaded.set(url.href, bytes);
226
216
  }
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;
217
+ return bytes;
218
+ });
234
219
  }
235
220
  /** A file, read now and hashed before it's returned, and not kept: a part is compiled once. */
236
221
  export function readCheckedSync(name, base, hash) {
237
- let url = null;
238
- let bytes;
239
- try {
240
- url = assetUrl(name, base);
222
+ const [url, bytes] = located(name, base, (url) => {
241
223
  if (!canReadSync()) {
242
224
  throw new Error("this thread can only use assets fetched up front, and it was left out of preloadAssets");
243
225
  }
244
- bytes = nodeFs() ? readFromDisk(url) : readSync(url);
245
- }
246
- catch (error) {
247
- throw loadFailure(name, url, error instanceof Error ? error.message : String(error), error);
248
- }
226
+ return [url, nodeFs() ? readFromDisk(url) : readSync(url)];
227
+ });
228
+ // Outside: a trap in the instance's hash is a crash, not a missing file.
249
229
  return checked(name, url, bytes, hash);
250
230
  }
@@ -170,6 +170,23 @@ export type PdfRenderOptions = {
170
170
  width: number;
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
+ /**
174
+ * Draws only this part of the page: x, y, width, height in points in the viewer frame, as text and
175
+ * annotation boxes are (top-left origin, `/Rotate` and the crop box applied). The bitmap holds the
176
+ * pixels of the whole page's bitmap at the scale `width` and `maxHeight` give that this covers,
177
+ * grown out to whole pixels and cut to the page, and says where they are. With a region the limits
178
+ * are on the region's bitmap, not the page's, and `width` may be up to 262,144.
179
+ */
180
+ region?: PdfBox;
181
+ };
182
+ /** A render of a region: where its pixels are in the whole page's bitmap at that scale. */
183
+ export type PdfRegionBitmap = PdfBitmap & {
184
+ /** The bitmap's top-left pixel, in the whole page's bitmap. */
185
+ x: number;
186
+ y: number;
187
+ /** The whole page's bitmap's size at this scale: what `render` without a region and its limits would give. */
188
+ pageWidth: number;
189
+ pageHeight: number;
173
190
  };
174
191
  export type PdfRenderAsyncOptions = PdfRenderOptions & {
175
192
  /** Stops the render at the next pause: `aborted`, with the document still usable. */
@@ -188,6 +205,67 @@ export type PdfBitmap = {
188
205
  */
189
206
  truncated: boolean;
190
207
  };
208
+ /** Where a link in this file goes: a page, and the place on it in that page's viewer frame. */
209
+ export type PdfDestination = {
210
+ /** The page's index, or -1 if it goes nowhere in this file. */
211
+ page: number;
212
+ /**
213
+ * How to show the page, as the file names it: "XYZ", "Fit", "FitH", "FitV", "FitR", "FitB", "FitBH",
214
+ * "FitBV"; null if it names none.
215
+ */
216
+ view: string | null;
217
+ /**
218
+ * The point to bring to the window's top-left, in points in the target page's viewer frame
219
+ * (top-left origin, its `/Rotate` and crop box applied). null for a coordinate the destination leaves
220
+ * as it was; under a quarter turn, the page's `left` is the viewer's `y`.
221
+ */
222
+ x: number | null;
223
+ y: number | null;
224
+ /** "XYZ"'s zoom, 1 for 100%; null to leave it as it was. */
225
+ zoom: number | null;
226
+ /** "FitR"'s rectangle, in the target page's viewer frame; null for any other view. */
227
+ box: PdfBox | null;
228
+ };
229
+ /** What a link does. Another file or a program is reported, never followed. */
230
+ export type PdfLinkTarget = {
231
+ kind: "uri";
232
+ uri: string;
233
+ }
234
+ /** A `GoTo` action or the link's `/Dest`, named destinations resolved. */
235
+ | {
236
+ kind: "page";
237
+ destination: PdfDestination;
238
+ }
239
+ /** `GoToR`: a destination in another file, by name or by page number there. */
240
+ | {
241
+ kind: "remote";
242
+ file: string;
243
+ name: string | null;
244
+ page: number | null;
245
+ }
246
+ /** `Launch`: a file or program. */
247
+ | {
248
+ kind: "launch";
249
+ file: string;
250
+ }
251
+ /** Any other action by its `/S` ("JavaScript", "SubmitForm", ...); a `Named` action's `/N` ("NextPage", ...) is `name`. */
252
+ | {
253
+ kind: "action";
254
+ action: string;
255
+ name: string | null;
256
+ }
257
+ /** No action and no destination. */
258
+ | {
259
+ kind: "none";
260
+ };
261
+ /** A `/Link` annotation and where it goes. */
262
+ export type PdfLink = {
263
+ /** Its index in what `annotations` returns for the page. */
264
+ annotation: number;
265
+ /** Its `/Rect` in the viewer frame, as its annotation's `box`. */
266
+ box: PdfBox;
267
+ target: PdfLinkTarget;
268
+ };
191
269
  export declare const ANNOT_SUBTYPE: {
192
270
  readonly text: 1;
193
271
  readonly link: 2;
@@ -195,10 +273,6 @@ export declare const ANNOT_SUBTYPE: {
195
273
  readonly stamp: 13;
196
274
  readonly widget: 20;
197
275
  };
198
- /**
199
- * An open PDF. Every call is synchronous CPU work on this thread. Closing is idempotent, and
200
- * every other method throws a PdfError with code "closed" afterwards.
201
- */
202
276
  /** A bookmark. `page` is -1 if it goes nowhere in this file; `url` is set if it links out. */
203
277
  export type PdfOutlineItem = {
204
278
  title: string;
@@ -206,6 +280,10 @@ export type PdfOutlineItem = {
206
280
  url: string | null;
207
281
  children: PdfOutlineItem[];
208
282
  };
283
+ /**
284
+ * An open PDF. Every call is synchronous CPU work on this thread. Closing is idempotent, and
285
+ * every other method throws a PdfError with code "closed" afterwards.
286
+ */
209
287
  export interface PdfDocument {
210
288
  readonly pageCount: number;
211
289
  /** The document's bookmarks; empty if it has none. */
@@ -237,11 +315,22 @@ export interface PdfDocument {
237
315
  };
238
316
  objects(index: number): PdfPageObjects;
239
317
  annotations(index: number): PdfAnnotation[];
318
+ /**
319
+ * The page's links, in the order `annotations` lists them, and where each goes. Loads drawing's
320
+ * part of the wasm, as `objects` does.
321
+ */
322
+ links(index: number): PdfLink[];
323
+ render(index: number, options: PdfRenderOptions & {
324
+ region: PdfBox;
325
+ }): PdfRegionBitmap;
240
326
  render(index: number, options: PdfRenderOptions): PdfBitmap;
241
327
  /**
242
328
  * The same pixels as `render`, giving the thread back between slices, and stoppable with a
243
329
  * signal. The last step, rasterizing, is one piece: about a third of a heavy page's time.
244
330
  */
331
+ renderAsync(index: number, options: PdfRenderAsyncOptions & {
332
+ region: PdfBox;
333
+ }): Promise<PdfRegionBitmap>;
245
334
  renderAsync(index: number, options: PdfRenderAsyncOptions): Promise<PdfBitmap>;
246
335
  close(): void;
247
336
  [Symbol.dispose](): void;
@@ -291,6 +380,7 @@ export declare class RectoDocument implements PdfDocument {
291
380
  };
292
381
  outline(): PdfOutlineItem[];
293
382
  annotations(index: number): PdfAnnotation[];
383
+ links(index: number): PdfLink[];
294
384
  scan(index: number): {
295
385
  text: PdfPageText;
296
386
  objects: PdfPageObjects;
@@ -300,7 +390,13 @@ export declare class RectoDocument implements PdfDocument {
300
390
  text: PdfPageText;
301
391
  annotations: PdfAnnotation[];
302
392
  };
393
+ render(index: number, options: PdfRenderOptions & {
394
+ region: PdfBox;
395
+ }): PdfRegionBitmap;
303
396
  render(index: number, options: PdfRenderOptions): PdfBitmap;
397
+ renderAsync(index: number, options: PdfRenderAsyncOptions & {
398
+ region: PdfBox;
399
+ }): Promise<PdfRegionBitmap>;
304
400
  renderAsync(index: number, options: PdfRenderAsyncOptions): Promise<PdfBitmap>;
305
401
  close(): void;
306
402
  [Symbol.dispose](): void;
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,6 +198,31 @@ 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
+ }
170
226
  const DEFAULT_SLICE_MS = 8;
171
227
  /** Back to the event loop, without the 4 ms a nested timer waits in a browser. */
172
228
  function nextTask() {
@@ -197,16 +253,14 @@ export class RectoDocument {
197
253
  instance.openDocuments += 1;
198
254
  instance.documents.open += 1;
199
255
  }
256
+ /** Before every call: open, the page there (null for a call about the whole document), and the instance able to run it. */
200
257
  #check(index) {
201
258
  if (this.#closed) {
202
259
  throw new PdfError("closed", "this document is closed");
203
260
  }
204
- if (!(Number.isInteger(index) && index >= 0 && index < this.pageCount)) {
261
+ if (index !== null && !(Number.isInteger(index) && index >= 0 && index < this.pageCount)) {
205
262
  throw new RangeError(`page ${index} is out of range (0..${this.pageCount - 1})`);
206
263
  }
207
- this.#checkInstance();
208
- }
209
- #checkInstance() {
210
264
  if (this.#instance.abandoned) {
211
265
  this.#moveOffAbandoned();
212
266
  }
@@ -271,25 +325,16 @@ export class RectoDocument {
271
325
  return this.#call(() => toObjects(this.#doc.objects(index)));
272
326
  }
273
327
  pageLabels() {
274
- if (this.#closed) {
275
- throw new PdfError("closed", "this document is closed");
276
- }
277
- this.#checkInstance();
328
+ this.#check(null);
278
329
  return this.#call(() => this.#doc.pageLabels());
279
330
  }
280
331
  structureTruncated() {
281
- if (this.#closed) {
282
- throw new PdfError("closed", "this document is closed");
283
- }
284
- this.#checkInstance();
332
+ this.#check(null);
285
333
  const [pages, pageLabels, outline] = this.#call(() => this.#doc.structureTruncated());
286
334
  return { pages: pages === 1, pageLabels: pageLabels === 1, outline: outline === 1 };
287
335
  }
288
336
  outline() {
289
- if (this.#closed) {
290
- throw new PdfError("closed", "this document is closed");
291
- }
292
- this.#checkInstance();
337
+ this.#check(null);
293
338
  return this.#call(() => {
294
339
  const top = [];
295
340
  // The open item at each level; an item's parent is the last one a level up.
@@ -306,6 +351,12 @@ export class RectoDocument {
306
351
  this.#check(index);
307
352
  return this.#call(() => toAnnotations(this.#doc.annotations(index)));
308
353
  }
354
+ links(index) {
355
+ this.#check(index);
356
+ // A viewer's call: its code is in drawing's part, so text alone downloads none of it.
357
+ this.#needsRender();
358
+ return this.#call(() => this.#doc.links(index).map(toLink));
359
+ }
309
360
  scan(index) {
310
361
  this.#check(index);
311
362
  this.#needsRender();
@@ -324,30 +375,17 @@ export class RectoDocument {
324
375
  }
325
376
  render(index, options) {
326
377
  this.#check(index);
327
- const maxHeight = options.maxHeight ?? DEFAULT_MAX_HEIGHT;
328
- if (!(options.width > 0 && options.width <= MAX_DIMENSION) ||
329
- !(maxHeight > 0)) {
330
- throw new RangeError(`render width must be in 1..${MAX_DIMENSION} and maxHeight above 0`);
331
- }
378
+ const args = renderArgs(options);
332
379
  this.#needsRender();
333
- return this.#call(() => consume(this.#doc.render(index, options.width, maxHeight), (page) => ({
334
- width: page.width,
335
- height: page.height,
336
- truncated: page.truncated,
337
- rgba: page.takeRgba(),
338
- })));
380
+ return this.#call(() => consume(this.#doc.render(index, ...args), (page) => bitmapOf(page, options.region !== undefined)));
339
381
  }
340
382
  async renderAsync(index, options) {
341
383
  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
- }
384
+ const args = renderArgs(options);
347
385
  this.#needsRender();
348
386
  const instance = this.#instance;
349
387
  const sliceMs = options.sliceMs ?? DEFAULT_SLICE_MS;
350
- const task = this.#call(() => this.#doc.renderTask(index, options.width, maxHeight));
388
+ const task = this.#call(() => this.#doc.renderTask(index, ...args));
351
389
  const stopIfAsked = () => {
352
390
  if (options.signal?.aborted) {
353
391
  throw new PdfError("aborted", "the signal was aborted", options.signal.reason);
@@ -370,12 +408,7 @@ export class RectoDocument {
370
408
  }
371
409
  await nextTask();
372
410
  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
- })));
411
+ return this.#call(() => consume(task.finish(), (page) => bitmapOf(page, options.region !== undefined)));
379
412
  }
380
413
  finally {
381
414
  if (!instance.dead && !instance.abandoned) {
package/dist/engine.d.ts CHANGED
@@ -15,6 +15,13 @@ export type PdfEngineOptions = {
15
15
  * with a document still open, the next open gets a new one.
16
16
  */
17
17
  recycleAboveBytes?: number;
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;
18
25
  /**
19
26
  * Files to fetch and verify while the engine is created, so that a missing one fails here and no later
20
27
  * call waits for it. `recto-render.wasm` saves the first `render` that wait; all three parts check the
package/dist/engine.js CHANGED
@@ -60,7 +60,7 @@ export function hashOn(instance) {
60
60
  };
61
61
  }
62
62
  const hashing = new WeakSet();
63
- function instantiate(generation, base, module, documents = { open: 0 }) {
63
+ function instantiate(generation, base, module, maxDecodedBytes, documents = { open: 0 }) {
64
64
  const parts = new Set();
65
65
  // A function of a part that isn't loaded yet: load it, then make the call this was meant to be. A
66
66
  // failure unwinds the engine's frames with its state half-done, so no call can follow on this
@@ -137,6 +137,9 @@ function instantiate(generation, base, module, documents = { open: 0 }) {
137
137
  return;
138
138
  }
139
139
  });
140
+ if (maxDecodedBytes !== undefined) {
141
+ glue.setMaxDecodedBytes(maxDecodedBytes);
142
+ }
140
143
  return instance;
141
144
  }
142
145
  function bytesOf(input) {
@@ -160,6 +163,7 @@ class RectoEngine {
160
163
  #starting = null;
161
164
  #base;
162
165
  #recycleAboveBytes;
166
+ #maxDecodedBytes;
163
167
  /** Past this, an instance with a document open takes no other. */
164
168
  #idleAboveBytes;
165
169
  /** Whether an instance that grew goes once nothing has been open on it for IDLE_MS: the default. */
@@ -167,7 +171,8 @@ class RectoEngine {
167
171
  #idle;
168
172
  /** Across every instance: a document that moved, or stayed on an old one, still counts. */
169
173
  #documents;
170
- constructor(instance, base, recycleAboveBytes) {
174
+ constructor(instance, base, recycleAboveBytes, maxDecodedBytes) {
175
+ this.#maxDecodedBytes = maxDecodedBytes;
171
176
  this.#instance = instance;
172
177
  this.#generation = instance.generation;
173
178
  this.#base = base;
@@ -218,7 +223,7 @@ class RectoEngine {
218
223
  return current;
219
224
  }
220
225
  this.#starting ??= compile("recto.wasm", this.#base)
221
- .then((module) => instantiate(this.#generation + 1, this.#base, module, this.#documents))
226
+ .then((module) => instantiate(this.#generation + 1, this.#base, module, this.#maxDecodedBytes, this.#documents))
222
227
  .finally(() => {
223
228
  this.#starting = null;
224
229
  });
@@ -238,7 +243,7 @@ class RectoEngine {
238
243
  return current;
239
244
  }
240
245
  // Compiled already: it's what the abandoned instance ran.
241
- const instance = instantiate(this.#generation + 1, this.#base, compiled.get(moduleKey("recto.wasm", this.#base)), this.#documents);
246
+ const instance = instantiate(this.#generation + 1, this.#base, compiled.get(moduleKey("recto.wasm", this.#base)), this.#maxDecodedBytes, this.#documents);
242
247
  this.#adopt(instance);
243
248
  return instance;
244
249
  }
@@ -290,10 +295,10 @@ class RectoEngine {
290
295
  }
291
296
  const owned = ownedSource(source);
292
297
  const instance = await this.#live();
293
- const loader = startRangeLoader(owned, options);
294
- const read = (offset, length) => {
298
+ const loader = await startRangeLoader(owned, options);
299
+ const read = (offset, length, ahead) => {
295
300
  const out = new Uint8Array(length);
296
- return loader.read(offset, out) ? out : undefined;
301
+ return loader.read(offset, out, ahead) ? out : undefined;
297
302
  };
298
303
  const password = options.password ?? null;
299
304
  let doc;
@@ -308,14 +313,11 @@ class RectoEngine {
308
313
  }, () => loader.failure);
309
314
  }
310
315
  catch (error) {
311
- if (loader.failure && !(error instanceof WebAssembly.RuntimeError)) {
312
- throw loader.failure;
313
- }
314
- throw engineFailure(instance, error);
316
+ throw loader.explain(loader.failure && !(error instanceof WebAssembly.RuntimeError) ? loader.failure : engineFailure(instance, error));
315
317
  }
316
318
  if (loader.failure) {
317
319
  doc.close();
318
- throw loader.failure;
320
+ throw loader.explain(loader.failure);
319
321
  }
320
322
  return Object.assign(doc, { stats: () => loader.stats() });
321
323
  }
@@ -350,7 +352,11 @@ async function createEngine(options) {
350
352
  ...(canRead ? names.filter(isPart) : PARTS).map((part) => compile(part, base)),
351
353
  preloadAssets(base, files),
352
354
  ]);
353
- return new RectoEngine(instantiate(0, base, module), base, options.recycleAboveBytes);
355
+ const maxDecodedBytes = options.maxDecodedBytes;
356
+ if (maxDecodedBytes !== undefined && !(typeof maxDecodedBytes === "number" && maxDecodedBytes >= 0)) {
357
+ throw new RangeError("maxDecodedBytes must be a number of bytes, 0 or more");
358
+ }
359
+ return new RectoEngine(instantiate(0, base, module, maxDecodedBytes), base, options.recycleAboveBytes, maxDecodedBytes);
354
360
  }
355
361
  let shared = null;
356
362
  let sharedReady = null;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { ANNOT_SUBTYPE, boxAt, CHAR_GENERATED, CHAR_UNMAPPED, charsText, PATH_FILL, PATH_STROKE, type PdfAnnotation, type PdfBitmap, type PdfBox, type PdfDocument, type PdfFont, type PdfOutlineItem, type PdfPageInfo, type PdfPageObjects, type PdfPageText, type PdfPathSegments, type PdfRenderAsyncOptions, type PdfRenderOptions, type PdfTextRuns, type PdfWords, SEGMENT_BEZIER, SEGMENT_CLOSE, SEGMENT_LINE, SEGMENT_MOVE, SEGMENT_RECT, } from "./document.ts";
1
+ export { ANNOT_SUBTYPE, boxAt, CHAR_GENERATED, CHAR_UNMAPPED, charsText, PATH_FILL, PATH_STROKE, type PdfAnnotation, type PdfBitmap, type PdfBox, type PdfDestination, type PdfDocument, type PdfFont, type PdfLink, type PdfLinkTarget, type PdfOutlineItem, type PdfPageInfo, type PdfPageObjects, type PdfPageText, type PdfPathSegments, type PdfRegionBitmap, type PdfRenderAsyncOptions, type PdfRenderOptions, type PdfTextRuns, type PdfWords, SEGMENT_BEZIER, SEGMENT_CLOSE, SEGMENT_LINE, SEGMENT_MOVE, SEGMENT_RECT, } from "./document.ts";
2
2
  export { createPdfEngine, openPdf, type PdfEngine, type PdfEngineOptions, type PdfEngineStats, type PdfOpenOptions, } from "./engine.ts";
3
3
  export { PdfError, type PdfErrorCode } from "./errors.ts";
4
4
  export type { PdfPool, PdfPoolDocument, PdfPoolOpenOptions, PdfPoolOptions } from "./pool.ts";
@@ -11,4 +11,5 @@ export type PoolRequest = {
11
11
  };
12
12
  export type PoolStart = {
13
13
  assetBase: string | undefined;
14
+ maxDecodedBytes: number | undefined;
14
15
  };
@@ -32,9 +32,9 @@ function failureOf(error) {
32
32
  }
33
33
  async function handle(message) {
34
34
  if (!("id" in message)) {
35
- const { assetBase } = message;
35
+ const { assetBase, maxDecodedBytes } = message;
36
36
  try {
37
- engine = await createPdfEngine(assetBase ? { assetBase } : {});
37
+ engine = await createPdfEngine({ ...(assetBase ? { assetBase } : {}), ...(maxDecodedBytes === undefined ? {} : { maxDecodedBytes }) });
38
38
  port.postMessage({ ready: true }, []);
39
39
  }
40
40
  catch (error) {
package/dist/pool.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- import type { PdfAnnotation, PdfBitmap, PdfOutlineItem, PdfPageInfo, PdfPageObjects, PdfPageText, PdfRenderOptions } from "./document.ts";
1
+ import type { PdfAnnotation, PdfBitmap, PdfBox, PdfLink, PdfOutlineItem, PdfPageInfo, PdfPageObjects, PdfPageText, PdfRegionBitmap, PdfRenderOptions } from "./document.ts";
2
2
  export type PdfPoolOptions = {
3
3
  /** At most this many threads, up to 64. Default: one less than the machine's cores, up to 8. */
4
4
  threads?: number;
5
5
  /** As for `createPdfEngine`. A string: it crosses to the threads. */
6
6
  assetBase?: string;
7
+ /** As for `createPdfEngine`, on every thread. */
8
+ maxDecodedBytes?: number;
7
9
  };
8
10
  export type PdfPoolOpenOptions = {
9
11
  password?: string;
@@ -15,8 +17,12 @@ export interface PdfPoolDocument {
15
17
  text(index: number): Promise<PdfPageText>;
16
18
  objects(index: number): Promise<PdfPageObjects>;
17
19
  annotations(index: number): Promise<PdfAnnotation[]>;
20
+ links(index: number): Promise<PdfLink[]>;
18
21
  outline(): Promise<PdfOutlineItem[]>;
19
22
  /** Takes no `signal`: a call that's running can't be stopped. */
23
+ render(index: number, options: PdfRenderOptions & {
24
+ region: PdfBox;
25
+ }): Promise<PdfRegionBitmap>;
20
26
  render(index: number, options: PdfRenderOptions): Promise<PdfBitmap>;
21
27
  /** Frees it on every thread, and fails its calls still waiting with `closed`. */
22
28
  close(): Promise<void>;