@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 +32 -52
- package/dist/document.d.ts +100 -4
- package/dist/document.js +72 -39
- package/dist/engine.d.ts +7 -0
- package/dist/engine.js +19 -13
- package/dist/index.d.ts +1 -1
- package/dist/pool-worker.d.ts +1 -0
- package/dist/pool-worker.js +2 -2
- package/dist/pool.d.ts +7 -1
- package/dist/pool.js +14 -6
- package/dist/ranged.d.ts +41 -11
- package/dist/ranged.js +579 -97
- package/dist/reading-order.d.ts +7 -0
- package/dist/reading-order.js +15 -6
- package/dist/text-items.js +1 -6
- package/package.json +1 -1
- package/wasm/pins.mjs +4 -4
- 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 +1 -1
- package/wasm/recto.gen.mjs +100 -12
- package/wasm/recto.types.d.mts +39 -5
- package/wasm/recto.wasm +0 -0
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
|
-
|
|
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 (
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
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
|
}
|
package/dist/document.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
package/dist/pool-worker.d.ts
CHANGED
package/dist/pool-worker.js
CHANGED
|
@@ -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>;
|