unpdf 1.9.1 → 2.0.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 CHANGED
@@ -6,11 +6,11 @@ Ships with a serverless build of Mozilla's [PDF.js](https://github.com/mozilla/p
6
6
 
7
7
  ## Features
8
8
 
9
- - 🏗️ Works in Node.js 22+, Bun, browsers, and serverless environments ([Cloudflare Workers example](./examples/cloudflare))
9
+ - 🏗️ Works in Node.js 24+, Bun, browsers, and serverless environments ([Cloudflare Workers example](./examples/cloudflare))
10
10
  - 🪭 Includes serverless build of PDF.js ([`unpdf/pdfjs`](#how-it-works))
11
- - 💬 Extract [text](#extracttext), [positioned text](#extracttextitems), [links](#extractlinks), [images](#extractimages), and [metadata](#getmeta) from PDF files
12
- - 🖼️ [Render pages as images](#renderpageasimage)
13
- - 🧠 Perfect for AI applications and PDF summarization
11
+ - 💬 Extract [text](#extract-text), [images as PNG](#extract-images-as-png), links, and metadata – no native dependencies
12
+ - 🖼️ [Render pages as PNG, JPEG, or WebP](#render-a-page-as-an-image)
13
+ - 🔍 [Spot scanned pages](#spot-scanned-pages) before sending them to OCR
14
14
  - 🧱 Opt-in to a [custom PDF.js build](#custom-pdfjs-build)
15
15
 
16
16
  ## Installation
@@ -23,268 +23,171 @@ pnpm add unpdf
23
23
  npm install unpdf
24
24
  ```
25
25
 
26
- ## Usage
26
+ ## Use Cases
27
+
28
+ Every method takes binary PDF data (`Uint8Array`, `ArrayBuffer`) or a document proxy from [`getDocumentProxy`](#getdocumentproxy). Pass a proxy to run several methods on one document without parsing it again.
29
+
30
+ ### Extract Text
27
31
 
28
32
  ```ts
29
33
  import { extractText, getDocumentProxy } from 'unpdf'
30
34
 
31
- // Fetch a PDF from the web or load it from the file system
32
35
  const buffer = await fetch('https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf')
33
36
  .then(res => res.arrayBuffer())
34
37
 
35
38
  const pdf = await getDocumentProxy(new Uint8Array(buffer))
36
39
  const { totalPages, text } = await extractText(pdf, { mergePages: true })
37
-
38
- console.log(`Total pages: ${totalPages}`)
39
- console.log(text)
40
40
  ```
41
41
 
42
- ## How It Works
42
+ Without `mergePages`, `text` holds one string per page. For coordinates and font sizes, e.g. to detect tables, use [`extractTextItems`](#extracttextitems).
43
43
 
44
- > [!NOTE]
45
- > The serverless PDF.js bundle is built from PDF.js v6.4.299. It requires the global `Iterator` constructor (Safari 18.4+); without it, importing the bundle throws.
44
+ ### Extract Images as PNG
46
45
 
47
- Heart and soul of this package is the [`pdfjs.rolldown.config.ts`](./pdfjs.rolldown.config.ts) file. It uses [Rolldown](https://rolldown.rs/) to bundle PDF.js into a single file for serverless environments. The key techniques:
46
+ PNG encoding needs no canvas and no native dependency, so it also runs in Cloudflare Workers:
48
47
 
49
- - **String replacements** strip browser-specific references from the PDF.js source.
50
- - **Worker inlining** embeds the PDF.js worker directly into the main bundle, since serverless runtimes can't load separate worker files.
51
- - **Global polyfills** fill in newer APIs like `Promise.try` and `Map.prototype.getOrInsert` for runtimes that lack them, plus `FinalizationRegistry` for Cloudflare Workers.
52
-
53
- ## API
54
-
55
- ### `definePDFJSModule`
56
-
57
- Sets the PDF.js build every other method uses – call it before any of them. See [Custom PDF.js Build](#custom-pdfjs-build).
48
+ ```ts
49
+ import { writeFile } from 'node:fs/promises'
50
+ import { extractImages } from 'unpdf'
58
51
 
59
- **Type Declaration**
52
+ const images = await extractImages(pdf, 1, { format: 'png' })
60
53
 
61
- ```ts
62
- function definePDFJSModule(pdfjs: () => Promise<PDFJS>): Promise<void>
54
+ for (const [index, image] of images.entries())
55
+ await writeFile(`image-${index + 1}.png`, image.data)
63
56
  ```
64
57
 
65
- ### `getResolvedPDFJS`
66
-
67
- Returns the resolved PDF.js module – the serverless build, unless `definePDFJSModule` set another. Use it to call the PDF.js API directly.
58
+ Without `format`, `data` holds raw RGBA pixels. Each image carries its `bounds` on the page.
68
59
 
69
- **Type Declaration**
60
+ In browsers, a document proxy decodes some images into bitmaps, which `extractImages` skips. Pass binary data instead, or create the proxy with `isOffscreenCanvasSupported: false` and `isImageDecoderSupported: false`.
70
61
 
71
- ```ts
72
- function getResolvedPDFJS(): Promise<PDFJS>
73
- ```
62
+ ### Render a Page as an Image
74
63
 
75
- **Example**
64
+ Browsers render on a DOM canvas, which a Web Worker lacks. Node.js and Bun need [`@napi-rs/canvas`](https://github.com/Brooooooklyn/canvas), passed as `canvasImport`. Serverless runtimes like Cloudflare Workers have no canvas.
76
65
 
77
66
  ```ts
78
- import { getResolvedPDFJS } from 'unpdf'
67
+ import { renderPageAsImage } from 'unpdf'
79
68
 
80
- const { version } = await getResolvedPDFJS()
69
+ const image = await renderPageAsImage(pdf, 1, {
70
+ canvasImport: () => import('@napi-rs/canvas'),
71
+ scale: 2,
72
+ format: 'jpeg',
73
+ quality: 0.8,
74
+ })
81
75
  ```
82
76
 
83
- ### `getDocumentProxy`
77
+ ### Spot Scanned Pages
84
78
 
85
- Creates a `PDFDocumentProxy` from binary PDF data. Every extraction method accepts either raw data or an existing proxy – use this when you want to reuse one document across multiple calls.
79
+ `inspectPages` measures text and images per page – where to draw the line between a text page and a scan depends on your documents:
86
80
 
87
- Applies sensible defaults: `useSystemFonts: true`; in Node.js additionally `disableFontFace: true`, plus `standardFontDataUrl` and `cMapUrl`/`cMapPacked` resolved from the local `pdfjs-dist` package for standard font and CJK character map support. Without `pdfjs-dist` installed, the Node.js defaults are skipped.
81
+ ```ts
82
+ import { inspectPages } from 'unpdf'
88
83
 
89
- **Type Declaration**
84
+ const { pages } = await inspectPages(pdf)
90
85
 
91
- ```ts
92
- function getDocumentProxy(
93
- data: DocumentInitParameters['data'],
94
- options?: DocumentInitParameters,
95
- ): Promise<PDFDocumentProxy>
86
+ // Hidden OCR layers count as invisible text, so subtract them.
87
+ const scannedPageNumbers = pages
88
+ .map((page, index) => ({ ...page, pageNumber: index + 1 }))
89
+ .filter(page => page.textLength - page.invisibleTextLength < 20 && page.imageCoverage > 0.8)
90
+ .map(page => page.pageNumber)
96
91
  ```
97
92
 
98
- **Example**
93
+ ### Process Untrusted PDFs
99
94
 
100
- Your options override the defaults:
95
+ PDF.js does not execute scripts embedded in PDFs, but resource limits are your job:
101
96
 
102
- ```ts
103
- const pdf = await getDocumentProxy(buffer, {
104
- disableFontFace: false,
105
- standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@latest/standard_fonts/',
106
- })
107
- ```
97
+ - **Image decoding:** `maxImageSize` is unlimited by default – pass e.g. `getDocumentProxy(data, { maxImageSize: 16_777_216 })` (~16 MP) so a single declared image can't allocate gigabytes.
98
+ - **Page fan-out:** `extractText`, `extractTextItems`, `extractLinks`, and `inspectPages` process all pages in one call – check `pdf.numPages` against a limit first.
99
+ - **Timeouts:** with the serverless build, parsing runs on your event loop (no worker) – race extraction calls against a timeout.
108
100
 
109
- ### `getMeta`
101
+ ## API
110
102
 
111
- Extracts metadata from a PDF. If `parseDates` is set to `true`, the date properties will be parsed into `Date` objects.
103
+ `data` is binary PDF data or a `PDFDocumentProxy` throughout.
104
+
105
+ ### `getDocumentProxy`
112
106
 
113
- **Type Declaration**
107
+ Creates a `PDFDocumentProxy` with `useSystemFonts: true`. In Node.js with `pdfjs-dist` installed, it uses that package's standard fonts instead of system fonts, and its CJK character maps (`cMapUrl`). Cloudflare Workers have no local `pdfjs-dist` – to extract text from CJK fonts without embedded mappings, pass `cMapUrl: 'https://unpkg.com/pdfjs-dist@6.4.299/cmaps/'`, `cMapPacked: true`, and `useWorkerFetch: true`. Your options override the defaults.
114
108
 
115
109
  ```ts
116
- function getMeta(
117
- data: DocumentInitParameters['data'] | PDFDocumentProxy,
118
- options?: {
119
- parseDates?: boolean
120
- },
121
- ): Promise<{
122
- info: Record<string, any>
123
- metadata: Record<string, any>
124
- }>
110
+ function getDocumentProxy(data: Uint8Array | ArrayBuffer, options?: DocumentInitParameters): Promise<PDFDocumentProxy>
125
111
  ```
126
112
 
127
113
  ### `extractText`
128
114
 
129
- Extracts all text from a PDF. If `mergePages` is set to `true`, the text of all pages will be merged into a single string with line breaks preserved and at most one blank line in a row. Otherwise, an array of strings for each page will be returned.
130
-
131
- **Type Declaration**
115
+ Extracts the text of every page, or one string with `mergePages: true` – line breaks kept, at most one blank line in a row.
132
116
 
133
117
  ```ts
134
- function extractText(
135
- data: DocumentInitParameters['data'] | PDFDocumentProxy,
136
- options?: {
137
- mergePages?: boolean
138
- }
139
- ): Promise<{
140
- totalPages: number
141
- text: string | string[]
142
- }>
118
+ function extractText(data, options?: { mergePages?: boolean }): Promise<{ totalPages: number, text: string | string[] }>
143
119
  ```
144
120
 
145
121
  ### `extractTextItems`
146
122
 
147
- Extracts text with layout information – one array of positioned items per page. Useful when plain text is not enough and you need coordinates, font sizes, or reading direction, e.g. for table detection or positional parsing.
148
-
149
- **Type Declaration**
123
+ Extracts positioned text items per page: `str`, `x` and `y` in PDF space (origin bottom-left), `width`, `height`, `fontSize`, `fontFamily`, `dir`, `hasEOL`.
150
124
 
151
125
  ```ts
152
- interface StructuredTextItem {
153
- str: string
154
- /** X position in PDF coordinate space (origin: bottom-left). */
155
- x: number
156
- /** Y position in PDF coordinate space (origin: bottom-left). */
157
- y: number
158
- width: number
159
- height: number
160
- fontSize: number
161
- fontFamily: string
162
- /** Text direction: `"ltr"`, `"rtl"`, or `"ttb"`. */
163
- dir: string
164
- /** Whether the text item is followed by a line break. */
165
- hasEOL: boolean
166
- }
167
-
168
- function extractTextItems(
169
- data: DocumentInitParameters['data'] | PDFDocumentProxy,
170
- ): Promise<{
171
- totalPages: number
172
- items: StructuredTextItem[][]
173
- }>
126
+ function extractTextItems(data): Promise<{ totalPages: number, items: StructuredTextItem[][] }>
174
127
  ```
175
128
 
176
129
  ### `extractLinks`
177
130
 
178
- Extracts all links from a PDF document, including hyperlinks and external URLs.
179
-
180
- **Type Declaration**
131
+ Extracts the URLs of link annotations per page.
181
132
 
182
133
  ```ts
183
- function extractLinks(
184
- data: DocumentInitParameters['data'] | PDFDocumentProxy,
185
- ): Promise<{
186
- totalPages: number
187
- links: string[]
188
- }>
134
+ function extractLinks(data): Promise<{ totalPages: number, links: string[][] }>
189
135
  ```
190
136
 
191
- **Example**
137
+ ### `extractImages`
138
+
139
+ Extracts the images painted on a page – image XObjects, inline images, and image masks – as RGBA pixels or PNG bytes, each with `width`, `height`, and `bounds` in PDF space.
192
140
 
193
141
  ```ts
194
- import { readFile } from 'node:fs/promises'
195
- import { extractLinks, getDocumentProxy } from 'unpdf'
142
+ function extractImages(data, pageNumber: number, options?: { format?: 'rgba' | 'png' }): Promise<ExtractedImage[]>
143
+ ```
196
144
 
197
- // Load a PDF file
198
- const buffer = await readFile('./document.pdf')
199
- const pdf = await getDocumentProxy(new Uint8Array(buffer))
145
+ ### `renderPageAsImage`
200
146
 
201
- // Extract all links from the PDF
202
- const { totalPages, links } = await extractLinks(pdf)
147
+ Renders a page as image bytes, or as a data URL with `toDataURL: true`. `width` or `height` scale the page to that size.
203
148
 
204
- console.log(`Total pages: ${totalPages}`)
205
- console.log(`Found ${links.length} links:`)
206
- for (const link of links) console.log(link)
149
+ ```ts
150
+ function renderPageAsImage(data, pageNumber: number, options?: {
151
+ canvasImport?: () => Promise<typeof import('@napi-rs/canvas')>
152
+ scale?: number // Default: 1
153
+ width?: number
154
+ height?: number
155
+ format?: 'png' | 'jpeg' | 'webp' // Default: 'png'
156
+ quality?: number // From 0 to 1, for JPEG and WebP
157
+ toDataURL?: boolean
158
+ }): Promise<Uint8Array | string>
207
159
  ```
208
160
 
209
- ### `extractImages`
161
+ ### `inspectPages`
210
162
 
211
- Extracts images from a specific page of a PDF document, including necessary metadata such as width, height, and calculated color channels.
212
-
213
- **Type Declaration**
163
+ Measures each page: `textLength`, `invisibleTextLength` (text drawn invisibly, as by OCR layers), `imageCount`, and `imageCoverage` (share of the page covered by images, from 0 to 1).
214
164
 
215
165
  ```ts
216
- interface ExtractedImageObject {
217
- data: Uint8ClampedArray
218
- width: number
219
- height: number
220
- channels: 1 | 3 | 4
221
- key: string
222
- }
223
-
224
- function extractImages(
225
- data: DocumentInitParameters['data'] | PDFDocumentProxy,
226
- pageNumber: number,
227
- ): Promise<ExtractedImageObject[]>
166
+ function inspectPages(data): Promise<{ totalPages: number, pages: PageInspection[] }>
228
167
  ```
229
168
 
230
- **Example**
169
+ ### `getMeta`
231
170
 
232
- Saves each image as PNG with [sharp](https://github.com/lovell/sharp), installed separately:
171
+ Extracts the `/Info` dictionary and the XMP metadata (`null` without). `parseDates: true` turns the dates into `Date` objects.
233
172
 
234
173
  ```ts
235
- import { readFile } from 'node:fs/promises'
236
- import sharp from 'sharp'
237
- import { extractImages, getDocumentProxy } from 'unpdf'
238
-
239
- const buffer = await readFile('./document.pdf')
240
- const pdf = await getDocumentProxy(new Uint8Array(buffer))
241
- const images = await extractImages(pdf, 1)
242
-
243
- for (const [index, image] of images.entries()) {
244
- await sharp(image.data, {
245
- raw: { width: image.width, height: image.height, channels: image.channels },
246
- })
247
- .png()
248
- .toFile(`image-${index + 1}.png`)
249
- }
174
+ function getMeta(data, options?: { parseDates?: boolean }): Promise<{ info: PDFInfo, metadata: Metadata | null }>
250
175
  ```
251
176
 
252
- ### `renderPageAsImage`
253
-
254
- Renders a PDF page as PNG and returns its bytes as an `ArrayBuffer`, or a data URL (`string`) with `toDataURL: true`.
255
-
256
- Rendering needs a canvas: browsers bring their own, while Node.js and Bun need the [`@napi-rs/canvas`](https://github.com/Brooooooklyn/canvas) package, passed as `canvasImport`. Serverless runtimes like Cloudflare Workers have neither.
177
+ ### `definePDFJSModule`
257
178
 
258
- **Type Declaration**
179
+ Sets the PDF.js build every other method uses. It loads on first use.
259
180
 
260
181
  ```ts
261
- function renderPageAsImage(
262
- data: DocumentInitParameters['data'] | PDFDocumentProxy,
263
- pageNumber: number,
264
- options?: {
265
- canvasImport?: () => Promise<typeof import('@napi-rs/canvas')>
266
- /** @default 1.0 */
267
- scale?: number
268
- width?: number
269
- height?: number
270
- toDataURL?: boolean
271
- },
272
- ): Promise<ArrayBuffer | string>
182
+ function definePDFJSModule(pdfjs: () => Promise<PDFJS>): void
273
183
  ```
274
184
 
275
- **Example**
276
-
277
- ```ts
278
- import { readFile, writeFile } from 'node:fs/promises'
279
- import { renderPageAsImage } from 'unpdf'
185
+ ### `getResolvedPDFJS`
280
186
 
281
- const buffer = await readFile('./dummy.pdf')
282
- const image = await renderPageAsImage(new Uint8Array(buffer), 1, {
283
- canvasImport: () => import('@napi-rs/canvas'),
284
- scale: 2,
285
- })
187
+ Returns the resolved PDF.js module – the serverless build, unless `definePDFJSModule` set another – to call the PDF.js API directly.
286
188
 
287
- await writeFile('dummy-page-1.png', new Uint8Array(image))
189
+ ```ts
190
+ function getResolvedPDFJS(): Promise<PDFJS>
288
191
  ```
289
192
 
290
193
  ## Custom PDF.js Build
@@ -292,24 +195,23 @@ await writeFile('dummy-page-1.png', new Uint8Array(image))
292
195
  `unpdf` uses its serverless build unless you define another one, e.g. to pin a different PDF.js version. Install `pdfjs-dist` and call `definePDFJSModule` before any other method:
293
196
 
294
197
  ```ts
295
- import { definePDFJSModule, extractText, getDocumentProxy } from 'unpdf'
198
+ import { definePDFJSModule } from 'unpdf'
296
199
 
297
- await definePDFJSModule(() => import('pdfjs-dist/legacy/build/pdf.mjs'))
298
-
299
- const pdf = await getDocumentProxy(/* … */)
300
- const { text } = await extractText(pdf)
200
+ definePDFJSModule(() => import('pdfjs-dist/legacy/build/pdf.mjs'))
301
201
  ```
302
202
 
303
- In Node.js, import the legacy build as shown. The default `pdfjs-dist` entry targets current browsers – it fails on Node.js 24 and older and warns on newer versions.
203
+ In Node.js, import the legacy build as shown. The default `pdfjs-dist` entry targets current browsers – it fails on Node.js 24 and warns on newer versions. `renderPageAsImage` still requires `canvasImport`, though PDF.js draws with the `@napi-rs/canvas` that `pdfjs-dist` installs as an optional dependency.
304
204
 
305
- ## Processing Untrusted PDFs
205
+ ## How It Works
306
206
 
307
- PDF.js does not execute scripts embedded in PDFs, but resource limits are your job:
207
+ > [!NOTE]
208
+ > The serverless PDF.js bundle is built from PDF.js v6.4.299. It requires the global `Iterator` constructor (Safari 18.4+); without it, importing the bundle throws.
308
209
 
309
- - **Image decoding:** `maxImageSize` is unlimited by default – pass e.g. `maxImageSize: 16_777_216` (~16 MP) so a single declared image can't allocate gigabytes.
310
- - **Page fan-out:** `extractText`, `extractTextItems`, and `extractLinks` process all pages in one call – check `pdf.numPages` against a limit first.
311
- - **Timeouts:** with the serverless build, parsing runs on your event loop (no worker) – race extraction calls against a timeout.
312
- - The raw `getDocument` from `unpdf/pdfjs` applies none of these defaults.
210
+ Heart and soul of this package is the [`pdfjs.rolldown.config.ts`](./pdfjs.rolldown.config.ts) file. It uses [Rolldown](https://rolldown.rs/) to bundle PDF.js into a single file for serverless environments. The key techniques:
211
+
212
+ - **String replacements** strip browser-specific references from the PDF.js source.
213
+ - **Worker inlining** embeds the PDF.js worker directly into the main bundle, since serverless runtimes can't load separate worker files.
214
+ - **Global polyfills** fill in newer APIs like `Promise.try` and `Map.prototype.getOrInsert` for runtimes that lack them, plus `FinalizationRegistry` for Cloudflare Workers.
313
215
 
314
216
  ## License
315
217