@web-ts-toolkit/pdf-reader 0.43.0 → 0.48.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.
- package/README.md +172 -38
- package/index.d.mts +163 -12
- package/index.mjs +642 -153
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Resource-safe browser PDF rendering, text extraction, and best-effort embedded-i
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
pnpm add @web-ts-toolkit/pdf-reader pdfjs-dist
|
|
8
|
+
pnpm add @web-ts-toolkit/pdf-reader pdfjs-dist@~6.2.108
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
`pdfjs-dist` is a peer dependency. This package is browser-oriented, ESM-only, and requires DOM canvas APIs. Automated browser compatibility coverage currently runs the real PDF.js worker and browser canvas suite in Headless Chromium only. Firefox and WebKit are not claimed by the automated package test boundary until their Playwright browsers are added to `vitest.browser.config.mts` and pass the same fixture suite in CI.
|
|
@@ -16,15 +16,27 @@ Canonical imports use named exports from the package root. There is no default e
|
|
|
16
16
|
|
|
17
17
|
## Unreleased Migration
|
|
18
18
|
|
|
19
|
+
New named root export `pdfTextToString(content)` assembles raw `PageResult.text` for local search/indexing. It preserves supplied item order, whitespace, and Unicode, skips marked-content entries, and appends one `\n` for every text item's `hasEOL: true` (including empty and final items). It guesses no spaces and trims nothing. Use `includePageImage: false` for text-only ingestion; page rendering still defaults to enabled. See the local ingestion recipe below.
|
|
20
|
+
|
|
21
|
+
Signal inputs are now validated before load/page work, including an already-loaded `load()` call. Replace `null` or malformed signal placeholders with an omitted property or `undefined`; invalid values produce `INVALID_OPTION`. Native signals from other realms (such as same-origin iframes) remain supported.
|
|
22
|
+
|
|
23
|
+
**Render cancellation/failure now permanently closes the reader.** Once a render task has been created, cancellation or render failure starts public PDF.js teardown automatically. The initiating operation still rejects promptly with `ABORTED` or the exact native error; explicit destruction produces `DESTROYED`. Later `load()`, `pages()` execution, and `convert()` fail with `DESTROYED`. Retry requires a fresh reader and fresh source bytes, since PDF.js may have transferred the original input. This intentionally replaces the previous same-reader render-retry promise: a rejected render promise does not mean PDF.js has finished its internal stream or image cleanup. No replacement document is silently loaded.
|
|
24
|
+
|
|
25
|
+
Pre-render/non-render cancellation and encoding cancellation/failure after successfully completed rendering still permit same-reader reuse. These safe retries share cached-page ownership: cleanup waits for pending acquisitions, upstream stage promises, and active processing. Cleanup timing can extend beyond caller settlement; use `destroy()` to observe authoritative teardown, including any failure from automatic teardown.
|
|
26
|
+
|
|
19
27
|
This follow-up release keeps the small named-export API but intentionally does not preserve the older application-local `PDFReader` names and defaults.
|
|
20
28
|
|
|
21
|
-
| Area
|
|
22
|
-
|
|
|
23
|
-
| Imports and worker setup
|
|
24
|
-
| Load cancellation and teardown
|
|
25
|
-
| Load options
|
|
26
|
-
| Page image results
|
|
27
|
-
| Convert option names
|
|
29
|
+
| Area | Previous contract | New contract and migration |
|
|
30
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
31
|
+
| Imports and worker setup | The application-local reader was typically imported as a default export and could hide PDF.js worker wiring inside app code. | Import named exports from `@web-ts-toolkit/pdf-reader` and call `configurePdfWorker(...)` in application code. There is no default export, no deep import, and no package-owned worker asset emission. |
|
|
32
|
+
| Load cancellation and teardown | Concurrent callers could accidentally tear down a shared load, and lifecycle states were mostly implicit. | Concurrent `load()` callers share one PDF.js task, but aborting one caller rejects only that caller. The `PDFDocumentProxy` returned by `load()` is borrowed; inspect it if needed, but call `reader.destroy()` instead of `document.destroy()`. Use `reader.state` for `new` / `loading` / `loaded` / `iterating` / `failed` / `destroyed`. |
|
|
33
|
+
| Load options | `load()` previously accepted only an `AbortSignal`-style cancellation pattern. | `load()` still accepts an `AbortSignal`, and now also accepts `{ signal, deadlineMs }`. `deadlineMs` is caller-local and rejects with `DEADLINE_EXCEEDED` without cancelling unrelated waiters. |
|
|
34
|
+
| Page image results | Older code used top-level page result fields such as `dataURL`, `dataUrl`, `mimeType`, and `isPNG`. | Read `page.pageImage` instead. `pageImageOutput: 'data-url'` returns `{ kind: 'data-url', mimeType, dataUrl }`; `pageImageOutput: 'blob'` returns `{ kind: 'blob', mimeType, blob }`. Keep blob object URLs alive through `decode()`/display and revoke on replacement/disposal/error; the package returns `Blob`s, never object URLs. |
|
|
35
|
+
| Convert option names | Older application-local option names included `getText`, `getDataURL`, `getImages`, and reader `config`. | Use `includeText`, `includePageImage`, `includeEmbeddedImages`, and constructor `options`. Unsupported legacy names are intentionally rejected by the type surface instead of being kept as aliases. |
|
|
36
|
+
| Source snapshot and byte limits | Earlier docs left header stabilization, byte accounting, and retry behavior implicit. | Effective headers are snapshotted once before `sourcePolicy(...)` and shared with PDF.js; in-memory `data` stays borrowed with its size rechecked after approval, and growth during approval fails before loading. Known byte lengths cover binary strings (one byte per code unit), number arrays (one byte per entry, length-first), and buffer/view `byteLength`. Synchronous load failures report `failed` and permit a fresh `load()` retry. |
|
|
37
|
+
| Page-stage cancellation | Synchronous callback cancellation could start unnecessary rendering or leave already-started promises unobserved. | Page-stage waits settle promptly with `ABORTED`/`DESTROYED`, including cancellation before wait registration. Active renders are cancelled; other started PDF.js work remains observed. Canvas/viewport callbacks are followed by lifecycle checks before subsequent work. Caller settlement does not imply upstream completion; see the contracts below. A caller-created `PDFWorker` stays caller-owned. |
|
|
38
|
+
| Option and output validation | Invalid runtime options and canvas results were partly unchecked. | Unsupported MIME values, malformed ranges, non-number/non-function scales, non-boolean flags, out-of-range deadlines, and empty/mismatched canvas encodes are rejected with `INVALID_OPTION` or `UNSUPPORTED_ENVIRONMENT` before page work or result publication. Caller-created canvases must be fresh DOM `HTMLCanvasElement`s. |
|
|
39
|
+
| Images and performance claims | Image layout handling and benchmark retention needed bounded evidence. | One-bit (`GRAYSCALE_1BPP`) images decode by declared kind with row padding (unsupported layouts warn/skip); shared `g_` references resolve via `commonObjs` with readiness waits. Page work stays serial (`OPERATION_IN_PROGRESS` on overlap); text/operator limits still apply after PDF.js returns complete structures, with no streaming-text or concurrency API change in this release. |
|
|
28
40
|
|
|
29
41
|
Release-note evidence for this follow-up is recorded in this README, the website package docs, and the task completion record. `CHANGELOG.md` was intentionally not edited for PDFR2-05 per maintainer instruction.
|
|
30
42
|
|
|
@@ -51,7 +63,9 @@ import { configurePdfWorker } from '@web-ts-toolkit/pdf-reader';
|
|
|
51
63
|
configurePdfWorker(new Worker(new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url), { type: 'module' }));
|
|
52
64
|
```
|
|
53
65
|
|
|
54
|
-
Worker URL emission differs by bundler. Keep this setup in application code where the bundler owns asset handling. If you pass an existing `Worker`, you still own terminating it.
|
|
66
|
+
Worker URL emission differs by bundler. Keep this setup in application code where the bundler owns asset handling. The package emits no worker asset itself: the URL above is emitted by the application's bundler (Vite's `?url` import) and handed to `configurePdfWorker(...)` before the first `load()`. If you pass an existing `Worker`, you still own terminating it.
|
|
67
|
+
|
|
68
|
+
A DOM `Worker` is the browser thread primitive. A PDF.js `PDFWorker` is PDF.js's ownership wrapper around one such thread: it runs the PDF.js worker script and multiplexes document tasks over it. When `getDocument()` creates its own `PDFWorker` internally (the default path when the source carries no `worker`), PDF.js attaches it to the loading task and destroys it with `task.destroy()`. A caller-created `PDFWorker` passed on the source is never adopted: the task leaves its worker slot empty, so neither `reader.destroy()` nor PDF.js teardown destroys it — the caller must.
|
|
55
69
|
|
|
56
70
|
For per-document isolation, skip the global helper and pass a caller-created PDF.js `PDFWorker` through the source object. The public `PdfSource` type accepts PDF.js `DocumentInitParameters`, including its `worker` field:
|
|
57
71
|
|
|
@@ -64,18 +78,27 @@ const reader = new PDFReader({ data: bytes, worker });
|
|
|
64
78
|
|
|
65
79
|
try {
|
|
66
80
|
await reader.load();
|
|
81
|
+
// ... pages()/convert() ...
|
|
67
82
|
} finally {
|
|
68
|
-
|
|
83
|
+
try {
|
|
84
|
+
await reader.destroy();
|
|
85
|
+
} finally {
|
|
86
|
+
await worker.destroy();
|
|
87
|
+
}
|
|
69
88
|
}
|
|
70
89
|
```
|
|
71
90
|
|
|
91
|
+
The nested `finally` destroys the caller-owned worker even when `reader.destroy()` rejects (for example, after a load failure). Destroy the reader first so in-flight document work settles before the worker goes away. When the `PDFWorker` wraps an externally supplied DOM `Worker` port instead of creating its own thread, also terminate that `Worker` yourself: `PDFWorker.destroy()` releases PDF.js state but does not terminate a caller-supplied port's thread.
|
|
92
|
+
|
|
72
93
|
## Quick Start
|
|
73
94
|
|
|
74
95
|
```ts
|
|
75
96
|
import { PDFReader } from '@web-ts-toolkit/pdf-reader';
|
|
76
97
|
|
|
98
|
+
const maxSourceBytes = 25_000_000;
|
|
99
|
+
if (file.size > maxSourceBytes) throw new Error('PDF exceeds the local file-size limit.');
|
|
77
100
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
78
|
-
const reader = new PDFReader(bytes);
|
|
101
|
+
const reader = new PDFReader(bytes, { limits: { maxSourceBytes } });
|
|
79
102
|
|
|
80
103
|
try {
|
|
81
104
|
await reader.load({ deadlineMs: 15_000 });
|
|
@@ -108,32 +131,71 @@ const reader = new PDFReader(
|
|
|
108
131
|
);
|
|
109
132
|
```
|
|
110
133
|
|
|
111
|
-
|
|
134
|
+
### Load contract
|
|
135
|
+
|
|
136
|
+
Call `load()` before `pages()` or `convert()`. Concurrent `load()` callers share one PDF.js loading task, but aborting one caller only rejects that caller. The fulfilled `load()` value is the reader-owned PDF.js `PDFDocumentProxy` exposed as a borrowed interoperability handle. You may inspect metadata or call supported PDF.js read methods, but do not call `document.destroy()` on it while the reader owns the lifecycle; call `reader.destroy()` instead. External proxy destruction is unsupported because PDF.js does not provide a reliable ownership notification to keep `reader.state` authoritative after that mutation. A rejected load attempt reports `failed` and a later `load()` starts a fresh attempt.
|
|
137
|
+
|
|
138
|
+
### Page-operation contract
|
|
139
|
+
|
|
140
|
+
Page operations are reader-serial: at most one executing `pages()` iterator or `convert()` call may be active per `PDFReader`. A second overlapping page operation fails fast with `OPERATION_IN_PROGRESS` before acquiring a page proxy or allocating a canvas; create a separate reader if you need independent concurrent conversions. Calling `pages()` only creates an iterator object and does not reserve the reader until the iterator starts executing. Page-stage waits (`getPage`, text, operator list, render, encoding) settle promptly on abort/destroy via one shared wait contract. Active renders are cancelled, including a render created during synchronous cancellation before wait registration. Racing other PDF.js work does not stop it: late `getPage()` values are retained only for safe cleanup and never processed, late text/operator values are dropped, and late rejections remain observed. This also applies when a source policy synchronously destroys the reader and later rejects. Canvas factories and viewport callbacks are followed by lifecycle checks before subsequent allocation/rendering. Caller settlement releases the operation lock but does not mean upstream work has finished. A suspended `pages()` generator only observes abort/destroy when the consumer resumes or closes it; destruction cannot force suspended consumer code to run.
|
|
141
|
+
|
|
142
|
+
After cancellation **before render creation**, during non-render acquisition/text/operator/image extraction, or during encoding **after successful rendering**, a sequential retry can start immediately on the same reader. PDF.js may return the same cached proxy, even for a previously pending `getPage()`. Shared ownership delays the package's `page.cleanup()` request until acquisitions, processing, and original stage promises for that page settle. A successful retry can return while older work still retains resources; `state === 'loaded'` describes the operation lock, not upstream idleness. Work that never settles retains ownership until `destroy()`; repeated non-render cancellation does not stop or bound that work. Malformed options and callback failures before rendering do not automatically destroy the reader.
|
|
143
|
+
|
|
144
|
+
Cancellation/failure while awaiting an already-created render is different: PDF.js can still clear shared images after its render promise rejects. The reader becomes `destroyed` and initiates public teardown before settling the operation, retaining its `ABORTED`/native error. Subsequent operations fail with `DESTROYED` before acquiring pages or reading images. The default `includePageImage: true` means ordinary conversions may enter this terminal path; text-only ingestion should explicitly disable it. To recover, explicitly create a fresh reader from the original `File` or another fresh source. Do not reuse a potentially detached typed array:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { PDFReader } from '@web-ts-toolkit/pdf-reader';
|
|
148
|
+
|
|
149
|
+
// Call explicitly after handling the original render error; this does not
|
|
150
|
+
// revive the old reader. Each call reads fresh bytes from the original File.
|
|
151
|
+
async function extractFresh(file: File) {
|
|
152
|
+
const maxSourceBytes = 25_000_000;
|
|
153
|
+
if (file.size > maxSourceBytes) throw new Error('PDF exceeds the local file-size limit.');
|
|
154
|
+
const recovery = new PDFReader(new Uint8Array(await file.arrayBuffer()), { limits: { maxSourceBytes } });
|
|
155
|
+
try {
|
|
156
|
+
await recovery.load();
|
|
157
|
+
return await recovery.convert({
|
|
158
|
+
pageRange: 1,
|
|
159
|
+
includeText: false,
|
|
160
|
+
includePageImage: false,
|
|
161
|
+
includeEmbeddedImages: true,
|
|
162
|
+
});
|
|
163
|
+
} finally {
|
|
164
|
+
await recovery.destroy();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Teardown contract
|
|
170
|
+
|
|
171
|
+
`destroy()` is idempotent, permanently closes the reader, cancels active renders, and causes in-flight lifecycle work to reject with `DESTROYED`. It clears retained page ownership and waits for PDF.js loading/document destruction, without waiting for orphaned acquisition or stage promises. Late acquisitions receive best-effort cleanup without starting processing. Active `pages()` or `convert()` calls release their temporary canvases as those operation promises settle; await those operations to observe canvas release. Awaiting a cancelled operation alone does not guarantee upstream completion or deferred page cleanup. A caller-created `PDFWorker` on the source stays caller-owned: destroy it explicitly in your own `finally` block even when `destroy()` rejects (see Worker Setup).
|
|
172
|
+
|
|
173
|
+
Interrupted renders automatically start the same teardown without awaiting it or replacing the initiating operation's error. Automatic teardown rejection is observed internally, preventing an unhandled rejection. Calling `destroy()` returns the same promise and reports that teardown's success or failure, including on repeated/reentrant calls. If your application must preserve an earlier conversion error, handle/report teardown errors separately rather than allowing an awaited `finally` to replace it. The `destroyed` state means closure has started, not that teardown succeeded or finished.
|
|
112
174
|
|
|
113
175
|
## Reader State And Ownership
|
|
114
176
|
|
|
115
177
|
`reader.state` exposes the public lifecycle:
|
|
116
178
|
|
|
117
|
-
| State | Meaning
|
|
118
|
-
| ----------- |
|
|
119
|
-
| `new` | Reader constructed but `load()` has not started.
|
|
120
|
-
| `loading` | One shared PDF.js loading task is in flight.
|
|
121
|
-
| `loaded` | The PDF.js document is loaded and no page operation is active.
|
|
122
|
-
| `iterating` | One executing `pages()` iterator or `convert()` call owns the reader page operation.
|
|
123
|
-
| `failed` | The most recent load attempt rejected. A later `load()` call starts a fresh attempt.
|
|
124
|
-
| `destroyed` |
|
|
179
|
+
| State | Meaning | Legal next states |
|
|
180
|
+
| ----------- | ------------------------------------------------------------------------------------------------ | ------------------------------- |
|
|
181
|
+
| `new` | Reader constructed but `load()` has not started. | `loading`, `destroyed` |
|
|
182
|
+
| `loading` | One shared PDF.js loading task is in flight. | `loaded`, `failed`, `destroyed` |
|
|
183
|
+
| `loaded` | The PDF.js document is loaded and no page operation is active. | `iterating`, `destroyed` |
|
|
184
|
+
| `iterating` | One executing `pages()` iterator or `convert()` call owns the reader page operation. | `loaded`, `destroyed` |
|
|
185
|
+
| `failed` | The most recent load attempt rejected. A later `load()` call starts a fresh attempt. | `loading`, `destroyed` |
|
|
186
|
+
| `destroyed` | Explicit or automatic teardown has started. The reader cannot be reused, even if teardown fails. | none |
|
|
125
187
|
|
|
126
188
|
Ownership and lifetime rules:
|
|
127
189
|
|
|
128
|
-
- Source bytes: the package snapshots the top-level PDF.js loading parameters once before `sourcePolicy(...)` runs and passes that same snapshot to PDF.js after approval. PDF.js may transfer typed-array ownership to its worker during `load()
|
|
190
|
+
- Source bytes: the package snapshots the top-level PDF.js loading parameters once before `sourcePolicy(...)` runs and passes that same snapshot to PDF.js after approval. In-memory `data` stays a borrowed reference (never copied by the package); its byte size is rechecked after approval, so growth during approval still fails before loading. PDF.js may transfer typed-array ownership to its worker during `load()`, detaching the caller's buffer.
|
|
129
191
|
- Loaded document proxy: `load()` returns the borrowed `PDFDocumentProxy` owned by the reader. Callers may use supported PDF.js read APIs on it, but external `document.destroy()` is unsupported and can leave `reader.state` reporting `loaded` until a later PDF.js method fails.
|
|
130
192
|
- Public aliases: `LoadedPdfDocument` and `LoadedPdfPage` intentionally expose PDF.js proxy types for adjacent interoperability code. The reader API returns `LoadedPdfDocument` from `load()` and package `PageResult` objects from `pages()`/`convert()`; it does not return raw page proxies.
|
|
131
|
-
- Pages: one executing `pages()` iterator or `convert()` call owns the reader-level page operation at a time.
|
|
193
|
+
- Pages: one executing `pages()` iterator or `convert()` call owns the reader-level page operation at a time. Processing ownership is released before yielding and on error/return paths. Cleanup is best-effort, once per idle cached-page ownership cycle; it is deferred across overlapping uncancelled work and retry acquisitions/processing. Idle bookkeeping is removed, while reader destruction clears retained ownership even if promises never settle. The reader coordinates package-started page work, not external page operations or manual proxy cleanup.
|
|
132
194
|
- Page canvases: the package owns temporary render/encode canvases and always zeroes their dimensions after each settle path.
|
|
133
|
-
- `Blob` page images: the returned `Blob` belongs to the caller. If you create an object URL, revoke it yourself.
|
|
195
|
+
- `Blob` page images: the returned `Blob` belongs to the caller. If you create an object URL, keep it alive through decoding/display and revoke it yourself on replacement, disposal, or error (see the preview example below).
|
|
134
196
|
- Data URLs: returned strings belong to the caller. They are copies; the package does not retain the temporary canvas after encoding.
|
|
135
197
|
- Embedded-image data URLs: these are best-effort PNG copies. Repeated image XObject placements on one page share one encoded PNG string while each returned image keeps its own transform and coordinates. Unsupported image operators/layouts are skipped with `logger.warn(...)` diagnostics.
|
|
136
|
-
- Documents and workers: the reader owns the loaded `PDFDocumentProxy` and the shared loading task it created. Call `destroy()` in your own `finally` block to terminate document-side resources. `configurePdfWorker(...)` mutates PDF.js application-global worker state; use a per-source PDF.js `PDFWorker` when separate readers need isolated worker configuration. When `configurePdfWorker(...)` receives a worker URL, PDF.js owns any worker instances it creates from that URL. When `configurePdfWorker(...)` receives an existing `Worker`, the caller still owns terminating that worker.
|
|
198
|
+
- Documents and workers: the reader owns the loaded `PDFDocumentProxy` and the shared loading task it created. Call `destroy()` in your own `finally` block to terminate document-side resources, and destroy a caller-created `PDFWorker` in a nested `finally` even when `destroy()` rejects (see Worker Setup). `configurePdfWorker(...)` mutates PDF.js application-global worker state; use a per-source PDF.js `PDFWorker` when separate readers need isolated worker configuration. When `configurePdfWorker(...)` receives a worker URL, PDF.js owns any worker instances it creates from that URL. When `configurePdfWorker(...)` receives an existing `Worker`, the caller still owns terminating that worker.
|
|
137
199
|
|
|
138
200
|
## Options
|
|
139
201
|
|
|
@@ -157,20 +219,51 @@ const pages = await reader.convert({
|
|
|
157
219
|
- `pageImageOutput`: `data-url` or `blob`; defaults to `data-url`.
|
|
158
220
|
- `includeText`: include PDF.js `TextContent`; defaults to `true`.
|
|
159
221
|
- `includeEmbeddedImages`: inspect the operator stream for raster images; defaults to `false`.
|
|
160
|
-
- `signal`: cancels active rendering and stops before subsequent expensive operations.
|
|
222
|
+
- `signal`: cancels active rendering and stops before subsequent expensive operations. Interrupted renders permanently close the reader; see the page-operation recovery contract above.
|
|
223
|
+
|
|
224
|
+
Both `load(signal)` / `load({ signal, deadlineMs })` and `pages({ signal })` / `convert({ signal })` validate signals structurally: `aborted` must be boolean and `addEventListener` / `removeEventListener` must be callable. Native and cross-realm signals are accepted without cloning their live abort state. Omitted or explicit `undefined` means no signal; `null`, primitives, and partial/non-callable shapes produce `INVALID_OPTION`. `load()` throws synchronously before source policy/PDF.js work, even when already loaded; conversion rejects before `getPage()` and releases its operation lock for a valid retry. `load({})` remains a valid empty options bag; an object containing any of the three signal members is treated as a direct signal and must satisfy the full shape. Nested signal references and load deadlines are read once per operation; custom signal implementations must preserve working event methods and live boolean abort state while in use.
|
|
161
225
|
|
|
162
226
|
Constructor `canvasFactory`, when supplied, must create a fresh DOM `HTMLCanvasElement` for each package-owned render or embedded-image copy. Non-DOM canvas objects are not part of the documented runtime contract unless they satisfy the browser `HTMLCanvasElement` behavior used by PDF.js and this package.
|
|
163
227
|
|
|
164
|
-
`pageImage` is a discriminated union so the output mode and payload cannot contradict each other:
|
|
228
|
+
`pageImage` is a discriminated union so the output mode and payload cannot contradict each other. This package returns `Blob` objects directly rather than object URLs so callers keep explicit ownership of any `URL.createObjectURL(...)` lifecycle. Keep the URL alive through decoding/display: revoking it immediately after assigning `preview.src` invalidates the preview before it loads.
|
|
229
|
+
|
|
230
|
+
Self-contained browser preview with one live URL at a time, released on replacement, disposal, or error:
|
|
165
231
|
|
|
166
232
|
```ts
|
|
233
|
+
function showBlobPreview(preview: HTMLImageElement, blob: Blob): Promise<void> {
|
|
234
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
235
|
+
const previous = preview.dataset.previewUrl;
|
|
236
|
+
preview.dataset.previewUrl = objectUrl;
|
|
237
|
+
if (previous) URL.revokeObjectURL(previous);
|
|
238
|
+
preview.src = objectUrl;
|
|
239
|
+
return preview.decode().catch((error: unknown) => {
|
|
240
|
+
// The preview never displayed: release the URL just installed.
|
|
241
|
+
if (preview.dataset.previewUrl === objectUrl) {
|
|
242
|
+
delete preview.dataset.previewUrl;
|
|
243
|
+
preview.removeAttribute('src');
|
|
244
|
+
}
|
|
245
|
+
URL.revokeObjectURL(objectUrl);
|
|
246
|
+
throw error;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function releaseBlobPreview(preview: HTMLImageElement): void {
|
|
251
|
+
const current = preview.dataset.previewUrl;
|
|
252
|
+
if (current) {
|
|
253
|
+
delete preview.dataset.previewUrl;
|
|
254
|
+
preview.removeAttribute('src');
|
|
255
|
+
URL.revokeObjectURL(current);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
167
259
|
if (page.pageImage?.kind === 'blob') {
|
|
168
|
-
const objectUrl = URL.createObjectURL(page.pageImage.blob);
|
|
169
260
|
try {
|
|
170
|
-
preview.
|
|
171
|
-
}
|
|
172
|
-
|
|
261
|
+
await showBlobPreview(preview, page.pageImage.blob);
|
|
262
|
+
} catch {
|
|
263
|
+
showPreviewFallback();
|
|
173
264
|
}
|
|
265
|
+
// ... later, when the preview is replaced or the view is disposed:
|
|
266
|
+
// releaseBlobPreview(preview);
|
|
174
267
|
}
|
|
175
268
|
|
|
176
269
|
if (page.pageImage?.kind === 'data-url') {
|
|
@@ -178,11 +271,50 @@ if (page.pageImage?.kind === 'data-url') {
|
|
|
178
271
|
}
|
|
179
272
|
```
|
|
180
273
|
|
|
181
|
-
|
|
274
|
+
## Plain Text And Local Ingestion
|
|
275
|
+
|
|
276
|
+
`pdfTextToString(content: PdfTextContent): string` is a pure named root utility for already-extracted text. It preserves PDF.js item order and each string verbatim, including supplied spaces and Unicode. It appends exactly one `\n` after every text item with `hasEOL: true`, even an empty or final item. Existing newlines are not deduplicated; trailing whitespace/newlines are retained. Empty or marker-only content returns `''`; marked-content entries are skipped. XFA-style string items without `hasEOL` contribute only their string. Fragmented words are concatenated without guessed spaces.
|
|
277
|
+
|
|
278
|
+
Configure the worker once as shown above. This browser recipe checks a local `File` before allocating its bytes, processes pages serially with 1-based page attribution, and displays only the current page via safe `textContent`:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { PDFReader, pdfTextToString } from '@web-ts-toolkit/pdf-reader';
|
|
282
|
+
|
|
283
|
+
async function ingestLocalPdf(
|
|
284
|
+
file: File,
|
|
285
|
+
output: HTMLPreElement,
|
|
286
|
+
indexPage: (record: { pageNumber: number; text: string }) => void | Promise<void>,
|
|
287
|
+
): Promise<void> {
|
|
288
|
+
const maxSourceBytes = 25_000_000;
|
|
289
|
+
if (file.size > maxSourceBytes) throw new Error('PDF exceeds the local file-size limit.');
|
|
290
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
291
|
+
const reader = new PDFReader(bytes, { limits: { maxSourceBytes, maxDocumentPages: 250 } });
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
await reader.load({ deadlineMs: 15_000 });
|
|
295
|
+
for await (const page of reader.pages({
|
|
296
|
+
includeText: true,
|
|
297
|
+
includePageImage: false,
|
|
298
|
+
includeEmbeddedImages: false,
|
|
299
|
+
})) {
|
|
300
|
+
if (!page.text) throw new Error(`Missing text content for page ${page.pageNumber}.`);
|
|
301
|
+
const text = pdfTextToString(page.text);
|
|
302
|
+
output.textContent = `Page ${page.pageNumber}\n${text}`;
|
|
303
|
+
await indexPage({ pageNumber: page.pageNumber, text });
|
|
304
|
+
}
|
|
305
|
+
} finally {
|
|
306
|
+
await reader.destroy();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Supply an application-local `indexPage` callback (and a document ID when indexing multiple files). Neither the helper nor this recipe uploads document data; storage/retention belongs to that callback. With both image options disabled, the package allocates no page/embedded-image canvases. The helper itself does no parsing, canvas work, or I/O and does not mutate the content.
|
|
312
|
+
|
|
313
|
+
This is not OCR or layout/reading-order reconstruction: scanned pages may yield empty text, and columns or positioned fragments remain in PDF.js-supplied order. PDF.js may already have normalized the strings before returning them. `pages()` yields complete pages, not streaming text chunks; text-item/code-unit limits still run **after PDF.js materializes each page's text**. The helper allocates a new string and imposes no additional limits. Keep extracted content out of `innerHTML`; `textContent` displays it literally.
|
|
182
314
|
|
|
183
315
|
## Performance And Concurrency
|
|
184
316
|
|
|
185
|
-
Page work is serial per reader in the runtime API. A loaded `PDFReader` permits one executing `pages()` iterator or `convert()` call at a time and rejects overlap with `OPERATION_IN_PROGRESS`; it does not queue page operations. PDFR-07 adds a real-browser benchmark under `packages/pdf-reader/benchmark/` that compares the current `pages()` behavior against an external page-level scheduler
|
|
317
|
+
Page work is serial per reader in the runtime API. A loaded `PDFReader` permits one executing `pages()` iterator or `convert()` call at a time and rejects overlap with `OPERATION_IN_PROGRESS`; it does not queue page operations. PDFR-07 adds a real-browser benchmark under `packages/pdf-reader/benchmark/` that compares the current `pages()` behavior against an external page-level scheduler candidate (not a runtime API) with concurrency `2`, a declared reorder window of `2` retained completed-but-unemittable pages, deterministic output ordering, and stage-synchronized abort handling.
|
|
186
318
|
|
|
187
319
|
Run it with:
|
|
188
320
|
|
|
@@ -190,9 +322,9 @@ Run it with:
|
|
|
190
322
|
pnpm --filter @web-ts-toolkit/pdf-reader benchmark
|
|
191
323
|
```
|
|
192
324
|
|
|
193
|
-
The
|
|
325
|
+
The current baseline (2026-09-12, Headless Chromium `151.0.7922.34`, `pdfjs-dist 6.2.108`, 1 warmup + 3 measured repeats) reports conversion-only, load, and load-inclusive times separately, with global simultaneous page/canvas peaks: `long.pdf` converts at a median `220.4 ms` serial vs `176.6 ms` bounded with simultaneous `2/2` ownership and retention within the window, while the bounded path loads a second document (~110–150 ms extra) so load-inclusive totals still favor serial on these small fixtures. `text-heavy.pdf` and `image-heavy.pdf` show modest conversion-only gains with the same doubled ownership. Peaks count PDF.js-internal page proxies and package-owned canvases simultaneously across both documents; PDF.js worker heap and compositor memory are excluded. The 2026-08-19 single-sample numbers are preserved as historical in the benchmark README and must not be compared sample-for-sample.
|
|
194
326
|
|
|
195
|
-
The package still does not expose a concurrency option because that evidence is not yet enough to set a safe browser memory/backpressure contract. See the benchmark README and the PDFR-07 completion evidence for the recorded browser/hardware context and full results.
|
|
327
|
+
The package still does not expose a concurrency option because that evidence is not yet enough to set a safe browser memory/backpressure contract, and no runtime concurrency or Blob-output change is proposed on the basis of these numbers. See the benchmark README and the PDFR-07 completion evidence for the recorded browser/hardware context and full results.
|
|
196
328
|
|
|
197
329
|
Overlapping `pages()` or `convert()` calls on the same reader are newly rejected rather than being allowed to multiply active page/canvas ownership. Sequential reuse after the prior operation finishes is still supported.
|
|
198
330
|
|
|
@@ -236,7 +368,7 @@ const reader = new PDFReader(bytes, {
|
|
|
236
368
|
|
|
237
369
|
Limits reduce accidental or malicious memory pressure but are not a complete sandbox. PDF parsing still occurs in PDF.js and its worker.
|
|
238
370
|
|
|
239
|
-
`maxSourceBytes` is enforced only when the package can know the byte length synchronously, including direct typed arrays, array buffers, number arrays, and `DocumentInitParameters.data`. It does not apply to remote downloads.
|
|
371
|
+
`maxSourceBytes` is enforced only when the package can know the byte length synchronously, including direct typed arrays, array buffers, number arrays, binary strings, and `DocumentInitParameters.data`. Binary strings count one byte per code unit and number arrays one byte per entry, matching PDF.js conversion; oversized arrays are rejected by length without traversing entries. The check runs before `sourcePolicy(...)` and again after approval, so arrays extended or resizable buffers grown during approval still fail before loading. It does not apply to remote downloads.
|
|
240
372
|
|
|
241
373
|
Text and operator limits are enforced after PDF.js returns a complete page text content or operator list, but before the package retains the text result or traverses the operator list. These checks bound package-owned processing and returned results; they cannot prevent PDF.js from initially constructing those PDF.js structures.
|
|
242
374
|
|
|
@@ -246,7 +378,7 @@ Embedded-image limits are enforced before allocating or encoding the next extrac
|
|
|
246
378
|
|
|
247
379
|
## Security Notes
|
|
248
380
|
|
|
249
|
-
- Prefer trusted `Uint8Array` input. PDF.js may transfer ownership of typed-array data to its worker.
|
|
381
|
+
- Prefer trusted `Uint8Array` input. PDF.js may transfer ownership of typed-array data to its worker, detaching your buffer; do not reuse the buffer after `load()` starts, and retry a detached-input failure with fresh bytes. Mutating borrowed byte content after approval is unsupported; only the size is re-validated.
|
|
250
382
|
- Use `sourcePolicy` to reject remote URLs, protocols, origins, credentialed requests, or header-bearing sources before PDF.js starts network work.
|
|
251
383
|
- `configurePdfWorker(...)` mutates PDF.js application-global worker options only when you call it, not at module evaluation. Reconfiguring it clears the previous URL-or-port setting so stale worker globals do not win unexpectedly, but the replacement can affect other PDF.js consumers in the same realm. Use a per-source PDF.js `PDFWorker` for isolation.
|
|
252
384
|
- Keep `withCredentials` disabled unless cross-origin credentials are explicitly required.
|
|
@@ -259,13 +391,15 @@ Embedded-image limits are enforced before allocating or encoding the next extrac
|
|
|
259
391
|
|
|
260
392
|
Embedded-image extraction stays on the package root as an opt-in convenience (`includeEmbeddedImages: true`), but the operator traversal now lives behind one private extractor boundary instead of mixing PDF.js internals into reader lifecycle code.
|
|
261
393
|
|
|
262
|
-
The real-browser fixture suite for the supported `pdfjs-dist` peer minor (`~6.2.108`) characterizes inline images, image XObjects, repeated references, nested save/restore transforms, mirrored transforms, grayscale, RGB, RGBA soft-mask images, browser `ImageBitmap`-backed image objects, and nested form XObjects with their own matrices.
|
|
394
|
+
The real-browser fixture suite for the supported `pdfjs-dist` peer minor (`~6.2.108`) characterizes inline images, image XObjects, repeated references, nested save/restore transforms, mirrored transforms, grayscale, RGB, RGBA soft-mask images, one-bit (`GRAYSCALE_1BPP`) image XObjects with pixel assertions under both `ImageBitmap` and packed-bit paths, browser `ImageBitmap`-backed image objects, and nested form XObjects with their own matrices.
|
|
263
395
|
|
|
264
396
|
Standalone image-mask operators, vector graphics, patterns, and unknown PDF.js image-object layouts remain unsupported. They are skipped with `logger.warn(...)` diagnostics rather than failing the entire page. The extractor never calls `ImageBitmap.close()` because PDF.js owns bitmap lifetime through `page.cleanup()` and document destruction.
|
|
265
397
|
|
|
266
398
|
Individual unsupported images are skipped. Supply `logger.warn` if those diagnostics matter to the application. Resource-limit and cancellation errors are never swallowed.
|
|
267
399
|
|
|
268
|
-
Image `x`, `y`, `width`, and `height` values are PDF user-space coordinates before viewport scaling. The full six-value graphics transform is included when consumers need custom coordinate conversion.
|
|
400
|
+
Image `x`, `y`, `width`, and `height` values are PDF user-space coordinates before viewport scaling. `x` is the left edge and `y` is the upper bound (top) of the transformed image square; `pageWidth`/`pageHeight` report the unscaled page size in the same units. The full six-value graphics transform is included when consumers need custom coordinate conversion. `size` reports the decoded PDF.js image source bytes (`dataLen` when reported, else raw `data.byteLength`), not the encoded PNG data-URL length.
|
|
401
|
+
|
|
402
|
+
Shared images painted on several pages are resolved through PDF.js's document-wide `commonObjs` store (`g_`-prefixed references) with readiness awaited through the same cancellation contract as other page-stage waits; page-local references keep using `page.objs`. Repeated placements on one page still share one encoded PNG string after per-placement aggregate pixel limits pass.
|
|
269
403
|
|
|
270
404
|
## Errors And Cleanup
|
|
271
405
|
|