@2018kguo/recto 0.3.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.
@@ -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;
@@ -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() {
@@ -306,6 +362,12 @@ export class RectoDocument {
306
362
  this.#check(index);
307
363
  return this.#call(() => toAnnotations(this.#doc.annotations(index)));
308
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
+ }
309
371
  scan(index) {
310
372
  this.#check(index);
311
373
  this.#needsRender();
@@ -324,30 +386,17 @@ export class RectoDocument {
324
386
  }
325
387
  render(index, options) {
326
388
  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
- }
389
+ const args = renderArgs(options);
332
390
  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
- })));
391
+ return this.#call(() => consume(this.#doc.render(index, ...args), (page) => bitmapOf(page, options.region !== undefined)));
339
392
  }
340
393
  async renderAsync(index, options) {
341
394
  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
- }
395
+ const args = renderArgs(options);
347
396
  this.#needsRender();
348
397
  const instance = this.#instance;
349
398
  const sliceMs = options.sliceMs ?? DEFAULT_SLICE_MS;
350
- const task = this.#call(() => this.#doc.renderTask(index, options.width, maxHeight));
399
+ const task = this.#call(() => this.#doc.renderTask(index, ...args));
351
400
  const stopIfAsked = () => {
352
401
  if (options.signal?.aborted) {
353
402
  throw new PdfError("aborted", "the signal was aborted", options.signal.reason);
@@ -370,12 +419,7 @@ export class RectoDocument {
370
419
  }
371
420
  await nextTask();
372
421
  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
- })));
422
+ return this.#call(() => consume(task.finish(), (page) => bitmapOf(page, options.region !== undefined)));
379
423
  }
380
424
  finally {
381
425
  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;
@@ -350,7 +355,11 @@ async function createEngine(options) {
350
355
  ...(canRead ? names.filter(isPart) : PARTS).map((part) => compile(part, base)),
351
356
  preloadAssets(base, files),
352
357
  ]);
353
- return new RectoEngine(instantiate(0, base, module), base, options.recycleAboveBytes);
358
+ const maxDecodedBytes = options.maxDecodedBytes;
359
+ if (maxDecodedBytes !== undefined && !(typeof maxDecodedBytes === "number" && maxDecodedBytes >= 0)) {
360
+ throw new RangeError("maxDecodedBytes must be a number of bytes, 0 or more");
361
+ }
362
+ return new RectoEngine(instantiate(0, base, module, maxDecodedBytes), base, options.recycleAboveBytes, maxDecodedBytes);
354
363
  }
355
364
  let shared = null;
356
365
  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>;
package/dist/pool.js CHANGED
@@ -39,6 +39,7 @@ class Pool {
39
39
  #limit;
40
40
  #ceiling;
41
41
  #assetBase;
42
+ #maxDecodedBytes;
42
43
  #threads = [];
43
44
  /** Started and not yet ready. */
44
45
  #startingThreads = new Set();
@@ -46,10 +47,11 @@ class Pool {
46
47
  #nextFile = 0;
47
48
  #nextId = 0;
48
49
  #closed = false;
49
- constructor(limit, assetBase) {
50
+ constructor(limit, assetBase, maxDecodedBytes) {
50
51
  this.#limit = limit;
51
52
  this.#ceiling = limit;
52
53
  this.#assetBase = assetBase;
54
+ this.#maxDecodedBytes = maxDecodedBytes;
53
55
  }
54
56
  #spawn() {
55
57
  const thread = { worker: null, running: null };
@@ -147,7 +149,7 @@ class Pool {
147
149
  };
148
150
  thread.worker = worker;
149
151
  }
150
- const start = { assetBase: this.#assetBase };
152
+ const start = { assetBase: this.#assetBase, maxDecodedBytes: this.#maxDecodedBytes };
151
153
  thread.worker.postMessage(start);
152
154
  }
153
155
  catch (error) {
@@ -240,13 +242,15 @@ class Pool {
240
242
  text: (index) => call("text", index),
241
243
  objects: (index) => call("objects", index),
242
244
  annotations: (index) => call("annotations", index),
245
+ links: (index) => call("links", index),
243
246
  outline: () => call("outline"),
244
- render: (index, renderOptions) => {
247
+ render: ((index, renderOptions) => {
245
248
  if (renderOptions?.signal) {
246
249
  return Promise.reject(new TypeError("a pool's render takes no signal: a call that's running can't be stopped. pool.close() ends all of them."));
247
250
  }
248
- return call("render", index, { width: renderOptions?.width, maxHeight: renderOptions?.maxHeight });
249
- },
251
+ const { width, maxHeight, region } = renderOptions ?? {};
252
+ return call("render", index, region === undefined ? { width, maxHeight } : { width, maxHeight, region });
253
+ }),
250
254
  close,
251
255
  [Symbol.asyncDispose]: close,
252
256
  };
@@ -283,5 +287,9 @@ export function startPool(options) {
283
287
  const assetBase = options.assetBase !== undefined && typeof location !== "undefined"
284
288
  ? new URL(options.assetBase, location.href).href
285
289
  : options.assetBase;
286
- return new Pool(threads, assetBase);
290
+ const maxDecodedBytes = options.maxDecodedBytes;
291
+ if (maxDecodedBytes !== undefined && !(typeof maxDecodedBytes === "number" && maxDecodedBytes >= 0)) {
292
+ throw new RangeError("maxDecodedBytes must be a number of bytes, 0 or more");
293
+ }
294
+ return new Pool(threads, assetBase, maxDecodedBytes);
287
295
  }
package/dist/ranged.d.ts CHANGED
@@ -43,19 +43,36 @@ export type PdfRangedStats = {
43
43
  export type PdfRangedDocument = PdfDocument & {
44
44
  stats(): PdfRangedStats;
45
45
  };
46
+ /**
47
+ * Bytes `start..end`. With `yieldTail`, read no further than that many bytes before the file's
48
+ * end, where a suffix asked for beside it starts, so no byte comes twice.
49
+ */
46
50
  type Range = {
47
51
  start: number;
48
52
  end: number;
53
+ yieldTail?: number;
49
54
  };
55
+ /** The file's last `suffix` bytes (`bytes=-n`), asked for before its size is known. */
56
+ type Suffix = {
57
+ suffix: number;
58
+ };
59
+ type Request = Range | Suffix;
60
+ /** `received`: the bytes read off the response, `bytes` and any read past it. */
50
61
  type Answer = {
51
62
  start: number;
52
63
  bytes: Uint8Array;
53
64
  length: number;
54
65
  etag: string | null;
55
66
  status: number;
67
+ received: number;
56
68
  };
57
- /** Fetches `ranges`; `limit` is how many bytes may still arrive. */
58
- type Fetch = (ranges: Range[], timeoutMs: number, limit: number) => Answer[];
69
+ /**
70
+ * Fetches `requests` in parallel; `limit` is how many bytes may still arrive. A range that
71
+ * fails throws. A suffix that isn't answered as one (a server that ignores or refuses it)
72
+ * gives null, and isn't counted: the caller asks for the tail by its offset instead.
73
+ */
74
+ type Fetch = (requests: Request[], timeoutMs: number, limit: number) => (Answer | null)[];
75
+ type FetchAsync = (requests: Request[], timeoutMs: number, limit: number) => Promise<(Answer | null)[]>;
59
76
  /** Fetched chunks of one file and the policy for fetching more. */
60
77
  declare class RangeLoader {
61
78
  #private;
@@ -73,23 +90,31 @@ declare class RangeLoader {
73
90
  stats(): PdfRangedStats;
74
91
  /** Fetches chunks `firstChunk..endChunk`, keeping only those not held yet. */
75
92
  fetchChunks(firstChunk: number, endChunk: number): void;
93
+ /**
94
+ * The first round: the head and, in parallel, the tail as a suffix, then the last
95
+ * cross-reference section if the tail didn't hold it. A file the head covers needs no more;
96
+ * a server that doesn't answer the suffix gets the tail by offset in a round of its own.
97
+ */
98
+ begin(fetchAsync: FetchAsync | null): Promise<void>;
76
99
  /**
77
100
  * The `startxref` offset in the fetched last chunk, if it names one. Null for a linearized
78
101
  * file, whose trailer points at the first-page section near the head.
79
102
  */
80
103
  startxref(): number | null;
81
104
  /**
82
- * Copies [offset, offset + out.length) into `out`, fetching what is missing first. On a
83
- * failed fetch it records the failure and fails the read; the call that made it then
84
- * throws the failure.
105
+ * Copies [offset, offset + out.length) into `out`, fetching what is missing first, and if it
106
+ * fetches, `ahead` bytes past it too, as far as the budget allows: the engine's guess at what
107
+ * it reads next, which then costs no round trip of its own. On a failed fetch it records the
108
+ * failure and fails the read; the call that made it then throws the failure.
85
109
  */
86
- read(offset: number, out: Uint8Array): boolean;
110
+ read(offset: number, out: Uint8Array, ahead?: number): boolean;
87
111
  }
88
112
  /**
89
- * Starts a ranged read of `source`: the head, then (for a larger file) the tail and its last
90
- * cross-reference section. Blocks while it fetches.
113
+ * Starts a ranged read of `source`: the head and tail, then the last cross-reference section
114
+ * if the tail didn't hold it. Blocks while it fetches, except in a browser, where these
115
+ * rounds are `fetch` calls.
91
116
  */
92
- export declare function startRangeLoader(source: string | Uint8Array, options?: PdfRangedOpenOptions): RangeLoader;
117
+ export declare function startRangeLoader(source: string | Uint8Array, options?: PdfRangedOpenOptions): Promise<RangeLoader>;
93
118
  /**
94
119
  * The caller's source in a form that stays valid across an await: bytes copied, a URL as an
95
120
  * absolute http(s) string. Runs before anything asynchronous.