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 +97 -195
- package/dist/index.d.mts +123 -118
- package/dist/index.mjs +429 -223
- package/dist/pdfjs.mjs +6 -6
- package/package.json +14 -31
- package/dist/index.cjs +0 -449
- package/dist/index.d.cts +0 -180
- package/dist/pdfjs.d.ts +0 -1
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
|
|
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](#
|
|
12
|
-
- 🖼️ [Render pages as
|
|
13
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
PNG encoding needs no canvas and no native dependency, so it also runs in Cloudflare Workers:
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
52
|
+
const images = await extractImages(pdf, 1, { format: 'png' })
|
|
60
53
|
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
for (const [index, image] of images.entries())
|
|
55
|
+
await writeFile(`image-${index + 1}.png`, image.data)
|
|
63
56
|
```
|
|
64
57
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
function getResolvedPDFJS(): Promise<PDFJS>
|
|
73
|
-
```
|
|
62
|
+
### Render a Page as an Image
|
|
74
63
|
|
|
75
|
-
|
|
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 {
|
|
67
|
+
import { renderPageAsImage } from 'unpdf'
|
|
79
68
|
|
|
80
|
-
const
|
|
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
|
-
###
|
|
77
|
+
### Spot Scanned Pages
|
|
84
78
|
|
|
85
|
-
|
|
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
|
-
|
|
81
|
+
```ts
|
|
82
|
+
import { inspectPages } from 'unpdf'
|
|
88
83
|
|
|
89
|
-
|
|
84
|
+
const { pages } = await inspectPages(pdf)
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
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
|
-
|
|
93
|
+
### Process Untrusted PDFs
|
|
99
94
|
|
|
100
|
-
|
|
95
|
+
PDF.js does not execute scripts embedded in PDFs, but resource limits are your job:
|
|
101
96
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
101
|
+
## API
|
|
110
102
|
|
|
111
|
-
|
|
103
|
+
`data` is binary PDF data or a `PDFDocumentProxy` throughout.
|
|
104
|
+
|
|
105
|
+
### `getDocumentProxy`
|
|
112
106
|
|
|
113
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
142
|
+
function extractImages(data, pageNumber: number, options?: { format?: 'rgba' | 'png' }): Promise<ExtractedImage[]>
|
|
143
|
+
```
|
|
196
144
|
|
|
197
|
-
|
|
198
|
-
const buffer = await readFile('./document.pdf')
|
|
199
|
-
const pdf = await getDocumentProxy(new Uint8Array(buffer))
|
|
145
|
+
### `renderPageAsImage`
|
|
200
146
|
|
|
201
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
### `
|
|
161
|
+
### `inspectPages`
|
|
210
162
|
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
### `getMeta`
|
|
231
170
|
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
### `
|
|
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
|
-
|
|
179
|
+
Sets the PDF.js build every other method uses. It loads on first use.
|
|
259
180
|
|
|
260
181
|
```ts
|
|
261
|
-
function
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
```ts
|
|
278
|
-
import { readFile, writeFile } from 'node:fs/promises'
|
|
279
|
-
import { renderPageAsImage } from 'unpdf'
|
|
185
|
+
### `getResolvedPDFJS`
|
|
280
186
|
|
|
281
|
-
|
|
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
|
-
|
|
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
|
|
198
|
+
import { definePDFJSModule } from 'unpdf'
|
|
296
199
|
|
|
297
|
-
|
|
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
|
|
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
|
-
##
|
|
205
|
+
## How It Works
|
|
306
206
|
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
- **
|
|
312
|
-
-
|
|
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
|
|