How to extract text from PDFs server-side with PDF.js in Node.js
Table of contents
pdfjs-dist in Node.js to extract text from PDFs without a browser. This tutorial covers basic text extraction, the TextContent item structure, parallel page processing, and CMap/font configuration.
PDF.js’s pdfjs-dist package runs in Node.js and can extract structured text from PDFs without rendering. The setup has four moving parts:
- Load the document with
getDocument({ data }), passing aUint8Arrayfrom yourBuffer. - Resolve
standardFontDataUrlandcMapUrlfrom thepdfjs-distpackage on disk. - Walk pages and call
page.getTextContent()— filter for items with"str" in item. - Skip the worker configuration (no-op in Node.js) and polyfill
Promise.withResolversif your Node version lacks it.
For parallel processing, wrap each page in a promise and Promise.all. If you’d rather skip the path-resolution dance, Nutrient Web SDK exposes textLinesForPageIndex in one call and Document Engine handles batch extraction over REST (representational state transfer).
PDF.js isn’t just for browsers. You can use pdfjs-dist in Node.js to extract text from PDFs without any rendering. This is useful for search indexing and content analysis.
Install
Install the pdfjs-dist package:
npm install pdfjs-distBasic text extraction
The following example loads a PDF buffer, walks each page, and returns an array of per-page text strings:
import { getDocument, PDFDocumentProxy } from "pdfjs-dist";import path from "path";import { createRequire } from "node:module";
// Resolve paths for CMap and font data from the pdfjs-dist package.const require = createRequire(import.meta.url);const pdfjsDistPath = require.resolve("pdfjs-dist");const fontDataPath = path.join(path.resolve(pdfjsDistPath, "../../standard_fonts"), "/");const cmapPath = path.join(path.resolve(pdfjsDistPath, "../../cmaps"), "/");
async function extractText(pdfBuffer) { const uint8Array = new Uint8Array(pdfBuffer);
const pdf = await getDocument({ data: uint8Array, standardFontDataUrl: fontDataPath, cMapUrl: cmapPath, }).promise;
const pageTexts = []; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const textContent = await page.getTextContent();
const pageText = textContent.items.reduce((text, item) => { if ("str" in item && "hasEOL" in item) { return text + item.str + (item.hasEOL ? "\n" : ""); } return text; }, "");
pageTexts.push(pageText); }
return pageTexts;}Understanding TextContent items
page.getTextContent() returns an object with an items array. Each item has:
{ str: "Hello world", // The text string. hasEOL: true, // Whether it's followed by a line break. dir: "ltr", // Text direction. width: 120.5, // Width in PDF points. height: 12, // Height in PDF points. transform: [12, 0, 0, 12, 72, 700], // Position/transform matrix.}Filter for items with str and hasEOL — some items are positioning markers without text.
Parallel page processing
For large PDFs, extract pages in parallel. The snippet below uses the pdf document from the basic example above:
const textPromises = [];for (let i = 1; i <= pdf.numPages; i++) { textPromises.push( pdf.getPage(i) .then((page) => page.getTextContent()) .then((textContent) => textContent.items.reduce((text, item) => { if ("str" in item && "hasEOL" in item) { return text + item.str + (item.hasEOL ? "\n" : ""); } return text; }, ""), ), );}const allTexts = await Promise.all(textPromises);Configuration notes
PDF.js requires three adjustments when running in Node.js that differ from a browser setup.
CMap and font paths
In Node.js, you need to resolve these paths from the pdfjs-dist package itself:
const pdfjsDistPath = require.resolve("pdfjs-dist");const fontDataPath = path.join(path.resolve(pdfjsDistPath, "../../standard_fonts"), "/");const cmapPath = path.join(path.resolve(pdfjsDistPath, "../../cmaps"), "/");These are directory paths to the bundled CMap and font files — required for proper text extraction from Chinese, Japanese, and Korean (CJK) and non-standard fonts.
No worker needed
In Node.js, you don’t need to configure GlobalWorkerOptions.workerSrc. PDF.js falls back to running in the main thread, which is fine for server-side batch processing.
Polyfill
If your Node.js version doesn’t support Promise.withResolvers, add a polyfill:
import "core-js/proposals/promise-with-resolvers";Key points
pdfjs-distworks in Node.js without a browser or canvas.getDocument({ data })acceptsUint8Arrayfrom aBuffer.page.getTextContent()gives you structured text with position metadata.- Filter text items for
"str" in itemto skip non-text markers. - No worker configuration is needed for server-side use.
- Always configure CMap and standard font paths for proper text extraction.
- Text extraction is the foundation for search indexing and content analysis.
FAQ
Yes. pdfjs-dist works in Node.js for text extraction, metadata reading, and structural inspection. The catch is that rendering (canvas painting) requires either a browser environment or an emulated canvas package, so use pdf-lib, a headless browser, or a server-side renderer for that. Text extraction itself has no rendering requirement.
In the browser, you typically point cMapUrl and standardFontDataUrl at a content delivery network (CDN). In Node.js, there’s no URL to fetch from, so you have to resolve the bundled cmaps and standard_fonts directories from inside the pdfjs-dist package. Without them, text extraction from PDFs with CJK or embedded standard fonts will return blanks or mojibake.
Promise.withResolvers and why do I need to polyfill it?Promise.withResolvers is a recent JavaScript API (Stage 4, available in Node 22+) that PDF.js uses internally. On older Node.js versions, you’ll get a ReferenceError at import time. The core-js/proposals/promise-with-resolvers polyfill adds it. Node 22+ users can skip the polyfill.
textContent.items missing the str field?getTextContent() returns a mixed array: Most items are text fragments with a str field, but some are positioning markers used internally to recreate the page’s reading order. Filter with "str" in item to skip them. The hasEOL flag tells you when an item should be followed by a line break in your assembled output.
worker.destroy() or loadingTask.destroy() after extraction?Yes, for long-running processes. PDF.js holds the parsed document tree in memory, and on a busy server, you’ll leak across requests if you don’t release it. Call await loadingTask.destroy() (or pdf.destroy()) when you’re done with each PDF. For short-lived scripts, process exit cleans up automatically.
Nutrient Web SDK exposes instance.textLinesForPageIndex(pageIndex) for client-side extraction with one call — no CMap setup, no item filtering. For server-side batch jobs, Document Engine provides a REST API for text extraction, OCR, redaction, and content analysis without bundling PDF.js into your service.
How Nutrient Web SDK handles this
No Node.js PDF.js setup, no CMap path resolution, and no polyfills are required. Nutrient Web SDK extracts text from any page with a single API call:
// Extract text from a page — one line.const textLines = await instance.textLinesForPageIndex(0);console.log(textLines.map((line) => line.contents).join("\n"));For server-side processing, Nutrient Document Engine provides text extraction, optical character recognition (OCR), and content analysis through a REST API, with no extra binary dependencies on the host.
See Nutrient Web SDK for an alternative to wiring up PDF.js in Node. The text extraction and OCR guides cover the API in depth. Follow the migration guide to switch, or talk to Sales about your requirements.