@2018kguo/recto 0.0.0-stage → 0.3.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 (53) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +25 -2
  3. package/dist/assets.d.ts +19 -0
  4. package/dist/assets.js +250 -0
  5. package/dist/document.d.ts +307 -0
  6. package/dist/document.js +405 -0
  7. package/dist/engine.d.ts +62 -0
  8. package/dist/engine.js +383 -0
  9. package/dist/errors.d.ts +36 -0
  10. package/dist/errors.js +14 -0
  11. package/dist/index.d.ts +18 -0
  12. package/dist/index.js +23 -0
  13. package/dist/page-measures.d.ts +34 -0
  14. package/dist/page-measures.js +166 -0
  15. package/dist/pool-worker.d.ts +14 -0
  16. package/dist/pool-worker.js +65 -0
  17. package/dist/pool.d.ts +36 -0
  18. package/dist/pool.js +287 -0
  19. package/dist/ranged.d.ts +98 -0
  20. package/dist/ranged.js +497 -0
  21. package/dist/reading-order.d.ts +14 -0
  22. package/dist/reading-order.js +173 -0
  23. package/dist/sha256.d.ts +1 -0
  24. package/dist/sha256.js +78 -0
  25. package/dist/text-items.d.ts +49 -0
  26. package/dist/text-items.js +272 -0
  27. package/package.json +48 -4
  28. package/wasm/FoxitDingbats.pfb +0 -0
  29. package/wasm/FoxitFixed.pfb +0 -0
  30. package/wasm/FoxitFixedBold.pfb +0 -0
  31. package/wasm/FoxitFixedBoldItalic.pfb +0 -0
  32. package/wasm/FoxitFixedItalic.pfb +0 -0
  33. package/wasm/FoxitSans.pfb +0 -0
  34. package/wasm/FoxitSansBold.pfb +0 -0
  35. package/wasm/FoxitSansBoldItalic.pfb +0 -0
  36. package/wasm/FoxitSansItalic.pfb +0 -0
  37. package/wasm/FoxitSerif.pfb +0 -0
  38. package/wasm/FoxitSerifBold.pfb +0 -0
  39. package/wasm/FoxitSerifBoldItalic.pfb +0 -0
  40. package/wasm/FoxitSerifItalic.pfb +0 -0
  41. package/wasm/FoxitSymbol.pfb +0 -0
  42. package/wasm/THIRD_PARTY_NOTICES +2362 -0
  43. package/wasm/cjk-fallback.ttf +0 -0
  44. package/wasm/cmaps.deflate +0 -0
  45. package/wasm/pins.d.mts +1 -0
  46. package/wasm/pins.mjs +23 -0
  47. package/wasm/recto-more.wasm +0 -0
  48. package/wasm/recto-rare.wasm +0 -0
  49. package/wasm/recto-render.wasm +0 -0
  50. package/wasm/recto.gen.d.mts +3 -0
  51. package/wasm/recto.gen.mjs +1203 -0
  52. package/wasm/recto.types.d.mts +295 -0
  53. package/wasm/recto.wasm +0 -0
package/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin Guo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ vendor/ holds patched copies of the hayro crates, MIT or Apache-2.0 licensed by
24
+ their authors; see vendor/README.md and each crate's licence files. The shipped
25
+ fonts and CMaps have their own licences, gathered in js/wasm/THIRD_PARTY_NOTICES
26
+ by the build.
package/README.md CHANGED
@@ -1,3 +1,26 @@
1
- # Temporary Holding Version
1
+ # @2018kguo/recto
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A PDF engine for JavaScript: text with positions, page objects and rendering, in Rust compiled
4
+ to WebAssembly. No runtime dependencies and no install scripts; the built wasm ships in this
5
+ package, so you don't need Rust.
6
+
7
+ ```js
8
+ import { readFile } from "node:fs/promises";
9
+ import { openPdf } from "@2018kguo/recto";
10
+
11
+ const bytes = await readFile("report.pdf");
12
+ await using doc = await openPdf(bytes);
13
+ doc.pageCount;
14
+ doc.text(0).text;
15
+ const { width, height, rgba } = doc.render(0, { width: 1024 });
16
+ ```
17
+
18
+ Calls run synchronously on the calling thread, so put heavy work on a `worker_threads` worker. In
19
+ a browser, use a dedicated Web Worker: `import` it there and call `openPdf`. Nothing else is
20
+ needed. It also runs on a page's main thread and in a service worker, where it fetches its assets
21
+ up front, but a main thread freezes for the length of each call.
22
+
23
+ [Documentation](https://github.com/2018kguo/recto#readme) ·
24
+ [Reference](https://github.com/2018kguo/recto/blob/main/docs/reference.md)
25
+
26
+ MIT. The bundled fonts and CMaps have their own licences: see `wasm/THIRD_PARTY_NOTICES`.
@@ -0,0 +1,19 @@
1
+ import { PINS } from "../wasm/pins.mjs";
2
+ export type AssetName = keyof typeof PINS;
3
+ export type WasmName = "recto.wasm" | "recto-render.wasm" | "recto-more.wasm" | "recto-rare.wasm";
4
+ /** The standard fonts, in the order the wasm asks for them (`font_index` in crates/recto/src/assets.rs). */
5
+ export declare const STANDARD_FONTS: readonly ["FoxitFixed.pfb", "FoxitFixedBold.pfb", "FoxitFixedBoldItalic.pfb", "FoxitFixedItalic.pfb", "FoxitSans.pfb", "FoxitSansBold.pfb", "FoxitSansBoldItalic.pfb", "FoxitSansItalic.pfb", "FoxitSerif.pfb", "FoxitSerifBold.pfb", "FoxitSerifBoldItalic.pfb", "FoxitSerifItalic.pfb", "FoxitSymbol.pfb", "FoxitDingbats.pfb"];
6
+ /** Whether an asset can be read at the moment the engine asks for it. */
7
+ export declare function canReadSync(): boolean;
8
+ /** `assetBase` as a directory URL, or null for the package's own files. */
9
+ export declare function assetBaseUrl(base: string | URL | undefined): URL | null;
10
+ /** A hash to use where there is no native one to call synchronously. A live instance's is quicker than this file's. */
11
+ export type SyncHash = (bytes: Uint8Array) => string | null;
12
+ /** The wasm, verified and then compiled. */
13
+ export declare function compileWasm(name: WasmName, base: URL | null): Promise<WebAssembly.Module>;
14
+ /** Fetches and verifies assets ahead of time; a thread that can't read them when needed has to. */
15
+ export declare function preloadAssets(base: URL | null, names: AssetName[]): Promise<void>;
16
+ /** An asset, read synchronously. Throws a PdfError rather than returning nothing. */
17
+ export declare function loadAssetSync(name: AssetName, base: URL | null, hash?: SyncHash): Uint8Array;
18
+ /** A file, read now and hashed before it's returned, and not kept: a part is compiled once. */
19
+ export declare function readCheckedSync(name: AssetName, base: URL | null, hash?: SyncHash): Uint8Array;
package/dist/assets.js ADDED
@@ -0,0 +1,250 @@
1
+ // The files in ../wasm: where they are, how to read them, and the check that they are the
2
+ // ones this build was made with.
3
+ //
4
+ // The engine asks for a font or CMap from inside a synchronous call, the first time a
5
+ // document needs one, so reading must be synchronous too: fs in Node, a synchronous
6
+ // XMLHttpRequest in a Web Worker. A page's main thread may not fetch bytes synchronously, and a
7
+ // service worker has no XMLHttpRequest, so there they are fetched up front instead. The URLs
8
+ // are written out as `new URL("literal", import.meta.url)` because that is the form
9
+ // bundlers recognise and rewrite, emitting the file.
10
+ //
11
+ // Each file is pinned by SHA-256 and hashed here, every byte, before it's used: the engine reads
12
+ // its tables as trusted, and a dev server's HTML fallback page must not read as a font. Nothing is
13
+ // left to `fetch`'s `integrity`, which a wrapper around `fetch` can drop, and which buffers the
14
+ // body anyway. pins.mjs is written by the build.
15
+ import { PINS } from "../wasm/pins.mjs";
16
+ import { PdfError } from "./errors.js";
17
+ import { sha256Hex } from "./sha256.js";
18
+ /** The standard fonts, in the order the wasm asks for them (`font_index` in crates/recto/src/assets.rs). */
19
+ export const STANDARD_FONTS = [
20
+ "FoxitFixed.pfb",
21
+ "FoxitFixedBold.pfb",
22
+ "FoxitFixedBoldItalic.pfb",
23
+ "FoxitFixedItalic.pfb",
24
+ "FoxitSans.pfb",
25
+ "FoxitSansBold.pfb",
26
+ "FoxitSansBoldItalic.pfb",
27
+ "FoxitSansItalic.pfb",
28
+ "FoxitSerif.pfb",
29
+ "FoxitSerifBold.pfb",
30
+ "FoxitSerifBoldItalic.pfb",
31
+ "FoxitSerifItalic.pfb",
32
+ "FoxitSymbol.pfb",
33
+ "FoxitDingbats.pfb",
34
+ ];
35
+ function packagedUrl(name) {
36
+ switch (name) {
37
+ case "recto.wasm":
38
+ return new URL("../wasm/recto.wasm", import.meta.url);
39
+ case "recto-render.wasm":
40
+ return new URL("../wasm/recto-render.wasm", import.meta.url);
41
+ case "recto-more.wasm":
42
+ return new URL("../wasm/recto-more.wasm", import.meta.url);
43
+ case "recto-rare.wasm":
44
+ return new URL("../wasm/recto-rare.wasm", import.meta.url);
45
+ case "FoxitFixed.pfb":
46
+ return new URL("../wasm/FoxitFixed.pfb", import.meta.url);
47
+ case "FoxitFixedBold.pfb":
48
+ return new URL("../wasm/FoxitFixedBold.pfb", import.meta.url);
49
+ case "FoxitFixedBoldItalic.pfb":
50
+ return new URL("../wasm/FoxitFixedBoldItalic.pfb", import.meta.url);
51
+ case "FoxitFixedItalic.pfb":
52
+ return new URL("../wasm/FoxitFixedItalic.pfb", import.meta.url);
53
+ case "FoxitSans.pfb":
54
+ return new URL("../wasm/FoxitSans.pfb", import.meta.url);
55
+ case "FoxitSansBold.pfb":
56
+ return new URL("../wasm/FoxitSansBold.pfb", import.meta.url);
57
+ case "FoxitSansBoldItalic.pfb":
58
+ return new URL("../wasm/FoxitSansBoldItalic.pfb", import.meta.url);
59
+ case "FoxitSansItalic.pfb":
60
+ return new URL("../wasm/FoxitSansItalic.pfb", import.meta.url);
61
+ case "FoxitSerif.pfb":
62
+ return new URL("../wasm/FoxitSerif.pfb", import.meta.url);
63
+ case "FoxitSerifBold.pfb":
64
+ return new URL("../wasm/FoxitSerifBold.pfb", import.meta.url);
65
+ case "FoxitSerifBoldItalic.pfb":
66
+ return new URL("../wasm/FoxitSerifBoldItalic.pfb", import.meta.url);
67
+ case "FoxitSerifItalic.pfb":
68
+ return new URL("../wasm/FoxitSerifItalic.pfb", import.meta.url);
69
+ case "FoxitSymbol.pfb":
70
+ return new URL("../wasm/FoxitSymbol.pfb", import.meta.url);
71
+ case "FoxitDingbats.pfb":
72
+ return new URL("../wasm/FoxitDingbats.pfb", import.meta.url);
73
+ case "cjk-fallback.ttf":
74
+ return new URL("../wasm/cjk-fallback.ttf", import.meta.url);
75
+ case "cmaps.deflate":
76
+ return new URL("../wasm/cmaps.deflate", import.meta.url);
77
+ }
78
+ }
79
+ /** A stalled server must not block the thread for ever; the largest asset is 5 MB. */
80
+ const ASSET_TIMEOUT_MS = 60_000;
81
+ /** Node's fs when there is one. `getBuiltinModule` keeps bundlers from following it. */
82
+ function nodeFs() {
83
+ return globalThis.process?.getBuiltinModule?.("node:fs");
84
+ }
85
+ /** Whether an asset can be read at the moment the engine asks for it. */
86
+ export function canReadSync() {
87
+ if (nodeFs()) {
88
+ return true;
89
+ }
90
+ return typeof document === "undefined" && typeof XMLHttpRequest !== "undefined";
91
+ }
92
+ /** `assetBase` as a directory URL, or null for the package's own files. */
93
+ export function assetBaseUrl(base) {
94
+ if (base === undefined) {
95
+ return null;
96
+ }
97
+ let url;
98
+ if (base instanceof URL) {
99
+ url = new URL(base);
100
+ }
101
+ else if (/^[a-z][a-z0-9+.-]+:/iu.test(base)) {
102
+ url = new URL(base);
103
+ }
104
+ else if (nodeFs()) {
105
+ url = globalThis.process.getBuiltinModule("node:url").pathToFileURL(base);
106
+ }
107
+ else {
108
+ url = new URL(base, location.href);
109
+ }
110
+ if (!url.pathname.endsWith("/")) {
111
+ url.pathname += "/";
112
+ }
113
+ if (nodeFs() && url.protocol !== "file:") {
114
+ throw new TypeError("In Node, assetBase must be a directory path or a file: URL: the engine reads fonts synchronously.");
115
+ }
116
+ return url;
117
+ }
118
+ function assetUrl(name, base) {
119
+ return base ? new URL(name, base) : packagedUrl(name);
120
+ }
121
+ function loadFailure(name, url, reason, cause) {
122
+ return new PdfError("missing-asset", `recto could not load ${name}${url ? ` from ${url.href}` : ""}: ${reason}. It ships in the package's wasm/ ` +
123
+ "directory. If a bundler or dev server moved the package without its files, copy them " +
124
+ "somewhere it serves and pass that directory as `assetBase` (with Vite's dev server, add " +
125
+ 'optimizeDeps: { exclude: ["@2018kguo/recto"] } instead; a server bundle should keep the package external).', cause);
126
+ }
127
+ function notThisBuild(name, url, bytes) {
128
+ 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
+ }
130
+ function checked(name, url, bytes, hash) {
131
+ const crypto = globalThis.process?.getBuiltinModule?.("node:crypto");
132
+ const hex = crypto ? crypto.createHash("sha256").update(bytes).digest("hex") : (hash?.(bytes) ?? sha256Hex(bytes));
133
+ if (hex !== PINS[name]) {
134
+ throw notThisBuild(name, url, bytes);
135
+ }
136
+ return bytes;
137
+ }
138
+ function readFromDisk(url) {
139
+ return nodeFs().readFileSync(url);
140
+ }
141
+ function readSync(url) {
142
+ const xhr = new XMLHttpRequest();
143
+ xhr.open("GET", url.href, false);
144
+ xhr.responseType = "arraybuffer";
145
+ xhr.timeout = ASSET_TIMEOUT_MS;
146
+ xhr.send();
147
+ if (xhr.status !== 200) {
148
+ throw new Error(`HTTP ${xhr.status}`);
149
+ }
150
+ return new Uint8Array(xhr.response);
151
+ }
152
+ /** `crypto.subtle` is only there in a secure context; a plain-http page hashes here. */
153
+ async function hashOf(bytes) {
154
+ const subtle = globalThis.crypto?.subtle;
155
+ if (!subtle) {
156
+ return sha256Hex(bytes);
157
+ }
158
+ const digest = new Uint8Array(await subtle.digest("SHA-256", bytes));
159
+ return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
160
+ }
161
+ /** The only way bytes get here from a network without blocking: whole, and hashed before they're used. */
162
+ async function fetchBytes(name, url) {
163
+ const response = await fetch(url);
164
+ if (!response.ok) {
165
+ throw new Error(`HTTP ${response.status}`);
166
+ }
167
+ const bytes = new Uint8Array(await response.arrayBuffer());
168
+ if ((await hashOf(bytes)) !== PINS[name]) {
169
+ throw notThisBuild(name, url, bytes);
170
+ }
171
+ return bytes;
172
+ }
173
+ /** The wasm, verified and then compiled. */
174
+ 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
+ }
188
+ return await compiling(name, WebAssembly.compile(bytes));
189
+ }
190
+ function compiling(name, module) {
191
+ return module.catch((error) => {
192
+ throw new PdfError("missing-asset", `recto could not compile ${name}: ${String(error)}. A Content-Security-Policy must allow 'wasm-unsafe-eval'.`, error);
193
+ });
194
+ }
195
+ /** Verified assets by URL: hash and read each once per thread. */
196
+ const loaded = new Map();
197
+ /** Fetches and verifies assets ahead of time; a thread that can't read them when needed has to. */
198
+ 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);
214
+ }
215
+ loaded.set(url.href, bytes);
216
+ }));
217
+ }
218
+ /** An asset, read synchronously. Throws a PdfError rather than returning nothing. */
219
+ 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;
226
+ }
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;
234
+ }
235
+ /** A file, read now and hashed before it's returned, and not kept: a part is compiled once. */
236
+ export function readCheckedSync(name, base, hash) {
237
+ let url = null;
238
+ let bytes;
239
+ try {
240
+ url = assetUrl(name, base);
241
+ if (!canReadSync()) {
242
+ throw new Error("this thread can only use assets fetched up front, and it was left out of preloadAssets");
243
+ }
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
+ }
249
+ return checked(name, url, bytes, hash);
250
+ }
@@ -0,0 +1,307 @@
1
+ import type { createGlue } from "../wasm/recto.gen.mjs";
2
+ import type * as Glue from "../wasm/recto.types.mjs";
3
+ import { PdfError } from "./errors.ts";
4
+ export type PdfBox = {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ };
10
+ export type PdfPageInfo = {
11
+ width: number;
12
+ height: number;
13
+ rotation: 0 | 90 | 180 | 270;
14
+ };
15
+ export type PdfFont = {
16
+ name: string;
17
+ /** PDF font descriptor flags (fixed pitch, serif, symbolic, italic, ...). */
18
+ flags: number;
19
+ /** 100 to 900 as the file states it, or as its name says; 0 where it says nothing. */
20
+ weight: number;
21
+ embedded: boolean;
22
+ /** Top of this font's char boxes, in ems above the baseline: the descriptor's, or 0.8. */
23
+ ascent: number;
24
+ /** Bottom of this font's char boxes, in ems (negative): the descriptor's, or -0.2. */
25
+ descent: number;
26
+ };
27
+ /** `PdfPageText.flags` bit: a space or line break the engine inferred; not in the content stream. */
28
+ export declare const CHAR_GENERATED = 1;
29
+ /** `PdfPageText.flags` bit: the glyph has no Unicode mapping, so its text is unreadable. */
30
+ export declare const CHAR_UNMAPPED = 2;
31
+ /**
32
+ * A page's characters as columns (structure of arrays): char `i` is entry
33
+ * `i` of every column, and entries `4i..4i+3` of `boxes`. Typed arrays, so
34
+ * a page crosses a thread boundary by transfer, not by copying objects.
35
+ */
36
+ export type PdfPageText = {
37
+ /** The whole page's text, generated spaces included. */
38
+ text: string;
39
+ count: number;
40
+ /** Unicode code points (U+FFFD where a glyph's code is no scalar value). */
41
+ codepoints: Uint32Array<ArrayBuffer>;
42
+ /** x, y, width, height per char: the advance by the font's ascent and descent, not glyph ink. */
43
+ boxes: Float32Array<ArrayBuffer>;
44
+ /** The size each char is drawn at, in points: `Tf` scaled by the text and page matrices. */
45
+ fontSizes: Float32Array<ArrayBuffer>;
46
+ /** The `Tf` operand each char was shown with, unscaled. */
47
+ tfSizes: Float32Array<ArrayBuffer>;
48
+ /** Radians, counter-clockwise. */
49
+ angles: Float32Array<ArrayBuffer>;
50
+ /** Fill colour as 0xRRGGBBAA. */
51
+ fills: Uint32Array<ArrayBuffer>;
52
+ /** Index into `fonts`; -1 for characters the engine generated. */
53
+ fontIds: Int32Array<ArrayBuffer>;
54
+ /** CHAR_GENERATED | CHAR_UNMAPPED. */
55
+ flags: Uint8Array<ArrayBuffer>;
56
+ /** One per font dictionary used on the page. */
57
+ fonts: PdfFont[];
58
+ /** The page showed more glyphs than one page may collect (2^19); the rest aren't here. */
59
+ truncated: boolean;
60
+ /** Runs of non-space chars in content order, as char ranges and boxes. */
61
+ words: PdfWords;
62
+ /**
63
+ * Index into `runs` per char: the text object (one show-text operator)
64
+ * the char was drawn by; -1 for chars the engine generated.
65
+ */
66
+ runIds: Int32Array<ArrayBuffer>;
67
+ runs: PdfTextRuns;
68
+ };
69
+ /**
70
+ * One entry per text object, in the order its chars first appear: where
71
+ * its text starts and how large it is drawn. Every char of a run sits on
72
+ * the run's baseline (a rise or a new line starts a new text object).
73
+ */
74
+ export type PdfTextRuns = {
75
+ count: number;
76
+ /** x, y per run: the text origin on the baseline, in the viewer frame. */
77
+ origins: Float32Array<ArrayBuffer>;
78
+ /** The font size as drawn, in points (Tf scaled by the text and current matrices). */
79
+ ems: Float32Array<ArrayBuffer>;
80
+ };
81
+ export type PdfWords = {
82
+ count: number;
83
+ /** First and last char index of each word, inclusive. */
84
+ firstChar: Uint32Array<ArrayBuffer>;
85
+ lastChar: Uint32Array<ArrayBuffer>;
86
+ /** x, y, width, height per word: the union of its char boxes. */
87
+ boxes: Float32Array<ArrayBuffer>;
88
+ };
89
+ /** Entry `i` of a `boxes` column. */
90
+ export declare function boxAt(boxes: Float32Array, i: number): PdfBox;
91
+ /** The text of chars `first..last` (inclusive). */
92
+ export declare function charsText(page: Pick<PdfPageText, "codepoints">, first: number, last: number): string;
93
+ /** Segment kinds, in the low bits of `PdfPathSegments.kinds`. */
94
+ export declare const SEGMENT_LINE = 0;
95
+ export declare const SEGMENT_BEZIER = 1;
96
+ export declare const SEGMENT_MOVE = 2;
97
+ /** `PdfPathSegments.kinds` bit: the segment closes its subpath. */
98
+ export declare const SEGMENT_CLOSE = 4;
99
+ /**
100
+ * `PdfPathSegments.kinds` bit on a move: its subpath is a rectangle drawn by
101
+ * `re` (pdf.js counts only those as rectangles).
102
+ */
103
+ export declare const SEGMENT_RECT = 8;
104
+ /** `paths.flags` bits. */
105
+ export declare const PATH_FILL = 1;
106
+ export declare const PATH_STROKE = 2;
107
+ /** All paths' segments end to end; path `i` owns `start[i]..start[i+1]`. */
108
+ export type PdfPathSegments = {
109
+ count: number;
110
+ /** x, y per segment end point, in the viewer frame. */
111
+ points: Float32Array<ArrayBuffer>;
112
+ /** SEGMENT_LINE | SEGMENT_BEZIER | SEGMENT_MOVE, plus SEGMENT_CLOSE and SEGMENT_RECT. */
113
+ kinds: Uint8Array<ArrayBuffer>;
114
+ };
115
+ /**
116
+ * A page's images and paths as columns. Form XObjects are flattened, their
117
+ * transforms applied; every box and point is in the viewer frame.
118
+ */
119
+ export type PdfPageObjects = {
120
+ textObjects: number;
121
+ /** The page held more than one page may collect (2^21 path segments, 2^19 images); the rest aren't here. */
122
+ truncated: boolean;
123
+ images: {
124
+ count: number;
125
+ /** x, y, width, height per image. */
126
+ boxes: Float32Array<ArrayBuffer>;
127
+ /** Pixel width, height per image (0, 0 when the engine cannot say). */
128
+ pixels: Uint32Array<ArrayBuffer>;
129
+ };
130
+ paths: {
131
+ count: number;
132
+ /** x, y, width, height per path. */
133
+ boxes: Float32Array<ArrayBuffer>;
134
+ /** PATH_FILL | PATH_STROKE. */
135
+ flags: Uint8Array<ArrayBuffer>;
136
+ /** count + 1 offsets into `segments`. */
137
+ start: Uint32Array<ArrayBuffer>;
138
+ };
139
+ segments: PdfPathSegments;
140
+ };
141
+ export type PdfAnnotation = {
142
+ subtype: number;
143
+ box: PdfBox;
144
+ /** /Contents (a FreeText's text; a note for most other kinds); "" when absent. */
145
+ contents: string;
146
+ /**
147
+ * A widget's form field: its type (/FT: "Tx", "Ch", "Btn", "Sig") and
148
+ * value (/V, both inherited through /Parent); null for other annotations.
149
+ * A multi-select choice field's /V array reads as "".
150
+ */
151
+ field: {
152
+ type: string;
153
+ value: string;
154
+ } | null;
155
+ /**
156
+ * Objects in the annotation's normal appearance, and how many of them are
157
+ * text: a stamp that paints words has text objects. 0 without an
158
+ * appearance.
159
+ */
160
+ appearanceObjects: number;
161
+ appearanceTextObjects: number;
162
+ /**
163
+ * The text the appearance's text objects paint, one object a line (a
164
+ * stamp's words, a list box's options), each line trimmed.
165
+ */
166
+ appearanceText: string;
167
+ };
168
+ export type PdfRenderOptions = {
169
+ /** Target width in pixels; the scale also keeps height within `maxHeight`. */
170
+ width: number;
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
+ maxHeight?: number;
173
+ };
174
+ export type PdfRenderAsyncOptions = PdfRenderOptions & {
175
+ /** Stops the render at the next pause: `aborted`, with the document still usable. */
176
+ signal?: AbortSignal;
177
+ /** About how long it runs before giving the thread back. Default 8. */
178
+ sliceMs?: number;
179
+ };
180
+ export type PdfBitmap = {
181
+ width: number;
182
+ height: number;
183
+ /** Opaque RGBA on a white background, four bytes a pixel. */
184
+ rgba: Uint8Array<ArrayBuffer>;
185
+ /**
186
+ * The page passed a limit on what one render may do (more than 32 groups that need the page
187
+ * redrawn under them), so past that those groups are drawn as if isolated.
188
+ */
189
+ truncated: boolean;
190
+ };
191
+ export declare const ANNOT_SUBTYPE: {
192
+ readonly text: 1;
193
+ readonly link: 2;
194
+ readonly freeText: 3;
195
+ readonly stamp: 13;
196
+ readonly widget: 20;
197
+ };
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
+ /** A bookmark. `page` is -1 if it goes nowhere in this file; `url` is set if it links out. */
203
+ export type PdfOutlineItem = {
204
+ title: string;
205
+ page: number;
206
+ url: string | null;
207
+ children: PdfOutlineItem[];
208
+ };
209
+ export interface PdfDocument {
210
+ readonly pageCount: number;
211
+ /** The document's bookmarks; empty if it has none. */
212
+ outline(): PdfOutlineItem[];
213
+ /** What each page is called, one string per page ("iv", "A-5"), or null if the file doesn't say. */
214
+ pageLabels(): string[] | null;
215
+ /**
216
+ * Which of the document's trees were cut short: `pages` means `pageCount` may be short, by a limit or
217
+ * because a damaged tree left no pages (an honestly empty document says false). `pageLabels` and `outline`
218
+ * mean those passed a limit, and may be wrong or incomplete. A page's own limits are `truncated` on its results.
219
+ */
220
+ structureTruncated(): {
221
+ pages: boolean;
222
+ pageLabels: boolean;
223
+ outline: boolean;
224
+ };
225
+ pageInfo(index: number): PdfPageInfo;
226
+ text(index: number): PdfPageText;
227
+ /** `text`, `objects` and `annotations` from one interpretation of the page. */
228
+ scan(index: number): {
229
+ text: PdfPageText;
230
+ objects: PdfPageObjects;
231
+ annotations: PdfAnnotation[];
232
+ };
233
+ /** `text` and `annotations` from one interpretation of the page. */
234
+ textAndAnnotations(index: number): {
235
+ text: PdfPageText;
236
+ annotations: PdfAnnotation[];
237
+ };
238
+ objects(index: number): PdfPageObjects;
239
+ annotations(index: number): PdfAnnotation[];
240
+ render(index: number, options: PdfRenderOptions): PdfBitmap;
241
+ /**
242
+ * The same pixels as `render`, giving the thread back between slices, and stoppable with a
243
+ * signal. The last step, rasterizing, is one piece: about a third of a heavy page's time.
244
+ */
245
+ renderAsync(index: number, options: PdfRenderAsyncOptions): Promise<PdfBitmap>;
246
+ close(): void;
247
+ [Symbol.dispose](): void;
248
+ }
249
+ export type Instance = {
250
+ glue: ReturnType<typeof createGlue>;
251
+ /** Loads a part of the wasm, hashed first, or throws `missing-asset` having changed nothing. */
252
+ loadPart(name: "recto-render.wasm" | "recto-more.wasm" | "recto-rare.wasm"): void;
253
+ memory: WebAssembly.Memory;
254
+ /** Its memory when it was made: past this, something used it, and dropping it gives memory back. */
255
+ startBytes: number;
256
+ generation: number;
257
+ openDocuments: number;
258
+ /** The engine's count of documents open, on any of its instances. */
259
+ documents: {
260
+ open: number;
261
+ };
262
+ /** Set once the instance trapped; nothing may call it again. */
263
+ dead: boolean;
264
+ /**
265
+ * Set once a part or asset failed to load inside a call. Nothing more runs on it, but a document's
266
+ * file is still whole there, so it reopens on another instance at its next call.
267
+ */
268
+ abandoned: boolean;
269
+ /** An asset's load failure, rethrown by the call that needed the asset. */
270
+ failure: PdfError | undefined;
271
+ /** Called when a document on this instance closes. */
272
+ onClose?: () => void;
273
+ };
274
+ /** What a call into the instance threw, as what the caller should see. */
275
+ export declare function engineFailure(instance: Instance, error: unknown): unknown;
276
+ export declare class RectoDocument implements PdfDocument {
277
+ #private;
278
+ readonly pageCount: number;
279
+ constructor(instance: Instance, doc: Glue.Document, reopen: (doc: Glue.Document, instance: Instance) => {
280
+ instance: Instance;
281
+ doc: Glue.Document;
282
+ }, fetchFailure?: () => PdfError | null);
283
+ pageInfo(index: number): PdfPageInfo;
284
+ text(index: number): PdfPageText;
285
+ objects(index: number): PdfPageObjects;
286
+ pageLabels(): string[] | null;
287
+ structureTruncated(): {
288
+ pages: boolean;
289
+ pageLabels: boolean;
290
+ outline: boolean;
291
+ };
292
+ outline(): PdfOutlineItem[];
293
+ annotations(index: number): PdfAnnotation[];
294
+ scan(index: number): {
295
+ text: PdfPageText;
296
+ objects: PdfPageObjects;
297
+ annotations: PdfAnnotation[];
298
+ };
299
+ textAndAnnotations(index: number): {
300
+ text: PdfPageText;
301
+ annotations: PdfAnnotation[];
302
+ };
303
+ render(index: number, options: PdfRenderOptions): PdfBitmap;
304
+ renderAsync(index: number, options: PdfRenderAsyncOptions): Promise<PdfBitmap>;
305
+ close(): void;
306
+ [Symbol.dispose](): void;
307
+ }