@web-ts-toolkit/pdf-reader 0.40.1 → 0.41.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 (4) hide show
  1. package/README.md +63 -23
  2. package/index.d.mts +46 -10
  3. package/index.mjs +372 -179
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -8,7 +8,9 @@ Resource-safe browser PDF rendering, text extraction, and best-effort embedded-i
8
8
  pnpm add @web-ts-toolkit/pdf-reader pdfjs-dist
9
9
  ```
10
10
 
11
- `pdfjs-dist` is a peer dependency. This package is browser-oriented, ESM-only, and requires DOM canvas APIs.
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.
12
+
13
+ The supported PDF.js compatibility contract is the `pdfjs-dist` `~6.2.108` peer minor. The package test and real-browser fixture suite run against `6.2.108`; future `6.x` minors are not admitted until rendering and embedded-image compatibility are exercised in a reproducible browser matrix.
12
14
 
13
15
  Canonical imports use named exports from the package root. There is no default export, no supported deep import, and no supported CommonJS `require()` entry.
14
16
 
@@ -16,20 +18,22 @@ Canonical imports use named exports from the package root. There is no default e
16
18
 
17
19
  This follow-up release keeps the small named-export API but intentionally does not preserve the older application-local `PDFReader` names and defaults.
18
20
 
19
- | Area | Previous contract | New contract and migration |
20
- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
- | 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. |
22
- | 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. Use `reader.state` for `new` / `loading` / `loaded` / `iterating` / `failed` / `destroyed`, and call `destroy()` to tear down shared work explicitly. |
23
- | 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. |
24
- | 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 }`. |
25
- | 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. |
21
+ | Area | Previous contract | New contract and migration |
22
+ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | 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. |
24
+ | 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`. |
25
+ | 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. |
26
+ | 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 }`. |
27
+ | 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. |
26
28
 
27
- The repository `CHANGELOG.md` contains the same release-level migration summary.
29
+ 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.
28
30
 
29
31
  ## Worker Setup
30
32
 
31
33
  Configure the PDF.js worker explicitly in your application. The package does not use bundler-specific `?url` imports and does not mutate PDF.js globals merely by being imported.
32
34
 
35
+ `configurePdfWorker(...)` writes PDF.js application-global worker state (`GlobalWorkerOptions.workerSrc` or `workerPort`) for the current JavaScript realm. Use it once at application startup only when every PDF.js consumer can share the same setting. Calling it again replaces the previous global URL-or-port setting and can collide with other direct or transitive PDF.js users.
36
+
33
37
  Vite example:
34
38
 
35
39
  ```ts
@@ -47,7 +51,23 @@ import { configurePdfWorker } from '@web-ts-toolkit/pdf-reader';
47
51
  configurePdfWorker(new Worker(new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url), { type: 'module' }));
48
52
  ```
49
53
 
50
- Worker URL emission differs by bundler. Keep this setup in application code where the bundler owns asset handling. Calling `configurePdfWorker(...)` again replaces the previous package-level worker configuration. If you pass an existing `Worker`, you still own terminating it.
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.
55
+
56
+ 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
+
58
+ ```ts
59
+ import { PDFWorker } from 'pdfjs-dist';
60
+ import { PDFReader } from '@web-ts-toolkit/pdf-reader';
61
+
62
+ const worker = new PDFWorker({ name: 'tenant-a' });
63
+ const reader = new PDFReader({ data: bytes, worker });
64
+
65
+ try {
66
+ await reader.load();
67
+ } finally {
68
+ await reader.destroy();
69
+ }
70
+ ```
51
71
 
52
72
  ## Quick Start
53
73
 
@@ -88,7 +108,7 @@ const reader = new PDFReader(
88
108
  );
89
109
  ```
90
110
 
91
- Call `load()` before `pages()` or `convert()`. Concurrent `load()` callers share one PDF.js loading task, but aborting one caller only rejects that caller; call `destroy()` to tear down the shared load. `destroy()` is idempotent, permanently closes the reader, cancels active renders, and causes in-flight lifecycle work to reject with `DESTROYED`.
111
+ 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. 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. `destroy()` is idempotent, permanently closes the reader, cancels active renders, and causes in-flight lifecycle work to reject with `DESTROYED`. It waits for PDF.js loading/document destruction, but active `pages()` or `convert()` calls finish their own page and canvas cleanup as those operation promises settle; await those operations if you need to observe that cleanup.
92
112
 
93
113
  ## Reader State And Ownership
94
114
 
@@ -98,20 +118,22 @@ Call `load()` before `pages()` or `convert()`. Concurrent `load()` callers share
98
118
  | ----------- | ------------------------------------------------------------------------------------ | ------------------------------- |
99
119
  | `new` | Reader constructed but `load()` has not started. | `loading`, `destroyed` |
100
120
  | `loading` | One shared PDF.js loading task is in flight. | `loaded`, `failed`, `destroyed` |
101
- | `loaded` | The PDF.js document is loaded and no page work is active. | `iterating`, `destroyed` |
102
- | `iterating` | `pages()` or `convert()` currently owns a page proxy, render task, or encoder step. | `loaded`, `destroyed` |
121
+ | `loaded` | The PDF.js document is loaded and no page operation is active. | `iterating`, `destroyed` |
122
+ | `iterating` | One executing `pages()` iterator or `convert()` call owns the reader page operation. | `loaded`, `destroyed` |
103
123
  | `failed` | The most recent load attempt rejected. A later `load()` call starts a fresh attempt. | `loading`, `destroyed` |
104
124
  | `destroyed` | `destroy()` completed or irrevocably won a race. The reader cannot be reused. | none |
105
125
 
106
126
  Ownership and lifetime rules:
107
127
 
108
- - Source bytes: the package passes your `PdfSource` to PDF.js unchanged after `sourcePolicy(...)` approval. PDF.js may transfer typed-array ownership to its worker during `load()`.
109
- - Pages: `pages()` and `convert()` own the live `PDFPageProxy` only while processing that page. The package calls `page.cleanup()` before yielding the result and on every error path.
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()`.
129
+ - 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
+ - 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. Within that operation, the package owns one live `PDFPageProxy` only while processing the current page and calls `page.cleanup()` before yielding the result and on every error path.
110
132
  - Page canvases: the package owns temporary render/encode canvases and always zeroes their dimensions after each settle path.
111
133
  - `Blob` page images: the returned `Blob` belongs to the caller. If you create an object URL, revoke it yourself.
112
134
  - Data URLs: returned strings belong to the caller. They are copies; the package does not retain the temporary canvas after encoding.
113
- - Embedded-image data URLs: these are best-effort PNG copies. Unsupported image operators/layouts are skipped with `logger.warn(...)` diagnostics.
114
- - 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. 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.
135
+ - 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.
115
137
 
116
138
  ## Options
117
139
 
@@ -137,6 +159,8 @@ const pages = await reader.convert({
137
159
  - `includeEmbeddedImages`: inspect the operator stream for raster images; defaults to `false`.
138
160
  - `signal`: cancels active rendering and stops before subsequent expensive operations.
139
161
 
162
+ 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
+
140
164
  `pageImage` is a discriminated union so the output mode and payload cannot contradict each other:
141
165
 
142
166
  ```ts
@@ -158,7 +182,7 @@ This package returns `Blob` objects directly rather than object URLs so callers
158
182
 
159
183
  ## Performance And Concurrency
160
184
 
161
- Page work remains serial in the runtime API. PDFR-07 adds a real-browser benchmark under `packages/pdf-reader/benchmark/` that compares the current `pages()` behavior against an external page-level scheduler with a hard concurrency bound of `2`, deterministic output ordering, and abort handling.
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 with a hard concurrency bound of `2`, deterministic output ordering, and abort handling.
162
186
 
163
187
  Run it with:
164
188
 
@@ -170,9 +194,11 @@ The recorded local run on Headless Chromium `151.0.7922.34` showed bounded overl
170
194
 
171
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.
172
196
 
197
+ 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
+
173
199
  ## Resource Limits
174
200
 
175
- Defaults reject documents above 1,000 pages, rendered pages above 40 megapixels, and copied embedded images above 25 megapixels.
201
+ Defaults reject documents above 1,000 pages, one page's retained text above 50,000 items or 5,000,000 string code units, embedded-image extraction above 100,000 PDF.js operators, rendered pages above 40 megapixels, one copied embedded image above 25 megapixels, one page above 1,000 extracted embedded images, and one page above 100 megapixels of aggregate decoded embedded-image pixels.
176
202
 
177
203
  ```ts
178
204
  import { PDFReader, PdfReaderError } from '@web-ts-toolkit/pdf-reader';
@@ -181,8 +207,13 @@ const reader = new PDFReader(bytes, {
181
207
  limits: {
182
208
  maxSourceBytes: 25_000_000,
183
209
  maxDocumentPages: 250,
210
+ maxTextItems: 10_000,
211
+ maxTextCodeUnits: 1_000_000,
212
+ maxOperatorCount: 20_000,
184
213
  maxCanvasPixels: 20_000_000,
185
214
  maxEmbeddedImagePixels: 10_000_000,
215
+ maxEmbeddedImages: 250,
216
+ maxEmbeddedImagePixelsTotal: 40_000_000,
186
217
  },
187
218
  sourcePolicy(source) {
188
219
  if (source.url && !source.url.startsWith('https://static.example.com/')) {
@@ -207,24 +238,28 @@ Limits reduce accidental or malicious memory pressure but are not a complete san
207
238
 
208
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.
209
240
 
210
- `sourcePolicy(source)` runs before `getDocument()` and can reject remote URLs, protocols, origins, credentials, headers, or any other caller-supplied loading parameters. The original source object still passes through unchanged when the policy allows it, so PDF.js passwords, CMap/font/WASM URLs, and caller-created workers remain available.
241
+ 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
+
243
+ Embedded-image limits are enforced before allocating or encoding the next extracted embedded-image canvas. `maxEmbeddedImagePixels` applies to one copied image, `maxEmbeddedImages` applies to extracted images returned for one page, and `maxEmbeddedImagePixelsTotal` applies to aggregate decoded pixels copied for one page, including repeated references to the same underlying PDF image object. Repeated image XObject references reuse one encoded data URL per page only after these aggregate placement limits pass; inline images are not cached by synthetic keys. Unsafe non-finite or overflowing image dimensions fail closed with a resource-limit error. Exact aggregate encoded PNG data-url bytes are not knowable until after browser canvas encoding, so the package does not claim a separate pre-encoding output-byte cap; use the decoded-pixel limits to bound package allocation and encoding work.
244
+
245
+ `sourcePolicy(source)` runs before `getDocument()` and can reject remote URLs, protocols, origins, credentials, headers, or any other caller-supplied loading parameters. The `source.rawSource` value is an immutable shallow snapshot of the PDF.js parameters that will be used for loading after approval. Top-level getters and later source-object mutation cannot change the approved URL, credential flag, headers reference, password, CMap/font/WASM URLs, or caller-created worker. Opaque objects such as caller-created workers and in-memory data references are preserved rather than recursively cloned.
211
246
 
212
247
  ## Security Notes
213
248
 
214
249
  - Prefer trusted `Uint8Array` input. PDF.js may transfer ownership of typed-array data to its worker.
215
250
  - Use `sourcePolicy` to reject remote URLs, protocols, origins, credentialed requests, or header-bearing sources before PDF.js starts network work.
216
- - `configurePdfWorker(...)` mutates PDF.js 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.
251
+ - `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.
217
252
  - Keep `withCredentials` disabled unless cross-origin credentials are explicitly required.
218
253
  - Use `maxSourceBytes` for synchronously knowable in-memory sources. Remote response-byte limits still belong to application-controlled fetch/range transport because this package does not intercept PDF.js network I/O.
219
254
  - Use `load({ deadlineMs })` or your own `AbortSignal` to bound load wait time. The package clears deadline timers and abort listeners on success, failure, caller abort, and destroy.
220
- - Keep `pdfjs-dist` current because malformed-document defenses belong primarily to PDF.js.
255
+ - Keep `pdfjs-dist` current within the supported `~6.2.108` peer minor because malformed-document defenses belong primarily to PDF.js. Wider PDF.js minors require new package compatibility evidence before use.
221
256
  - Treat extracted text and images as untrusted content. Do not inject text as HTML without escaping or sanitization.
222
257
 
223
258
  ## Embedded Images
224
259
 
225
260
  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.
226
261
 
227
- The real-browser fixture suite for the supported `pdfjs-dist` peer minor (`~5.7.284`) 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.
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.
228
263
 
229
264
  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.
230
265
 
@@ -242,10 +277,15 @@ Validation and resource failures use `PdfReaderError` with stable codes:
242
277
  - `DESTROYED`
243
278
  - `DOCUMENT_NOT_LOADED`
244
279
  - `IMAGE_LIMIT_EXCEEDED`
280
+ - `IMAGE_COUNT_LIMIT_EXCEEDED`
281
+ - `IMAGE_TOTAL_PIXELS_LIMIT_EXCEEDED`
245
282
  - `INVALID_OPTION`
283
+ - `OPERATOR_LIMIT_EXCEEDED`
284
+ - `OPERATION_IN_PROGRESS`
246
285
  - `PAGE_LIMIT_EXCEEDED`
247
286
  - `SOURCE_LIMIT_EXCEEDED`
248
287
  - `SOURCE_POLICY_VIOLATION`
288
+ - `TEXT_LIMIT_EXCEEDED`
249
289
  - `UNSUPPORTED_ENVIRONMENT`
250
290
 
251
291
  Package-owned errors cover invalid options, lifecycle cancellation/destroy/deadline states, resource limits, source-policy/known-byte rejections, and unsupported canvas/runtime features such as missing 2D contexts or missing `toBlob()` support.
package/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as pdfjs_dist from 'pdfjs-dist';
2
2
  import { PDFDocumentProxy, PDFPageProxy, PageViewport } from 'pdfjs-dist';
3
3
 
4
- type PdfReaderErrorCode = 'ABORTED' | 'CANVAS_LIMIT_EXCEEDED' | 'DEADLINE_EXCEEDED' | 'DESTROYED' | 'DOCUMENT_NOT_LOADED' | 'IMAGE_LIMIT_EXCEEDED' | 'INVALID_OPTION' | 'PAGE_LIMIT_EXCEEDED' | 'SOURCE_LIMIT_EXCEEDED' | 'SOURCE_POLICY_VIOLATION' | 'UNSUPPORTED_ENVIRONMENT';
4
+ type PdfReaderErrorCode = 'ABORTED' | 'CANVAS_LIMIT_EXCEEDED' | 'DEADLINE_EXCEEDED' | 'DESTROYED' | 'DOCUMENT_NOT_LOADED' | 'IMAGE_LIMIT_EXCEEDED' | 'IMAGE_COUNT_LIMIT_EXCEEDED' | 'IMAGE_TOTAL_PIXELS_LIMIT_EXCEEDED' | 'INVALID_OPTION' | 'OPERATOR_LIMIT_EXCEEDED' | 'OPERATION_IN_PROGRESS' | 'PAGE_LIMIT_EXCEEDED' | 'SOURCE_LIMIT_EXCEEDED' | 'SOURCE_POLICY_VIOLATION' | 'TEXT_LIMIT_EXCEEDED' | 'UNSUPPORTED_ENVIRONMENT';
5
5
  /** An actionable package error with a stable machine-readable code. */
6
6
  declare class PdfReaderError extends Error {
7
7
  readonly code: PdfReaderErrorCode;
@@ -36,17 +36,27 @@ interface PdfReaderLimits {
36
36
  maxSourceBytes?: number;
37
37
  /** Maximum number of document pages accepted after loading. Defaults to 1,000. */
38
38
  maxDocumentPages?: number;
39
+ /** Maximum PDF.js text items retained for one page. Defaults to 50,000. */
40
+ maxTextItems?: number;
41
+ /** Maximum string code units retained across one page's text items. Defaults to 5,000,000. */
42
+ maxTextCodeUnits?: number;
43
+ /** Maximum PDF.js operators traversed for embedded-image extraction on one page. Defaults to 100,000. */
44
+ maxOperatorCount?: number;
39
45
  /** Maximum pixels allocated for one rendered page. Defaults to 40 megapixels. */
40
46
  maxCanvasPixels?: number;
41
47
  /** Maximum pixels allocated while copying one embedded image. Defaults to 25 megapixels. */
42
48
  maxEmbeddedImagePixels?: number;
49
+ /** Maximum extracted embedded images returned for one page. Defaults to 1,000. */
50
+ maxEmbeddedImages?: number;
51
+ /** Maximum decoded embedded-image pixels copied across one page. Defaults to 100 megapixels. */
52
+ maxEmbeddedImagePixelsTotal?: number;
43
53
  }
44
54
  interface PdfReaderLogger {
45
55
  warn(message: string, error?: unknown): void;
46
56
  }
47
57
  interface PdfReaderSourceInfo {
48
- /** The original source value that will be passed to PDF.js if policy allows it. */
49
- rawSource: PdfSource;
58
+ /** Immutable shallow snapshot that will be passed to PDF.js if policy allows it. */
59
+ rawSource: Readonly<PdfDocumentInitParameters>;
50
60
  /** Broad source shape so callers can distinguish direct bytes from URL-based inputs. */
51
61
  kind: 'bytes' | 'url' | 'document-init-parameters';
52
62
  /** Normalized URL string when one is present on the source. */
@@ -60,9 +70,9 @@ interface PdfReaderSourceInfo {
60
70
  /** Whether PDF.js would send credentials for URL-based loading. */
61
71
  withCredentials: boolean;
62
72
  }
63
- type PdfReaderSourcePolicy = (source: PdfReaderSourceInfo) => void | Promise<void>;
73
+ type PdfReaderSourcePolicy = (source: PdfReaderSourceInfo) => void | PromiseLike<void>;
64
74
  interface PdfReaderOptions {
65
- /** Canvas boundary injectable for tests or non-DOM canvas implementations. */
75
+ /** Creates a fresh DOM `HTMLCanvasElement` for each package-owned page or embedded-image render. */
66
76
  canvasFactory?: () => HTMLCanvasElement;
67
77
  /** Optional warning sink. The package never writes directly to the console. */
68
78
  logger?: PdfReaderLogger;
@@ -127,7 +137,21 @@ interface PageResult {
127
137
  /** Best-effort embedded raster images extracted from the current page. */
128
138
  images: ExtractedImage[];
129
139
  }
140
+ /**
141
+ * Borrowed PDF.js document proxy returned by `PDFReader.load()`.
142
+ *
143
+ * The reader owns lifecycle teardown. Callers may inspect metadata and call
144
+ * supported PDF.js read methods, but must not call `destroy()` on this proxy
145
+ * while the `PDFReader` owns it. Use `PDFReader.destroy()` instead.
146
+ */
130
147
  type LoadedPdfDocument = PDFDocumentProxy;
148
+ /**
149
+ * PDF.js page proxy alias for consumers interoperating with PDF.js APIs.
150
+ *
151
+ * `PDFReader.pages()` returns package `PageResult` objects, not raw page
152
+ * proxies. This alias is exported so applications can type adjacent PDF.js
153
+ * integration code without relying on unsupported package deep imports.
154
+ */
131
155
  type LoadedPdfPage = PDFPageProxy;
132
156
 
133
157
  /** Browser PDF reader with bounded canvas allocation and deterministic cleanup. */
@@ -137,7 +161,14 @@ declare class PDFReader {
137
161
  get numPages(): number | undefined;
138
162
  /** Current lifecycle state for load, iteration/rendering, retry, and teardown boundaries. */
139
163
  get state(): PdfReaderState;
140
- /** Loads once and returns the underlying PDF.js document proxy. */
164
+ /**
165
+ * Loads once and returns the borrowed PDF.js document proxy.
166
+ *
167
+ * The returned proxy remains owned by this reader. Callers may inspect it and
168
+ * use supported PDF.js read methods, but must not call `destroy()` on the
169
+ * proxy while the reader owns lifecycle teardown. Call `reader.destroy()` to
170
+ * release document and worker resources.
171
+ */
141
172
  load(signal?: AbortSignal): Promise<PDFDocumentProxy>;
142
173
  load(options?: LoadOptions): Promise<PDFDocumentProxy>;
143
174
  /** Streams page results so callers do not need to retain the entire document conversion in memory. */
@@ -183,11 +214,16 @@ type PdfUrlSource = PdfUrlSourceOptions & {
183
214
  declare function pdfUrlSource(url: string | URL, options?: PdfUrlSourceOptions): PdfUrlSource;
184
215
 
185
216
  /**
186
- * Configures PDF.js without bundler-specific imports or package-load side effects.
187
- * Pass either a worker URL emitted by your bundler or an existing module Worker.
217
+ * Configures PDF.js application-global worker state.
218
+ *
219
+ * Use this once at application startup when every PDF.js consumer can share the
220
+ * same worker configuration. Calling it again replaces the previous global
221
+ * worker URL or port and can collide with other PDF.js consumers in the same
222
+ * JavaScript realm. For isolation, pass a caller-created PDF.js `PDFWorker` on
223
+ * an individual `DocumentInitParameters` source instead of using this helper.
188
224
  *
189
- * Calling this again replaces the previous configuration. When passing an
190
- * existing `Worker`, the caller retains ownership of terminating it.
225
+ * When passing an existing DOM `Worker`, the caller retains ownership of
226
+ * terminating it.
191
227
  */
192
228
  declare function configurePdfWorker(worker: string | Worker): void;
193
229
 
package/index.mjs CHANGED
@@ -42,94 +42,128 @@ function getTransformedUnitBounds(transform) {
42
42
 
43
43
  // src/embeddedImages.ts
44
44
  var identityTransform = [1, 0, 0, 1, 0, 0];
45
- var propagatedErrorCodes = /* @__PURE__ */ new Set(["ABORTED", "DESTROYED", "IMAGE_LIMIT_EXCEEDED"]);
45
+ var propagatedErrorCodes = /* @__PURE__ */ new Set([
46
+ "ABORTED",
47
+ "DESTROYED",
48
+ "IMAGE_LIMIT_EXCEEDED",
49
+ "IMAGE_COUNT_LIMIT_EXCEEDED",
50
+ "IMAGE_TOTAL_PIXELS_LIMIT_EXCEEDED",
51
+ "OPERATOR_LIMIT_EXCEEDED"
52
+ ]);
46
53
  async function extractEmbeddedImages(page, viewport, options) {
47
54
  const operators = await page.getOperatorList();
55
+ enforceOperatorLimit(operators.fnArray.length, options.maxOperators);
48
56
  const images = [];
49
57
  const stack = [];
50
58
  let transform = identityTransform;
51
- for (let index = 0; index < operators.fnArray.length; index += 1) {
52
- options.throwIfDestroyed();
53
- options.throwIfAborted(options.signal);
54
- const operation = operators.fnArray[index];
55
- if (operation === OPS.save) {
56
- stack.push([...transform]);
57
- continue;
58
- }
59
- if (operation === OPS.restore || operation === OPS.paintFormXObjectEnd) {
60
- transform = stack.pop() ?? identityTransform;
61
- continue;
62
- }
63
- if (operation === OPS.transform) {
64
- const next = readTransformArgs(operators.argsArray[index]);
65
- if (next) transform = Util.transform(transform, next);
66
- continue;
67
- }
68
- if (operation === OPS.paintFormXObjectBegin) {
69
- stack.push([...transform]);
70
- const next = readTransformMatrix(operators.argsArray[index]?.[0]);
71
- if (next) transform = Util.transform(transform, next);
72
- continue;
73
- }
74
- if (operation === OPS.paintImageMaskXObject) {
75
- warn(options.logger, "Skipped embedded image operator paintImageMaskXObject: image masks are not supported.");
76
- continue;
77
- }
78
- if (operation !== OPS.paintXObject && operation !== OPS.paintImageXObject && operation !== OPS.paintInlineImageXObject) {
79
- continue;
80
- }
81
- try {
82
- const paintedImage = await resolvePaintedImage(page, operation, operators.argsArray[index], index);
83
- if (!paintedImage) continue;
84
- const dataUrl = imageToDataUrl(paintedImage.image, options);
85
- if (!dataUrl) {
86
- warn(
87
- options.logger,
88
- `Skipped embedded image ${paintedImage.label}: unsupported PDF.js image shape or data layout.`
89
- );
90
- continue;
91
- }
92
- const bounds = getTransformedUnitBounds(transform);
93
- images.push({
94
- dataUrl,
95
- x: bounds.left,
96
- y: bounds.top,
97
- width: bounds.width,
98
- height: bounds.height,
99
- size: imageByteLength(paintedImage.image),
100
- mimeType: "image/png",
101
- pageWidth: viewport.width / viewport.scale,
102
- pageHeight: viewport.height / viewport.scale,
103
- transform: [...transform]
104
- });
105
- } catch (error) {
59
+ let totalPixels = 0;
60
+ const encodedXObjects = /* @__PURE__ */ new Map();
61
+ try {
62
+ for (let index = 0; index < operators.fnArray.length; index += 1) {
106
63
  options.throwIfDestroyed();
107
64
  options.throwIfAborted(options.signal);
108
- if (shouldPropagateEmbeddedImageError(error)) throw error;
109
- const label = readOperationLabel(operation, operators.argsArray[index], index);
110
- warn(options.logger, `Failed to extract embedded image ${label}.`, error);
65
+ const operation = operators.fnArray[index];
66
+ if (operation === OPS.save) {
67
+ stack.push([...transform]);
68
+ continue;
69
+ }
70
+ if (operation === OPS.restore || operation === OPS.paintFormXObjectEnd) {
71
+ transform = stack.pop() ?? identityTransform;
72
+ continue;
73
+ }
74
+ if (operation === OPS.transform) {
75
+ const next = readTransformArgs(operators.argsArray[index]);
76
+ if (next) transform = Util.transform(transform, next);
77
+ continue;
78
+ }
79
+ if (operation === OPS.paintFormXObjectBegin) {
80
+ stack.push([...transform]);
81
+ const next = readTransformMatrix(operators.argsArray[index]?.[0]);
82
+ if (next) transform = Util.transform(transform, next);
83
+ continue;
84
+ }
85
+ if (operation === OPS.paintImageMaskXObject) {
86
+ warn(options.logger, "Skipped embedded image operator paintImageMaskXObject: image masks are not supported.");
87
+ continue;
88
+ }
89
+ if (operation !== OPS.paintXObject && operation !== OPS.paintImageXObject && operation !== OPS.paintInlineImageXObject) {
90
+ continue;
91
+ }
92
+ try {
93
+ const args = operators.argsArray[index];
94
+ const reference = operation === OPS.paintInlineImageXObject ? void 0 : readImageReference(args);
95
+ let encoded = reference ? encodedXObjects.get(reference) : void 0;
96
+ if (encoded) {
97
+ enforceNextImageLimits(images.length, totalPixels, encoded.dimensions.pixels, options);
98
+ } else {
99
+ const paintedImage = await resolvePaintedImage(page, operation, args, index);
100
+ options.throwIfDestroyed();
101
+ options.throwIfAborted(options.signal);
102
+ if (!paintedImage) continue;
103
+ const dimensions = readImageDimensions(paintedImage.image, options.maxPixels);
104
+ if (!dimensions || !isSupportedImageSource(paintedImage.image)) {
105
+ warn(
106
+ options.logger,
107
+ `Skipped embedded image ${paintedImage.label}: unsupported PDF.js image shape or data layout.`
108
+ );
109
+ continue;
110
+ }
111
+ enforceNextImageLimits(images.length, totalPixels, dimensions.pixels, options);
112
+ const dataUrl = imageToDataUrl(paintedImage.image, dimensions, options);
113
+ if (!dataUrl) {
114
+ warn(
115
+ options.logger,
116
+ `Skipped embedded image ${paintedImage.label}: unsupported PDF.js image shape or data layout.`
117
+ );
118
+ continue;
119
+ }
120
+ encoded = { dataUrl, dimensions, size: imageByteLength(paintedImage.image) };
121
+ if (paintedImage.reference) encodedXObjects.set(paintedImage.reference, encoded);
122
+ }
123
+ totalPixels += encoded.dimensions.pixels;
124
+ const bounds = getTransformedUnitBounds(transform);
125
+ images.push({
126
+ dataUrl: encoded.dataUrl,
127
+ x: bounds.left,
128
+ y: bounds.top,
129
+ width: bounds.width,
130
+ height: bounds.height,
131
+ size: encoded.size,
132
+ mimeType: "image/png",
133
+ pageWidth: viewport.width / viewport.scale,
134
+ pageHeight: viewport.height / viewport.scale,
135
+ transform: [...transform]
136
+ });
137
+ } catch (error) {
138
+ options.throwIfDestroyed();
139
+ options.throwIfAborted(options.signal);
140
+ if (shouldPropagateEmbeddedImageError(error)) throw error;
141
+ const label = readOperationLabel(operation, operators.argsArray[index], index);
142
+ warn(options.logger, `Failed to extract embedded image ${label}.`, error);
143
+ }
111
144
  }
145
+ } finally {
146
+ encodedXObjects.clear();
112
147
  }
113
148
  return images;
114
149
  }
150
+ function readImageReference(args) {
151
+ const reference = Array.isArray(args) ? args[0] : void 0;
152
+ return typeof reference === "string" ? reference : void 0;
153
+ }
115
154
  async function resolvePaintedImage(page, operation, args, index) {
116
155
  if (operation === OPS.paintInlineImageXObject) {
117
156
  const inlineImage = Array.isArray(args) ? args[0] : void 0;
118
157
  if (!inlineImage || typeof inlineImage !== "object") return void 0;
119
158
  return { image: inlineImage, label: `inline@${index}` };
120
159
  }
121
- const reference = Array.isArray(args) ? args[0] : void 0;
122
- if (typeof reference !== "string") return void 0;
160
+ const reference = readImageReference(args);
161
+ if (!reference) return void 0;
123
162
  const image = await page.objs.get(reference);
124
- return { image, label: reference };
163
+ return { image, label: reference, reference };
125
164
  }
126
- function imageToDataUrl(image, options) {
127
- const width = image.width;
128
- const height = image.height;
129
- if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) {
130
- return void 0;
131
- }
132
- const canvas = allocateCanvas(width, height, options.maxPixels, options.createCanvas);
165
+ function imageToDataUrl(image, dimensions, options) {
166
+ const canvas = allocateCanvas(dimensions.width, dimensions.height, options.maxPixels, options.createCanvas);
133
167
  try {
134
168
  const context = canvas.getContext("2d");
135
169
  if (!context) return void 0;
@@ -139,9 +173,9 @@ function imageToDataUrl(image, options) {
139
173
  }
140
174
  if (!ArrayBuffer.isView(image.data)) return void 0;
141
175
  const source = new Uint8ClampedArray(image.data.buffer, image.data.byteOffset, image.data.byteLength);
142
- const rgba = toRgba(source, width, height);
176
+ const rgba = toRgba(source, dimensions);
143
177
  if (!rgba) return void 0;
144
- const pixels = context.createImageData(width, height);
178
+ const pixels = context.createImageData(dimensions.width, dimensions.height);
145
179
  pixels.data.set(rgba);
146
180
  context.putImageData(pixels, 0, 0);
147
181
  return canvas.toDataURL("image/png");
@@ -149,13 +183,17 @@ function imageToDataUrl(image, options) {
149
183
  releaseCanvas(canvas);
150
184
  }
151
185
  }
152
- function toRgba(data, width, height) {
153
- const pixels = width * height;
154
- if (data.length === pixels * 4) return data;
155
- if (data.length !== pixels && data.length !== pixels * 3) return void 0;
156
- const channels = data.length / pixels;
157
- const rgba = new Uint8ClampedArray(pixels * 4);
158
- for (let pixel = 0; pixel < pixels; pixel += 1) {
186
+ function toRgba(data, dimensions) {
187
+ const rgbaLength = multiplySafe(dimensions.pixels, 4);
188
+ const rgbLength = multiplySafe(dimensions.pixels, 3);
189
+ if (rgbaLength === void 0 || rgbLength === void 0) {
190
+ throw new PdfReaderError("IMAGE_LIMIT_EXCEEDED", "embedded image has unsafe decoded pixel dimensions.");
191
+ }
192
+ if (data.length === rgbaLength) return data;
193
+ if (data.length !== dimensions.pixels && data.length !== rgbLength) return void 0;
194
+ const channels = data.length / dimensions.pixels;
195
+ const rgba = new Uint8ClampedArray(rgbaLength);
196
+ for (let pixel = 0; pixel < dimensions.pixels; pixel += 1) {
159
197
  const input = pixel * channels;
160
198
  const output = pixel * 4;
161
199
  rgba[output] = data[input];
@@ -165,11 +203,52 @@ function toRgba(data, width, height) {
165
203
  }
166
204
  return rgba;
167
205
  }
206
+ function enforceOperatorLimit(count, limit) {
207
+ if (!Number.isSafeInteger(count) || count > limit) {
208
+ throw new PdfReaderError("OPERATOR_LIMIT_EXCEEDED", `PDF page has ${count} operators; limit is ${limit}.`);
209
+ }
210
+ }
211
+ function enforceNextImageLimits(currentImageCount, currentTotalPixels, nextPixels, options) {
212
+ const nextImageCount = currentImageCount + 1;
213
+ if (!Number.isSafeInteger(nextImageCount) || nextImageCount > options.maxImages) {
214
+ throw new PdfReaderError(
215
+ "IMAGE_COUNT_LIMIT_EXCEEDED",
216
+ `PDF page has more than ${options.maxImages} extractable embedded images.`
217
+ );
218
+ }
219
+ const nextTotalPixels = currentTotalPixels + nextPixels;
220
+ if (!Number.isSafeInteger(nextTotalPixels) || nextTotalPixels > options.maxTotalPixels) {
221
+ throw new PdfReaderError(
222
+ "IMAGE_TOTAL_PIXELS_LIMIT_EXCEEDED",
223
+ `PDF page embedded images require more than ${options.maxTotalPixels} decoded pixels.`
224
+ );
225
+ }
226
+ }
227
+ function readImageDimensions(image, limit) {
228
+ const { width, height } = image;
229
+ if (width === void 0 || height === void 0) return void 0;
230
+ if (typeof width !== "number" || typeof height !== "number") return void 0;
231
+ const pixelWidth = Math.ceil(width);
232
+ const pixelHeight = Math.ceil(height);
233
+ const pixels = multiplySafe(pixelWidth, pixelHeight);
234
+ if (pixelWidth <= 0 || pixelHeight <= 0 || pixels === void 0 || pixels > limit) {
235
+ throw new PdfReaderError("IMAGE_LIMIT_EXCEEDED", `embedded image requires ${pixels} pixels; limit is ${limit}.`);
236
+ }
237
+ return { width: pixelWidth, height: pixelHeight, pixels };
238
+ }
239
+ function isSupportedImageSource(image) {
240
+ return typeof ImageBitmap !== "undefined" && image.bitmap instanceof ImageBitmap || ArrayBuffer.isView(image.data);
241
+ }
242
+ function multiplySafe(left, right) {
243
+ if (!Number.isSafeInteger(left) || !Number.isSafeInteger(right)) return void 0;
244
+ const product = left * right;
245
+ return Number.isSafeInteger(product) ? product : void 0;
246
+ }
168
247
  function allocateCanvas(width, height, limit, createCanvas) {
169
248
  const pixelWidth = Math.ceil(width);
170
249
  const pixelHeight = Math.ceil(height);
171
- const pixels = pixelWidth * pixelHeight;
172
- if (!Number.isSafeInteger(pixels) || pixels > limit) {
250
+ const pixels = multiplySafe(pixelWidth, pixelHeight);
251
+ if (pixels === void 0 || pixels > limit) {
173
252
  throw new PdfReaderError("IMAGE_LIMIT_EXCEEDED", `embedded image requires ${pixels} pixels; limit is ${limit}.`);
174
253
  }
175
254
  const canvas = createCanvas();
@@ -266,10 +345,15 @@ function validatePageRange(pageRange) {
266
345
  // src/PDFReader.ts
267
346
  var defaultLimits = {
268
347
  maxDocumentPages: 1e3,
348
+ maxTextItems: 5e4,
349
+ maxTextCodeUnits: 5e6,
350
+ maxOperatorCount: 1e5,
269
351
  maxCanvasPixels: 4e7,
270
- maxEmbeddedImagePixels: 25e6
352
+ maxEmbeddedImagePixels: 25e6,
353
+ maxEmbeddedImages: 1e3,
354
+ maxEmbeddedImagePixelsTotal: 1e8
271
355
  };
272
- var _source, _createCanvas, _logger, _limits, _sourcePolicy, _loadingState, _loadingTask, _document, _destroyPromise, _activeRenderTasks, _activePageWorkCount, _destroyed, _lastLoadFailed, _PDFReader_instances, load_fn, processPage_fn, encodePageImage_fn, resolveViewport_fn, encodeCanvasToBlob_fn, allocateCanvas_fn, waitForRender_fn, releaseCanvas_fn, getOrCreateLoadState_fn, awaitWithSignal_fn, createAbortedError_fn, createDestroyedError_fn, createDeadlineExceededError_fn, createUnsupportedEnvironmentError_fn, rethrowLifecycleError_fn, throwIfDestroyed_fn, throwIfAborted_fn, resolveLoadOptions_fn, isAbortSignal_fn, enforceSourcePolicy_fn, isPromiseLike_fn, inspectSource_fn, toPdfJsSource_fn, isPdfTypedArray_fn, knownByteLength_fn, readUrl_fn, readHttpHeaders_fn, validateLimits_fn;
356
+ var _source, _createCanvas, _logger, _limits, _sourcePolicy, _loadingState, _loadingTask, _document, _destroyPromise, _destroyController, _activeRenderTasks, _activePageOperation, _activePageWorkCount, _destroyed, _lastLoadFailed, _PDFReader_instances, load_fn, processPage_fn, enforceTextLimits_fn, encodePageImage_fn, resolveViewport_fn, encodeCanvasToBlob_fn, allocateCanvas_fn, waitForRender_fn, releaseCanvas_fn, acquirePageOperation_fn, releasePageOperation_fn, getOrCreateLoadState_fn, destroyLoadStateTask_fn, awaitWithSignal_fn, awaitWithDestroy_fn, createAbortedError_fn, createDestroyedError_fn, createDeadlineExceededError_fn, createUnsupportedEnvironmentError_fn, rethrowLifecycleError_fn, throwIfDestroyed_fn, throwIfAborted_fn, resolveLoadOptions_fn, isAbortSignal_fn, enforceSourcePolicy_fn, createSourceSnapshot_fn, inspectSource_fn, sourceKind_fn, toPdfJsSource_fn, freezeSource_fn, isPdfTypedArray_fn, knownByteLength_fn, readUrl_fn, readHttpHeaders_fn, validateLimits_fn;
273
357
  var PDFReader = class {
274
358
  constructor(source, options = {}) {
275
359
  __privateAdd(this, _PDFReader_instances);
@@ -282,7 +366,9 @@ var PDFReader = class {
282
366
  __privateAdd(this, _loadingTask);
283
367
  __privateAdd(this, _document);
284
368
  __privateAdd(this, _destroyPromise);
369
+ __privateAdd(this, _destroyController, new AbortController());
285
370
  __privateAdd(this, _activeRenderTasks, /* @__PURE__ */ new Set());
371
+ __privateAdd(this, _activePageOperation, false);
286
372
  __privateAdd(this, _activePageWorkCount, 0);
287
373
  __privateAdd(this, _destroyed, false);
288
374
  __privateAdd(this, _lastLoadFailed, false);
@@ -299,7 +385,7 @@ var PDFReader = class {
299
385
  /** Current lifecycle state for load, iteration/rendering, retry, and teardown boundaries. */
300
386
  get state() {
301
387
  if (__privateGet(this, _destroyed)) return "destroyed";
302
- if (__privateGet(this, _activePageWorkCount) > 0) return "iterating";
388
+ if (__privateGet(this, _activePageOperation) || __privateGet(this, _activePageWorkCount) > 0) return "iterating";
303
389
  if (__privateGet(this, _document)) return "loaded";
304
390
  if (__privateGet(this, _loadingState)) return "loading";
305
391
  if (__privateGet(this, _lastLoadFailed)) return "failed";
@@ -314,37 +400,43 @@ var PDFReader = class {
314
400
  }
315
401
  /** Streams page results so callers do not need to retain the entire document conversion in memory. */
316
402
  async *pages(options = {}) {
317
- __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
318
- const documentProxy = __privateGet(this, _document);
319
- if (!documentProxy) {
320
- throw new PdfReaderError("DOCUMENT_NOT_LOADED", "Document not loaded. Call load() first.");
321
- }
322
- const resolved = resolveConvertOptions(options);
323
- const [start, end] = resolvePageNumbers(resolved.pageRange, documentProxy.numPages);
324
- if (start > documentProxy.numPages) {
325
- throw new PdfReaderError("INVALID_OPTION", `pageRange starts after the last page (${documentProxy.numPages}).`);
326
- }
327
- for (let pageNumber = start; pageNumber <= Math.min(end, documentProxy.numPages); pageNumber += 1) {
328
- __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
329
- __privateMethod(this, _PDFReader_instances, throwIfAborted_fn).call(this, resolved.signal);
330
- let page;
331
- let ownsActivePageWork = false;
332
- try {
333
- __privateSet(this, _activePageWorkCount, __privateGet(this, _activePageWorkCount) + 1);
334
- ownsActivePageWork = true;
335
- page = await documentProxy.getPage(pageNumber);
336
- const result = await __privateMethod(this, _PDFReader_instances, processPage_fn).call(this, page, pageNumber, documentProxy.numPages, resolved);
337
- page.cleanup();
338
- page = void 0;
339
- __privateSet(this, _activePageWorkCount, __privateGet(this, _activePageWorkCount) - 1);
340
- ownsActivePageWork = false;
341
- yield result;
342
- } catch (error) {
343
- __privateMethod(this, _PDFReader_instances, rethrowLifecycleError_fn).call(this, error, resolved.signal);
344
- } finally {
345
- if (ownsActivePageWork) __privateSet(this, _activePageWorkCount, __privateGet(this, _activePageWorkCount) - 1);
346
- page?.cleanup();
403
+ let ownsPageOperation = false;
404
+ try {
405
+ __privateMethod(this, _PDFReader_instances, acquirePageOperation_fn).call(this);
406
+ ownsPageOperation = true;
407
+ const documentProxy = __privateGet(this, _document);
408
+ if (!documentProxy) {
409
+ throw new PdfReaderError("DOCUMENT_NOT_LOADED", "Document not loaded. Call load() first.");
410
+ }
411
+ const resolved = resolveConvertOptions(options);
412
+ const [start, end] = resolvePageNumbers(resolved.pageRange, documentProxy.numPages);
413
+ if (start > documentProxy.numPages) {
414
+ throw new PdfReaderError("INVALID_OPTION", `pageRange starts after the last page (${documentProxy.numPages}).`);
415
+ }
416
+ for (let pageNumber = start; pageNumber <= Math.min(end, documentProxy.numPages); pageNumber += 1) {
417
+ __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
418
+ __privateMethod(this, _PDFReader_instances, throwIfAborted_fn).call(this, resolved.signal);
419
+ let page;
420
+ let ownsActivePageWork = false;
421
+ try {
422
+ __privateSet(this, _activePageWorkCount, __privateGet(this, _activePageWorkCount) + 1);
423
+ ownsActivePageWork = true;
424
+ page = await documentProxy.getPage(pageNumber);
425
+ const result = await __privateMethod(this, _PDFReader_instances, processPage_fn).call(this, page, pageNumber, documentProxy.numPages, resolved);
426
+ page.cleanup();
427
+ page = void 0;
428
+ __privateSet(this, _activePageWorkCount, __privateGet(this, _activePageWorkCount) - 1);
429
+ ownsActivePageWork = false;
430
+ yield result;
431
+ } catch (error) {
432
+ __privateMethod(this, _PDFReader_instances, rethrowLifecycleError_fn).call(this, error, resolved.signal);
433
+ } finally {
434
+ if (ownsActivePageWork) __privateSet(this, _activePageWorkCount, __privateGet(this, _activePageWorkCount) - 1);
435
+ page?.cleanup();
436
+ }
347
437
  }
438
+ } finally {
439
+ if (ownsPageOperation) __privateMethod(this, _PDFReader_instances, releasePageOperation_fn).call(this);
348
440
  }
349
441
  }
350
442
  /** Collects `pages()` into an array. Prefer `pages()` for large documents. */
@@ -357,6 +449,7 @@ var PDFReader = class {
357
449
  async destroy() {
358
450
  if (__privateGet(this, _destroyPromise)) return __privateGet(this, _destroyPromise);
359
451
  __privateSet(this, _destroyed, true);
452
+ __privateGet(this, _destroyController).abort();
360
453
  const loadingTask = __privateGet(this, _loadingTask);
361
454
  const loadingState = __privateGet(this, _loadingState);
362
455
  __privateSet(this, _loadingTask, void 0);
@@ -370,8 +463,7 @@ var PDFReader = class {
370
463
  }
371
464
  if (!loadingState) return;
372
465
  loadingState.destroyed = true;
373
- loadingState.destroyPromise ?? (loadingState.destroyPromise = loadingState.task ? loadingState.task.destroy() : Promise.resolve());
374
- await loadingState.destroyPromise;
466
+ await __privateMethod(this, _PDFReader_instances, destroyLoadStateTask_fn).call(this, loadingState);
375
467
  })());
376
468
  await __privateGet(this, _destroyPromise);
377
469
  }
@@ -385,7 +477,9 @@ _loadingState = new WeakMap();
385
477
  _loadingTask = new WeakMap();
386
478
  _document = new WeakMap();
387
479
  _destroyPromise = new WeakMap();
480
+ _destroyController = new WeakMap();
388
481
  _activeRenderTasks = new WeakMap();
482
+ _activePageOperation = new WeakMap();
389
483
  _activePageWorkCount = new WeakMap();
390
484
  _destroyed = new WeakMap();
391
485
  _lastLoadFailed = new WeakMap();
@@ -416,7 +510,9 @@ processPage_fn = async function(page, pageNumber, numPages, options) {
416
510
  __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
417
511
  if (options.includeText) {
418
512
  try {
419
- result.text = await page.getTextContent();
513
+ const text = await page.getTextContent();
514
+ __privateMethod(this, _PDFReader_instances, enforceTextLimits_fn).call(this, text);
515
+ result.text = text;
420
516
  } catch (error) {
421
517
  __privateMethod(this, _PDFReader_instances, rethrowLifecycleError_fn).call(this, error, options.signal);
422
518
  }
@@ -429,6 +525,9 @@ processPage_fn = async function(page, pageNumber, numPages, options) {
429
525
  signal: options.signal,
430
526
  createCanvas: __privateGet(this, _createCanvas),
431
527
  maxPixels: __privateGet(this, _limits).maxEmbeddedImagePixels,
528
+ maxImages: __privateGet(this, _limits).maxEmbeddedImages,
529
+ maxTotalPixels: __privateGet(this, _limits).maxEmbeddedImagePixelsTotal,
530
+ maxOperators: __privateGet(this, _limits).maxOperatorCount,
432
531
  logger: __privateGet(this, _logger),
433
532
  throwIfAborted: (signal) => __privateMethod(this, _PDFReader_instances, throwIfAborted_fn).call(this, signal),
434
533
  throwIfDestroyed: () => __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this)
@@ -455,6 +554,30 @@ processPage_fn = async function(page, pageNumber, numPages, options) {
455
554
  }
456
555
  return result;
457
556
  };
557
+ enforceTextLimits_fn = function(text) {
558
+ const items = text.items;
559
+ if (!Array.isArray(items) || !Number.isSafeInteger(items.length)) {
560
+ throw new PdfReaderError("TEXT_LIMIT_EXCEEDED", "PDF text content has an unsafe item count.");
561
+ }
562
+ if (items.length > __privateGet(this, _limits).maxTextItems) {
563
+ throw new PdfReaderError(
564
+ "TEXT_LIMIT_EXCEEDED",
565
+ `PDF page has ${items.length} text items; limit is ${__privateGet(this, _limits).maxTextItems}.`
566
+ );
567
+ }
568
+ let codeUnits = 0;
569
+ for (const item of items) {
570
+ const value = item && typeof item === "object" ? item.str : void 0;
571
+ if (typeof value !== "string") continue;
572
+ codeUnits += value.length;
573
+ if (!Number.isSafeInteger(codeUnits) || codeUnits > __privateGet(this, _limits).maxTextCodeUnits) {
574
+ throw new PdfReaderError(
575
+ "TEXT_LIMIT_EXCEEDED",
576
+ `PDF page text has more than ${__privateGet(this, _limits).maxTextCodeUnits} string code units.`
577
+ );
578
+ }
579
+ }
580
+ };
458
581
  encodePageImage_fn = async function(canvas, mimeType, output, jpegQuality, signal) {
459
582
  __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
460
583
  __privateMethod(this, _PDFReader_instances, throwIfAborted_fn).call(this, signal);
@@ -481,11 +604,16 @@ encodeCanvasToBlob_fn = async function(canvas, mimeType, quality, signal) {
481
604
  if (typeof canvas.toBlob !== "function") {
482
605
  throw __privateMethod(this, _PDFReader_instances, createUnsupportedEnvironmentError_fn).call(this, "Canvas Blob encoding is not supported by this canvas implementation.");
483
606
  }
607
+ __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
484
608
  __privateMethod(this, _PDFReader_instances, throwIfAborted_fn).call(this, signal);
485
609
  return await new Promise((resolve, reject) => {
486
610
  let settled = false;
487
611
  const onAbort = () => settle(() => reject(__privateMethod(this, _PDFReader_instances, createAbortedError_fn).call(this)));
488
- const cleanup = () => signal?.removeEventListener("abort", onAbort);
612
+ const onDestroy = () => settle(() => reject(__privateMethod(this, _PDFReader_instances, createDestroyedError_fn).call(this)));
613
+ const cleanup = () => {
614
+ signal?.removeEventListener("abort", onAbort);
615
+ __privateGet(this, _destroyController).signal.removeEventListener("abort", onDestroy);
616
+ };
489
617
  const settle = (callback) => {
490
618
  if (settled) return;
491
619
  settled = true;
@@ -493,6 +621,7 @@ encodeCanvasToBlob_fn = async function(canvas, mimeType, quality, signal) {
493
621
  callback();
494
622
  };
495
623
  signal?.addEventListener("abort", onAbort, { once: true });
624
+ __privateGet(this, _destroyController).signal.addEventListener("abort", onDestroy, { once: true });
496
625
  try {
497
626
  const resolveBlob = (blob) => {
498
627
  if (!blob) {
@@ -531,7 +660,7 @@ waitForRender_fn = async function(renderTask, signal) {
531
660
  __privateGet(this, _activeRenderTasks).add(renderTask);
532
661
  signal?.addEventListener("abort", abort, { once: true });
533
662
  try {
534
- await renderTask.promise;
663
+ await __privateMethod(this, _PDFReader_instances, awaitWithSignal_fn).call(this, renderTask.promise, signal);
535
664
  __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
536
665
  __privateMethod(this, _PDFReader_instances, throwIfAborted_fn).call(this, signal);
537
666
  } catch (error) {
@@ -545,6 +674,19 @@ releaseCanvas_fn = function(canvas) {
545
674
  canvas.width = 0;
546
675
  canvas.height = 0;
547
676
  };
677
+ acquirePageOperation_fn = function() {
678
+ __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
679
+ if (__privateGet(this, _activePageOperation)) {
680
+ throw new PdfReaderError(
681
+ "OPERATION_IN_PROGRESS",
682
+ "Another pages() or convert() operation is already active for this PDFReader."
683
+ );
684
+ }
685
+ __privateSet(this, _activePageOperation, true);
686
+ };
687
+ releasePageOperation_fn = function() {
688
+ __privateSet(this, _activePageOperation, false);
689
+ };
548
690
  getOrCreateLoadState_fn = function() {
549
691
  const existing = __privateGet(this, _loadingState);
550
692
  if (existing) return existing;
@@ -552,32 +694,45 @@ getOrCreateLoadState_fn = function() {
552
694
  const state = { destroyed: false };
553
695
  state.promise = (async () => {
554
696
  try {
555
- const policyResult = __privateMethod(this, _PDFReader_instances, enforceSourcePolicy_fn).call(this);
556
- if (__privateMethod(this, _PDFReader_instances, isPromiseLike_fn).call(this, policyResult)) await policyResult;
697
+ const source = __privateMethod(this, _PDFReader_instances, createSourceSnapshot_fn).call(this);
698
+ const policyResult = __privateMethod(this, _PDFReader_instances, enforceSourcePolicy_fn).call(this, source.info);
699
+ if (policyResult) await __privateMethod(this, _PDFReader_instances, awaitWithDestroy_fn).call(this, policyResult);
557
700
  if (state.destroyed || __privateGet(this, _destroyed)) {
558
701
  throw __privateMethod(this, _PDFReader_instances, createDestroyedError_fn).call(this);
559
702
  }
560
- const task = getDocument(__privateMethod(this, _PDFReader_instances, toPdfJsSource_fn).call(this, __privateGet(this, _source)));
703
+ const task = getDocument(source.pdfJsSource);
561
704
  state.task = task;
562
- const documentProxy = await task.promise;
563
- if (state.destroyed || state.destroyPromise) {
564
- await state.destroyPromise?.catch(() => void 0);
705
+ let documentProxy;
706
+ try {
707
+ documentProxy = await __privateMethod(this, _PDFReader_instances, awaitWithDestroy_fn).call(this, task.promise);
708
+ } catch (error) {
709
+ if (state.destroyed || __privateGet(this, _destroyed)) {
710
+ await state.destroyPromise?.catch(() => void 0);
711
+ throw __privateMethod(this, _PDFReader_instances, createDestroyedError_fn).call(this);
712
+ }
713
+ await __privateMethod(this, _PDFReader_instances, destroyLoadStateTask_fn).call(this, state).catch(() => void 0);
714
+ throw error;
715
+ }
716
+ if (state.destroyed) {
717
+ await __privateMethod(this, _PDFReader_instances, destroyLoadStateTask_fn).call(this, state).catch(() => void 0);
565
718
  throw __privateMethod(this, _PDFReader_instances, createDestroyedError_fn).call(this);
566
719
  }
567
720
  if (documentProxy.numPages > __privateGet(this, _limits).maxDocumentPages) {
568
- await task.destroy();
569
- throw new PdfReaderError(
721
+ const error = new PdfReaderError(
570
722
  "PAGE_LIMIT_EXCEEDED",
571
723
  `PDF has ${documentProxy.numPages} pages; limit is ${__privateGet(this, _limits).maxDocumentPages}.`
572
724
  );
725
+ await __privateMethod(this, _PDFReader_instances, destroyLoadStateTask_fn).call(this, state).catch(() => void 0);
726
+ throw error;
573
727
  }
574
728
  __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
729
+ state.task = void 0;
575
730
  __privateSet(this, _loadingTask, task);
576
731
  __privateSet(this, _document, documentProxy);
577
732
  __privateSet(this, _lastLoadFailed, false);
578
733
  return documentProxy;
579
734
  } catch (error) {
580
- if (state.destroyed || state.destroyPromise || __privateGet(this, _destroyed)) {
735
+ if (state.destroyed || __privateGet(this, _destroyed)) {
581
736
  await state.destroyPromise?.catch(() => void 0);
582
737
  throw __privateMethod(this, _PDFReader_instances, createDestroyedError_fn).call(this);
583
738
  }
@@ -591,14 +746,23 @@ getOrCreateLoadState_fn = function() {
591
746
  __privateSet(this, _loadingState, state);
592
747
  return state;
593
748
  };
749
+ destroyLoadStateTask_fn = function(state) {
750
+ if (state.destroyPromise) return state.destroyPromise;
751
+ const task = state.task;
752
+ state.task = void 0;
753
+ state.destroyPromise = task ? Promise.resolve(task.destroy()) : Promise.resolve();
754
+ return state.destroyPromise;
755
+ };
594
756
  awaitWithSignal_fn = async function(promise, signal, deadlineMs) {
595
757
  __privateMethod(this, _PDFReader_instances, throwIfAborted_fn).call(this, signal);
596
- if (!signal && deadlineMs === void 0) return promise;
758
+ __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
597
759
  return await new Promise((resolve, reject) => {
598
760
  let settled = false;
599
761
  const onAbort = () => settle(() => reject(__privateMethod(this, _PDFReader_instances, createAbortedError_fn).call(this)));
762
+ const onDestroy = () => settle(() => reject(__privateMethod(this, _PDFReader_instances, createDestroyedError_fn).call(this)));
600
763
  const cleanup = () => {
601
764
  signal?.removeEventListener("abort", onAbort);
765
+ __privateGet(this, _destroyController).signal.removeEventListener("abort", onDestroy);
602
766
  if (timer !== void 0) clearTimeout(timer);
603
767
  };
604
768
  const settle = (callback) => {
@@ -609,12 +773,32 @@ awaitWithSignal_fn = async function(promise, signal, deadlineMs) {
609
773
  };
610
774
  const timer = deadlineMs === void 0 ? void 0 : setTimeout(() => settle(() => reject(__privateMethod(this, _PDFReader_instances, createDeadlineExceededError_fn).call(this))), deadlineMs);
611
775
  signal?.addEventListener("abort", onAbort, { once: true });
776
+ __privateGet(this, _destroyController).signal.addEventListener("abort", onDestroy, { once: true });
612
777
  promise.then(
613
778
  (value) => settle(() => resolve(value)),
614
779
  (error) => settle(() => reject(error))
615
780
  ).catch(() => void 0);
616
781
  });
617
782
  };
783
+ awaitWithDestroy_fn = async function(promise) {
784
+ __privateMethod(this, _PDFReader_instances, throwIfDestroyed_fn).call(this);
785
+ return await new Promise((resolve, reject) => {
786
+ let settled = false;
787
+ const onDestroy = () => settle(() => reject(__privateMethod(this, _PDFReader_instances, createDestroyedError_fn).call(this)));
788
+ const cleanup = () => __privateGet(this, _destroyController).signal.removeEventListener("abort", onDestroy);
789
+ const settle = (callback) => {
790
+ if (settled) return;
791
+ settled = true;
792
+ cleanup();
793
+ callback();
794
+ };
795
+ __privateGet(this, _destroyController).signal.addEventListener("abort", onDestroy, { once: true });
796
+ Promise.resolve(promise).then(
797
+ (value) => settle(() => resolve(value)),
798
+ (error) => settle(() => reject(error))
799
+ );
800
+ });
801
+ };
618
802
  createAbortedError_fn = function() {
619
803
  return new PdfReaderError("ABORTED", "PDF operation was aborted.");
620
804
  };
@@ -649,8 +833,7 @@ resolveLoadOptions_fn = function(options) {
649
833
  isAbortSignal_fn = function(value) {
650
834
  return "aborted" in value && "addEventListener" in value && "removeEventListener" in value;
651
835
  };
652
- enforceSourcePolicy_fn = function() {
653
- const source = __privateMethod(this, _PDFReader_instances, inspectSource_fn).call(this, __privateGet(this, _source));
836
+ enforceSourcePolicy_fn = function(source) {
654
837
  if (source.byteLength !== void 0 && __privateGet(this, _limits).maxSourceBytes !== void 0 && source.byteLength > __privateGet(this, _limits).maxSourceBytes) {
655
838
  throw new PdfReaderError(
656
839
  "SOURCE_LIMIT_EXCEEDED",
@@ -660,8 +843,8 @@ enforceSourcePolicy_fn = function() {
660
843
  if (!__privateGet(this, _sourcePolicy)) return;
661
844
  try {
662
845
  const result = __privateGet(this, _sourcePolicy).call(this, source);
663
- if (!__privateMethod(this, _PDFReader_instances, isPromiseLike_fn).call(this, result)) return;
664
- return result.catch((error) => {
846
+ if (result === void 0) return;
847
+ return Promise.resolve(result).catch((error) => {
665
848
  if (error instanceof PdfReaderError) throw error;
666
849
  throw new PdfReaderError("SOURCE_POLICY_VIOLATION", "PDF source rejected by sourcePolicy.");
667
850
  });
@@ -670,56 +853,52 @@ enforceSourcePolicy_fn = function() {
670
853
  throw new PdfReaderError("SOURCE_POLICY_VIOLATION", "PDF source rejected by sourcePolicy.");
671
854
  }
672
855
  };
673
- isPromiseLike_fn = function(value) {
674
- return value instanceof Promise;
856
+ createSourceSnapshot_fn = function() {
857
+ const kind = __privateMethod(this, _PDFReader_instances, sourceKind_fn).call(this, __privateGet(this, _source));
858
+ const pdfJsSource = __privateMethod(this, _PDFReader_instances, toPdfJsSource_fn).call(this, __privateGet(this, _source));
859
+ const info = __privateMethod(this, _PDFReader_instances, inspectSource_fn).call(this, pdfJsSource, kind);
860
+ return { pdfJsSource, info };
861
+ };
862
+ inspectSource_fn = function(source, kind) {
863
+ const headers = __privateMethod(this, _PDFReader_instances, readHttpHeaders_fn).call(this, source.httpHeaders);
864
+ const info = {
865
+ rawSource: source,
866
+ kind,
867
+ url: __privateMethod(this, _PDFReader_instances, readUrl_fn).call(this, source.url),
868
+ byteLength: __privateMethod(this, _PDFReader_instances, knownByteLength_fn).call(this, source.data),
869
+ hasHttpHeaders: headers.hasHttpHeaders,
870
+ httpHeaders: headers.httpHeaders,
871
+ withCredentials: source.withCredentials === true
872
+ };
873
+ return Object.freeze(info);
675
874
  };
676
- inspectSource_fn = function(source) {
875
+ sourceKind_fn = function(source) {
876
+ if (typeof source === "string") return "url";
877
+ if (typeof URL !== "undefined" && source instanceof URL) return "url";
878
+ if (source instanceof ArrayBuffer || __privateMethod(this, _PDFReader_instances, isPdfTypedArray_fn).call(this, source) || Array.isArray(source)) return "bytes";
879
+ return "document-init-parameters";
880
+ };
881
+ toPdfJsSource_fn = function(source) {
677
882
  if (typeof source === "string") {
678
- return {
679
- rawSource: source,
680
- kind: "url",
681
- url: source,
682
- hasHttpHeaders: false,
683
- withCredentials: false
684
- };
883
+ return __privateMethod(this, _PDFReader_instances, freezeSource_fn).call(this, { url: source });
685
884
  }
686
885
  if (typeof URL !== "undefined" && source instanceof URL) {
687
- return {
688
- rawSource: source,
689
- kind: "url",
690
- url: source.toString(),
691
- hasHttpHeaders: false,
692
- withCredentials: false
693
- };
886
+ return __privateMethod(this, _PDFReader_instances, freezeSource_fn).call(this, { url: source.toString() });
694
887
  }
695
- const byteLength = __privateMethod(this, _PDFReader_instances, knownByteLength_fn).call(this, source);
696
- if (byteLength !== void 0) {
697
- return {
698
- rawSource: source,
699
- kind: "bytes",
700
- byteLength,
701
- hasHttpHeaders: false,
702
- withCredentials: false
703
- };
888
+ if (source instanceof ArrayBuffer || __privateMethod(this, _PDFReader_instances, isPdfTypedArray_fn).call(this, source) || Array.isArray(source)) {
889
+ return __privateMethod(this, _PDFReader_instances, freezeSource_fn).call(this, { data: source });
704
890
  }
705
- const candidate = source;
706
- const headers = __privateMethod(this, _PDFReader_instances, readHttpHeaders_fn).call(this, candidate.httpHeaders);
707
- return {
708
- rawSource: source,
709
- kind: "document-init-parameters",
710
- url: __privateMethod(this, _PDFReader_instances, readUrl_fn).call(this, candidate.url),
711
- byteLength: __privateMethod(this, _PDFReader_instances, knownByteLength_fn).call(this, candidate.data),
712
- hasHttpHeaders: headers !== void 0,
713
- httpHeaders: headers,
714
- withCredentials: candidate.withCredentials === true
715
- };
891
+ const snapshot = {};
892
+ for (const key of Reflect.ownKeys(source)) {
893
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
894
+ if (!descriptor?.enumerable) continue;
895
+ snapshot[key] = source[key];
896
+ }
897
+ if (typeof URL !== "undefined" && snapshot.url instanceof URL) snapshot.url = snapshot.url.toString();
898
+ return __privateMethod(this, _PDFReader_instances, freezeSource_fn).call(this, snapshot);
716
899
  };
717
- toPdfJsSource_fn = function(source) {
718
- if (typeof source === "string") return { url: source };
719
- if (source instanceof URL) return { url: source };
720
- if (source instanceof ArrayBuffer || __privateMethod(this, _PDFReader_instances, isPdfTypedArray_fn).call(this, source) || Array.isArray(source))
721
- return { data: source };
722
- return source;
900
+ freezeSource_fn = function(source) {
901
+ return Object.freeze(source);
723
902
  };
724
903
  isPdfTypedArray_fn = function(source) {
725
904
  return ArrayBuffer.isView(source) && !(source instanceof DataView);
@@ -736,10 +915,24 @@ readUrl_fn = function(value) {
736
915
  return void 0;
737
916
  };
738
917
  readHttpHeaders_fn = function(value) {
739
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
740
- const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
741
- if (entries.length === 0) return void 0;
742
- return Object.freeze(Object.fromEntries(entries));
918
+ if (!value || typeof value !== "object") return { hasHttpHeaders: false };
919
+ const entries = [];
920
+ try {
921
+ for (const [name, headerValue] of Object.entries(value)) {
922
+ if (typeof headerValue === "string") entries.push([name, headerValue]);
923
+ }
924
+ if ("forEach" in value && typeof value.forEach === "function") {
925
+ value.forEach((headerValue, name) => {
926
+ if (typeof name === "string" && typeof headerValue === "string") entries.push([name, headerValue]);
927
+ });
928
+ }
929
+ } catch {
930
+ return { hasHttpHeaders: true };
931
+ }
932
+ return {
933
+ hasHttpHeaders: true,
934
+ httpHeaders: entries.length > 0 ? Object.freeze(Object.fromEntries(entries)) : void 0
935
+ };
743
936
  };
744
937
  validateLimits_fn = function() {
745
938
  for (const [name, value] of Object.entries(__privateGet(this, _limits))) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@web-ts-toolkit/pdf-reader",
3
3
  "description": "Resource-safe browser PDF rendering, text extraction, and embedded-image extraction built on PDF.js",
4
4
  "homepage": "https://web-ts-toolkit.pages.dev/docs/packages/pdf-reader",
5
- "version": "0.40.1",
5
+ "version": "0.41.0",
6
6
  "sideEffects": false,
7
7
  "keywords": [
8
8
  "pdf",
@@ -26,7 +26,7 @@
26
26
  "node": ">=22.13.0"
27
27
  },
28
28
  "peerDependencies": {
29
- "pdfjs-dist": "^6.2.108"
29
+ "pdfjs-dist": "~6.2.108"
30
30
  },
31
31
  "files": [
32
32
  "**/*",