Library / SDK
unjs/unpdf avatar
unjs/unpdf

unpdf: PDF text extraction that runs on Node, Deno, Bun and the edge

📄 PDF extraction and rendering across all JavaScript runtimes

1,254 stars49 forksTypeScriptMIT

At a glance

What is it?
unpdf wraps Mozilla's PDF.js in a serverless bundle with the worker inlined and the missing globals polyfilled, so the same extraction code runs in a Node process, a browser, a Cloudflare Worker or a Bun script.
Who is it for?
unpdf fits a JavaScript team that needs to read PDFs in a serverless function, an edge worker or a browser, especially one feeding documents into a model for summarisation, and it fits a team that has been bitten by pdf-parse and wants a maintained alternative. It is a poor fit if you only run in Node, because there the extra bundle work buys you nothing, and a poor fit if you need rendering fidelity in a browser, since extraction is the focus.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the package actually wraps

unpdf is a set of utilities for PDF extraction and rendering across JavaScript runtimes, including Node.js, Deno, Bun, the browser, and serverless environments such as Cloudflare Workers. The stated use case is AI applications that need to summarise or analyse PDF documents.

The position against the incumbent is stated too. If you are coming from pdf-parse, unpdf is described as a modern, actively maintained alternative with broader runtime support.

The features are modest. It works in Node.js, the browser and serverless environments, it includes a serverless build of PDF.js, it extracts text, links and images from PDF files, it is aimed at AI applications and PDF summarisation, and it lets you opt in to the official or legacy PDF.js build.

That last item is the one that distinguishes it from a thin wrapper. The package ships its own bundled build of PDF.js, and if you need the official distribution or the older legacy one you can define it yourself before anything else runs.

The project is MIT licensed, the primary language is TypeScript, the last push was on 2026-10-15, and the releases are v1.9.0 on 2026-10-15, v1.8.1 on 2026-08-13 and v1.8.0 on 2026-07-24. The author is Johann Schopplich, and the package is part of the unjs organisation.

The bundle, and the three techniques that make it work

The explanation of how it works is the part that tells you whether it will run in your environment, and the configuration file named as the heart and soul of the package is pdfjs.rolldown.config.ts.

It uses Rolldown to bundle PDF.js into a single file for serverless environments, and there are three techniques involved.

String replacements strip browser-specific references from the PDF.js source. PDF.js upstream assumes a browser, so things that do not exist in a worker have to be removed rather than shimmed.

Worker inlining embeds the PDF.js worker directly into the main bundle, since serverless runtimes cannot load separate worker files. This is the technique that makes a single-file distribution possible at all, and it also means the worker cannot be cached or started separately from the main module.

Global polyfills provide missing APIs like Promise.withResolvers and FinalizationRegistry, which are unavailable in Node.js below 22 and in Cloudflare Workers respectively. The polyfills are why the bundled build behaves where the official build does not.

There is a version note worth taking seriously. The serverless PDF.js bundle is built from PDF.js v6.4.299, and it requires the global Iterator constructor, which the documentation dates to Safari 18.4 and later. Without it, importing the bundle throws, so an older Safari will fail at import time rather than at first use.

And the corresponding warning for the official build: PDF.js v5.x uses Promise.withResolvers, which may not be supported in all environments, such as Node below 22. The advice is to use the bundled serverless build, which includes a polyfill, or to use an older version of PDF.js.

Extracting text, and the option that changes the shape of the result

The main path is two functions. You load the document into a proxy, then extract from it.

ts
import { 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)

The option that matters is mergePages. If it is set to true, the text of all pages is merged into a single string with line breaks preserved and at most one blank line in a row. Otherwise you get an array of strings, one per page.

That difference is the whole design decision for an AI use case, because a single string is what you can hand to a summarisation prompt without a chunking step, and the at most one blank line rule is there to stop the page separators from inflating the token count of a document whose pages are mostly blank.

The extraction helpers go further than plain text. getMeta extracts metadata, and with parseDates set to true the date properties are parsed into Date objects rather than left as strings. extractTextItems extracts text with layout information, one array of positioned items per page, and each item carries the string, the x and y position in PDF coordinate space with the origin at bottom-left, the width and height, the font size, the font family, and the text direction, which is ltr, rtl or ttb. The documentation says it is useful when plain text is not enough and you need coordinates, font sizes or reading direction, for example for table detection or positional parsing. There is also extractLinks and extractImages in the feature list, and the API reference covers the rest.

Every extraction method accepts either raw data or an existing proxy, so you can load a document once and run several extractions against it.

Swapping the PDF.js build, and dropping to the raw API

Usually you do not need to worry about the PDF.js build, because unpdf ships the serverless build of the latest PDF.js version. But the escape hatch is there, and it has to be used in a specific order.

ts
import { definePDFJSModule, extractText, getDocumentProxy } from 'unpdf'

// Define the PDF.js build before using any other method
await definePDFJSModule(() => import('pdfjs-dist'))

// Now, you can use all unpdf methods with the official PDF.js build
const pdf = await getDocumentProxy(/* … */)
const { text } = await extractText(pdf)

The comment is the requirement: define the build before using any other method. If you call extractText first, the serverless build has already been resolved and your definition comes too late.

When you need more than the helper methods, getResolvedPDFJS gives you the resolved PDF.js module directly:

ts
import { getResolvedPDFJS } from 'unpdf'

const { version } = await getResolvedPDFJS()

If no other build was defined, the serverless build is always used. With the module in hand you can drive PDF.js yourself:

ts
import { readFile } from 'node:fs/promises'
import { getResolvedPDFJS } from 'unpdf'

const { getDocument } = await getResolvedPDFJS()
const data = await readFile('./dummy.pdf')
const document = await getDocument(new Uint8Array(data)).promise

console.log(await document.getMetadata())

There is also a getDocumentProxy helper that creates a PDFDocumentProxy from binary PDF data and applies sensible defaults: useSystemFonts is true, in Node.js disableFontFace is additionally true, and standardFontDataUrl plus cMapUrl and cMapPacked are resolved from the local pdfjs-dist package so standard fonts and CJK character maps work without a network fetch.

The packaging, the test matrix, and the runtime floor

The package manifest shows how much attention went into the shipping shape, which is what determines whether a library survives contact with a real build pipeline.

It is an ES module with sideEffects false, and the exports map is thorough. The root export carries separate import and require conditions with their own type declarations, pointing at dist/index.mjs and dist/index.cjs, and there is a second export path for the PDF.js bundle itself at./pdfjs. The build is tsdown followed by a post-build script, and the PDF.js bundle is built separately with rolldown against pdfjs.rolldown.config.ts, after which the PDF.js type definitions are copied out of node_modules.

The test setup is where the runtime claims get checked. Vitest is the test runner, there is a separate type test with tsc --noEmit and another against the built types in test/types, and there is a test:attw script that runs attw with the cjs-resolves-to-esm rule ignored. That is a sign the author cares about the CommonJS and ESM interop story, which is where a package like this usually breaks.

The runtime floor is node >=22 in the engines field, and the reason for it is in the PDF.js version, since Promise.withResolvers and the Iterator constructor both need a modern runtime. The repository has an examples directory with a cloudflare folder, and the top level holds an eslint config in TypeScript form, a vitest config, a tsdown config, a pnpm workspace file and a scripts directory.

For an adopter, the practical check is which runtime you are deploying to and whether it has the Iterator constructor, because that is the one that throws at import time.

Editorial conclusion

unpdf fits a JavaScript team that needs to read PDFs in a serverless function, an edge worker or a browser, especially one feeding documents into a model for summarisation, and it fits a team that has been bitten by pdf-parse and wants a maintained alternative. It is a poor fit if you only run in Node, because there the extra bundle work buys you nothing, and a poor fit if you need rendering fidelity in a browser, since extraction is the focus. Before adopting, check that your runtime has the Iterator constructor, which the bundled PDF.js build requires, and pin the Node version accordingly since the package declares node >=22.

Frequently asked questions

What is unpdf and what does it do?

It is a set of utilities for PDF extraction and rendering across JavaScript runtimes, including Node.js, Deno, Bun, the browser and serverless environments like Cloudflare Workers. It ships with a serverless build of Mozilla's PDF.js and provides helpers to extract text, links, images and metadata, and it is aimed at AI applications that summarise or analyse PDF documents.

How do I extract text from a PDF with unpdf?

Load the document with getDocumentProxy and pass it to extractText. With the mergePages option set to true the pages are merged into one string with line breaks preserved and at most one blank line in a row, and without it you get an array of strings, one per page.

Why does the unpdf serverless build need polyfills?

Because the runtime is missing APIs that PDF.js expects. Global polyfills provide Promise.withResolvers and FinalizationRegistry, which are unavailable in Node.js below 22 and in Cloudflare Workers respectively, and the worker is inlined into the main bundle because serverless runtimes cannot load separate worker files.

Can I use the official PDF.js build with unpdf?

Yes, by calling definePDFJSModule with an import of pdfjs-dist before you use any other unpdf method. The order matters, because once the serverless build has been resolved your definition comes too late, and PDF.js v5.x uses Promise.withResolvers which may be unsupported on Node below 22.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. unjs/unpdf on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/unjs-unpdf.svg)](https://hysenlabs.com/projects/unjs-unpdf)

Was this useful?