Skip to content

Repository files navigation

@happyvertical/pdf

License: MIT

Node.js PDF utilities for extracting text, reading metadata, extracting or rendering images, running OCR fallback, and generating PDFs from Markdown or HTML.

Features

  • Direct PDF text, metadata, and image extraction through unpdf
  • OCR fallback for scanned or image-based PDFs through @happyvertical/ocr
  • Document analysis with getInfo() strategy recommendations
  • Batched image extraction for large PDFs
  • Web-safe image output from extracted PDF images (webp by default)
  • Markdown-to-PDF generation with pdf-lib and marked
  • HTML-to-PDF generation with puppeteer-core and a system Chromium
  • Typed errors for dependency, file-size, image-collection, and OCR failures

Requirements

  • Node.js >=24
  • ESM imports
  • A Chromium or Chrome binary only when using renderHtmlToPdf
  • Extra memory for OCR-heavy workloads; 2 GB+ is a practical starting point

This package is currently Node-first. Browser provider code exists internally, but the public package export does not expose a stable browser entry point yet.

Installation

pnpm add @happyvertical/pdf
npm install @happyvertical/pdf
yarn add @happyvertical/pdf
bun add @happyvertical/pdf

Basic Usage

import { getPDFReader } from '@happyvertical/pdf';

const reader = await getPDFReader();
const text = await reader.extractText('/path/to/document.pdf');

console.log(text);

extractText() first tries embedded PDF text. When OCR is enabled and direct text extraction finds nothing useful, the Node reader renders pages and sends them through @happyvertical/ocr.

Analyze Before Processing

Use getInfo() when routing many documents or deciding whether OCR is likely to be needed.

import { getPDFReader } from '@happyvertical/pdf';

const reader = await getPDFReader();
const info = await reader.getInfo('/path/to/document.pdf');

console.log({
  pages: info.pageCount,
  hasEmbeddedText: info.hasEmbeddedText,
  hasImages: info.hasImages,
  strategy: info.recommendedStrategy,
});

const text = await reader.extractText('/path/to/document.pdf', {
  skipOCRFallback: info.recommendedStrategy === 'text',
});

Metadata And Images

import { getPDFReader } from '@happyvertical/pdf';

const reader = await getPDFReader();

const metadata = await reader.extractMetadata('/path/to/document.pdf');
console.log(metadata.title, metadata.author, metadata.pageCount);

const images = await reader.extractImages('/path/to/document.pdf');
console.log(images.map((image) => image.format)); // image/webp by default

extractImages() returns WebP images by default so callers can store or serve image.data directly. Pass outputFormat: 'original' when you need the raw provider bytes, especially for OCR pipelines that prefer raw RGB data.

const rawImages = await reader.extractImages('/path/to/document.pdf', {
  outputFormat: 'original',
});

For image-heavy PDFs, process image batches incrementally instead of retaining every image buffer in memory.

await reader.extractImages('/path/to/large-document.pdf', {
  batchSize: 4,
  collect: false,
  onBatch: async ({ images, pages }) => {
    console.log(`Processing ${images.length} images from pages ${pages.join(', ')}`);
  },
});

Explicit OCR

For scanned PDFs, prefer extractText() with OCR fallback. If you need to call OCR yourself, render full pages rather than extracting embedded images.

import { getPDFReader } from '@happyvertical/pdf';

const reader = await getPDFReader({
  defaultOCROptions: {
    language: 'eng',
    confidenceThreshold: 70,
  },
});

const pages = await reader.renderPages('/path/to/scanned.pdf', {
  scale: 2,
  outputFormat: 'original',
});

const result = await reader.performOCR(pages);
console.log(result.text, result.confidence);

Generate PDFs

Markdown

renderMarkdownToPdf() is pure JavaScript. It supports common Markdown blocks and inline emphasis, uses PDF standard fonts, and paginates content.

import { writeFile } from 'node:fs/promises';
import { renderMarkdownToPdf } from '@happyvertical/pdf';

const pdf = await renderMarkdownToPdf('# Status\n\nEverything is green.', {
  title: 'Status Report',
  pageSize: 'letter',
});

await writeFile('status.pdf', pdf);

HTML

renderHtmlToPdf() uses puppeteer-core and never downloads a browser. Install Chromium in the runtime image or set PUPPETEER_EXECUTABLE_PATH.

import { writeFile } from 'node:fs/promises';
import { renderHtmlToPdf } from '@happyvertical/pdf';

const pdf = await renderHtmlToPdf('<h1>Invoice</h1>', {
  format: 'Letter',
  margin: { top: '0.5in', bottom: '0.5in' },
});

await writeFile('invoice.pdf', pdf);

You can also pass executablePath directly or call resolveChromiumExecutablePath() to inspect what binary will be used.

Configuration

import { getPDFReader } from '@happyvertical/pdf';

const reader = await getPDFReader({
  provider: 'auto',
  enableOCR: true,
  timeout: 30_000,
  maxFileSize: 50 * 1024 * 1024,
  ocrProvider: 'auto',
  defaultOCROptions: {
    language: 'eng',
    confidenceThreshold: 70,
  },
});

Environment variables use the HAVE_PDF_ prefix and are merged with explicit options. Explicit options always win.

Environment variable Type Example
HAVE_PDF_ENABLE_OCR boolean true
HAVE_PDF_TIMEOUT number 30000
HAVE_PDF_PROVIDER string auto, unpdf, kreuzberg
HAVE_PDF_OCR_PROVIDER string auto, tesseract, onnx
HAVE_PDF_MAX_FILE_SIZE number 52428800

Providers

  • auto: default. In Node.js, uses the combined unpdf plus OCR reader.
  • unpdf: Node.js text, metadata, image extraction, page rendering, and OCR fallback through @happyvertical/ocr.
  • kreuzberg: optional Node.js provider via @kreuzberg/node; useful when available, but not required.

The pdfjs provider is not a stable public package target yet.

Automatic selection stays on unpdf when enableOCR is false or ocrProvider names a backend Kreuzberg cannot serve, since Kreuzberg is in the chain for its integrated OCR.

Kreuzberg needs an AVX2-capable CPU

The prebuilt @kreuzberg/node binary executes AVX2 instructions, so using it on a pre-AVX2 x86-64 host (x86-64-v2 and below) raises SIGILL and terminates the process. The binary is loaded lazily on the first native call, so any code that merely asks whether the provider works is enough to trigger it. optionalDependencies cannot prevent this: npm and pnpm only skip a package by os, cpu, or libc, and AVX2 is a microarchitecture level rather than an architecture.

This package therefore reads the avx2 flag from /proc/cpuinfo before loading anything native. Automatic selection falls back to unpdf when the flag is absent. Requesting provider: 'kreuzberg' explicitly still returns a reader, and that reader degrades the same way it does for any other missing dependency rather than killing the process: checkDependencies() reports available: false with an error naming AVX2, and extraction calls return their empty result. Check checkDependencies() before relying on this provider.

Non-Linux and non-x86-64 hosts keep attempting the load: their CPU flags are not readable this way, and the fault has only been observed on older Linux servers.

Errors

import {
  PDFDependencyError,
  PDFFileSizeError,
  PDFImageCollectionLimitError,
  PDFOCRFallbackError,
  PDFUnsupportedError,
  getPDFReader,
} from '@happyvertical/pdf';

try {
  const reader = await getPDFReader();
  await reader.extractText('/path/to/document.pdf');
} catch (error) {
  if (error instanceof PDFDependencyError) {
    console.error('Missing dependency:', error.message);
  } else if (error instanceof PDFFileSizeError) {
    console.error('File too large:', error.message);
  } else if (error instanceof PDFImageCollectionLimitError) {
    console.error('Use batched image extraction:', error.message);
  } else if (error instanceof PDFOCRFallbackError) {
    console.error('OCR fallback failed:', error.message);
  } else if (error instanceof PDFUnsupportedError) {
    console.error('Unsupported operation:', error.message);
  } else {
    throw error;
  }
}

Legacy Compatibility

These exports remain for older callers, but new code should use getPDFReader().

import {
  checkOCRDependencies,
  extractImagesFromPDF,
  extractTextFromPDF,
  performOCROnImages,
} from '@happyvertical/pdf';

Development

Lifecycle-protected pull requests merge through the managed merge queue so the synthetic merge revision is revalidated before it reaches main.

pnpm install
pnpm build
pnpm typecheck
pnpm lint
pnpm test
pnpm test:coverage
pnpm docs:api
pnpm docs:api:check
pnpm pack --dry-run

The package ships generated JavaScript and declaration files from dist/. Run pnpm build to refresh the declarations before inspecting the full API surface locally. The generated API reference lives in docs/api/; run pnpm docs:api after changing public exports or JSDoc, and run pnpm docs:api:check to verify the committed reference is current.

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages