Utilities for PDF extraction and rendering across all JavaScript runtimes β Node.js, Bun, the browser, and serverless environments like Cloudflare Workers. Especially useful for AI applications that need to summarize or analyze PDF documents.
Ships with a serverless build of Mozilla's PDF.js, optimized for edge environments. If you're coming from pdf-parse, unpdf is a modern, actively maintained alternative with broader runtime support.
- ποΈ Works in Node.js 22+, Bun, browsers, and serverless environments (Cloudflare Workers example)
- πͺ Includes serverless build of PDF.js (
unpdf/pdfjs) - π¬ Extract text, positioned text, links, images, and metadata from PDF files
- πΌοΈ Render pages as images
- π§ Perfect for AI applications and PDF summarization
- π§± Opt-in to a custom PDF.js build
# pnpm
pnpm add unpdf
# npm
npm install unpdfimport { extractText, getDocumentProxy } from 'unpdf'
// Fetch a PDF from the web or load it from the file system
const buffer = await fetch('https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf')
.then(res => res.arrayBuffer())
const pdf = await getDocumentProxy(new Uint8Array(buffer))
const { totalPages, text } = await extractText(pdf, { mergePages: true })
console.log(`Total pages: ${totalPages}`)
console.log(text)Note
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.
Heart and soul of this package is the pdfjs.rolldown.config.ts file. It uses Rolldown to bundle PDF.js into a single file for serverless environments. The key techniques:
- String replacements strip browser-specific references from the PDF.js source.
- Worker inlining embeds the PDF.js worker directly into the main bundle, since serverless runtimes can't load separate worker files.
- Global polyfills fill in newer APIs like
Promise.tryandMap.prototype.getOrInsertfor runtimes that lack them, plusFinalizationRegistryfor Cloudflare Workers.
Sets the PDF.js build every other method uses β call it before any of them. See Custom PDF.js Build.
Type Declaration
function definePDFJSModule(pdfjs: () => Promise<PDFJS>): Promise<void>Returns the resolved PDF.js module β the serverless build, unless definePDFJSModule set another. Use it to call the PDF.js API directly.
Type Declaration
function getResolvedPDFJS(): Promise<PDFJS>Example
import { getResolvedPDFJS } from 'unpdf'
const { version } = await getResolvedPDFJS()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.
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.
Type Declaration
function getDocumentProxy(
data: DocumentInitParameters['data'],
options?: DocumentInitParameters,
): Promise<PDFDocumentProxy>Example
Your options override the defaults:
const pdf = await getDocumentProxy(buffer, {
disableFontFace: false,
standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@latest/standard_fonts/',
})Extracts metadata from a PDF. If parseDates is set to true, the date properties will be parsed into Date objects.
Type Declaration
function getMeta(
data: DocumentInitParameters['data'] | PDFDocumentProxy,
options?: {
parseDates?: boolean
},
): Promise<{
info: Record<string, any>
metadata: Record<string, any>
}>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.
Type Declaration
function extractText(
data: DocumentInitParameters['data'] | PDFDocumentProxy,
options?: {
mergePages?: boolean
}
): Promise<{
totalPages: number
text: string | string[]
}>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.
Type Declaration
interface StructuredTextItem {
str: string
/** X position in PDF coordinate space (origin: bottom-left). */
x: number
/** Y position in PDF coordinate space (origin: bottom-left). */
y: number
width: number
height: number
fontSize: number
fontFamily: string
/** Text direction: `"ltr"`, `"rtl"`, or `"ttb"`. */
dir: string
/** Whether the text item is followed by a line break. */
hasEOL: boolean
}
function extractTextItems(
data: DocumentInitParameters['data'] | PDFDocumentProxy,
): Promise<{
totalPages: number
items: StructuredTextItem[][]
}>Extracts all links from a PDF document, including hyperlinks and external URLs.
Type Declaration
function extractLinks(
data: DocumentInitParameters['data'] | PDFDocumentProxy,
): Promise<{
totalPages: number
links: string[]
}>Example
import { readFile } from 'node:fs/promises'
import { extractLinks, getDocumentProxy } from 'unpdf'
// Load a PDF file
const buffer = await readFile('./document.pdf')
const pdf = await getDocumentProxy(new Uint8Array(buffer))
// Extract all links from the PDF
const { totalPages, links } = await extractLinks(pdf)
console.log(`Total pages: ${totalPages}`)
console.log(`Found ${links.length} links:`)
for (const link of links) console.log(link)Extracts images from a specific page of a PDF document, including necessary metadata such as width, height, and calculated color channels.
Type Declaration
interface ExtractedImageObject {
data: Uint8ClampedArray
width: number
height: number
channels: 1 | 3 | 4
key: string
}
function extractImages(
data: DocumentInitParameters['data'] | PDFDocumentProxy,
pageNumber: number,
): Promise<ExtractedImageObject[]>Example
Saves each image as PNG with sharp, installed separately:
import { readFile } from 'node:fs/promises'
import sharp from 'sharp'
import { extractImages, getDocumentProxy } from 'unpdf'
const buffer = await readFile('./document.pdf')
const pdf = await getDocumentProxy(new Uint8Array(buffer))
const images = await extractImages(pdf, 1)
for (const [index, image] of images.entries()) {
await sharp(image.data, {
raw: { width: image.width, height: image.height, channels: image.channels },
})
.png()
.toFile(`image-${index + 1}.png`)
}Renders a PDF page as PNG and returns its bytes as an ArrayBuffer, or a data URL (string) with toDataURL: true.
Rendering needs a canvas: browsers bring their own, while Node.js and Bun need the @napi-rs/canvas package, passed as canvasImport. Serverless runtimes like Cloudflare Workers have neither.
Type Declaration
function renderPageAsImage(
data: DocumentInitParameters['data'] | PDFDocumentProxy,
pageNumber: number,
options?: {
canvasImport?: () => Promise<typeof import('@napi-rs/canvas')>
/** @default 1.0 */
scale?: number
width?: number
height?: number
toDataURL?: boolean
},
): Promise<ArrayBuffer | string>Example
import { readFile, writeFile } from 'node:fs/promises'
import { renderPageAsImage } from 'unpdf'
const buffer = await readFile('./dummy.pdf')
const image = await renderPageAsImage(new Uint8Array(buffer), 1, {
canvasImport: () => import('@napi-rs/canvas'),
scale: 2,
})
await writeFile('dummy-page-1.png', new Uint8Array(image))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:
import { definePDFJSModule, extractText, getDocumentProxy } from 'unpdf'
await definePDFJSModule(() => import('pdfjs-dist/legacy/build/pdf.mjs'))
const pdf = await getDocumentProxy(/* β¦ */)
const { text } = await extractText(pdf)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.
PDF.js does not execute scripts embedded in PDFs, but resource limits are your job:
- Image decoding:
maxImageSizeis unlimited by default β pass e.g.maxImageSize: 16_777_216(~16 MP) so a single declared image can't allocate gigabytes. - Page fan-out:
extractText,extractTextItems, andextractLinksprocess all pages in one call β checkpdf.numPagesagainst a limit first. - Timeouts: with the serverless build, parsing runs on your event loop (no worker) β race extraction calls against a timeout.
- The raw
getDocumentfromunpdf/pdfjsapplies none of these defaults.
MIT License Β© 2023-PRESENT Johann Schopplich