npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

extract-pdf

v0.1.378

Published

Convert a PDF (URL or ArrayBuffer) into clean HTML with structural tagging — headings, lists, footnotes, code blocks, bold/italic. Slim by default (PDF.js loads at runtime from the pdfjs-serverless CDN build), with optional Granite Docling OCR for pages w

Readme

extract-pdf

When users upload a PDF, they expect an instant chat response, not to wait for 5 min on OCR model.

Instant no-backend-needed javascript to convert a PDF (URL or ArrayBuffer) into clean HTML with structural tagging — headings, lists, footnotes, code blocks, bold/italic, and Table of Contents entries. Works in Node.js, Cloudflare Workers, and browser environments via pdfjs-serverless.

Slim by default: the package bundles no PDF engine. PDF.js is loaded lazily at runtime from jsDelivr's ESM build of pdfjs-serverless (https://cdn.jsdelivr.net/npm/pdfjs-serverless@1/+esm, pinned to the major version) — a zero-dependency, single ~1.6 MB minified redistribution of Mozilla PDF.js built for serverless/edge runtimes. Node.js and Bun can't import remote URLs, so there the loader falls back to the locally-installed pdfjs-serverless optional dependency. Everything OCR-related (Granite Docling via @huggingface/transformers, @napi-rs/canvas) is likewise optional and imported only when actually used.

For OCR-grade fidelity on pages with infographics, charts, and tables, the package also ships the Granite Docling path (merged from the former extract-pdf-docling package): run all pages through the frontend JS parser, all pages through Docling OCR, a hybrid mode that regex-scans pages and OCRs only the ones that need it, or point at the URL of a remote docling-compatible processor (see server/).

Install

bun add extract-pdf

Usage

import { convertPDFToHTML } from "extract-pdf";

const { html, title, author } = await convertPDFToHTML(
  "https://example.com/paper.pdf",
);
// or pass an ArrayBuffer from fs.readFile / fetch
const { html } = await convertPDFToHTML(buffer, { addPageNumbers: true });

Options

| Option | Default | Description | | ----------------- | ---------------------- | ------------------------------------------------------------------------------------------ | | addPageNumbers | false | Inserts [n] markers at each page boundary | | addCitation | true | Reads PDF metadata and first-page heading to populate title/author in the return value | | method | "ts-block-algorithm" | Parsing engine — "ts-block-algorithm", "liteparse", or "liteparse-wasm" (see below) | | liteParseOptions| {} | Passed through to LiteParse's constructor when method is "liteparse" or "liteparse-wasm" | | processor | "frontend" | Where OCR happens — "frontend", "hybrid", "docling", or a processor URL (see below) | | processorUrl | — | Remote docling-compatible API used by "hybrid"/"docling" instead of the in-process model | | ocrScanOptions | {} | Threshold tuning for the hybrid page scan (scanPagesForOCR) | | doclingOptions | {} | prompt, maxTokens, scale for the Docling OCR model |

Return value

{ html: string, title?: string, author?: string, format: "pdf",
  processor: string, ocrScan: OcrScanResult }

Parse methods

convertPDFToHTML supports three interchangeable parsing engines via options.method:

| Method | Engine | Environments | OCR | | ------------------------------------ | -------------------------------------------------------------------- | ----------------------------------- | --- | | "ts-block-algorithm" (default) | The pure-TS pipeline documented below (this package) | Node.js, Cloudflare Workers, browser | No | | "liteparse" | LiteParse (native, @llamaindex/liteparse) | Node.js only | Optional | | "liteparse-wasm" | LiteParse (WASM, @llamaindex/liteparse-wasm) | Node.js, Cloudflare Workers, browser | Optional (via callback) |

import { convertPDFToHTML } from "extract-pdf";

const { html } = await convertPDFToHTML(buffer, { method: "liteparse" });

// Or the WASM build, which also runs in browsers and Cloudflare Workers:
const { html } = await convertPDFToHTML(buffer, { method: "liteparse-wasm" });

LiteParse ships a native (napi) addon, so method: "liteparse" only runs in Node.js — it is not bundled into browser or Cloudflare Workers builds. Install it explicitly (bun add @llamaindex/liteparse) since it's an optional dependency; if it isn't installed, convertPDFToHTML returns { error } instead of throwing.

method: "liteparse-wasm" delegates to LiteParse's WebAssembly build instead, which runs anywhere WASM does — browsers, Cloudflare Workers, and Node.js. Install it explicitly (bun add @llamaindex/liteparse-wasm) since it's also an optional dependency; if it isn't installed, convertPDFToHTML returns { error } instead of throwing. The WASM build has no OCR engine built in — pass a liteParseOptions.ocrEngine callback (e.g. backed by tesseract-js) to enable OCR.

By default both LiteParse paths run with OCR disabled (ocrEnabled: false) — matching this package's "instant, no backend" philosophy. Use detectPdfNeedsOcr (below) to decide when a document is worth re-parsing with liteParseOptions: { ocrEnabled: true }.

Processor modes: frontend, hybrid, Docling OCR, or a remote URL

Separately from method, the processor option decides whether pages go through the Granite Docling OCR model — which preserves layout, tables, charts, code, and formulas at the cost of model inference per page:

| processor | What happens | | ----------------------- | --------------------------------------------------------------------------------------------------------- | | "frontend" (default) | All pages parsed by the pure-JS text-layer pipeline. No OCR, no model, instant. | | "hybrid" | JS pipeline everywhere + a regex scan (scanPagesForOCR) flags pages with infographics/figures/tables or no usable text layer; only those pages are rasterized and OCR'd with Docling. | | "docling" | Every page rasterized and OCR'd with Docling. | | "https://..." (URL) | Like "docling", but every page is POSTed to that docling-compatible processor API instead of running the model in-process. |

// Hybrid: fast JS parse, OCR only the pages that need it
const { html, ocrScan } = await convertPDFToHTML(buffer, { processor: "hybrid" });
console.log(ocrScan.pagesNeedingOcr); // e.g. [3, 7] — pages with tables/figures
// each ocrScan.pages[i] lists reasons: "table-caption", "figure-caption",
// "table-rows", "numeric-grid", "no-text", "sparse-text", "garbled-text"

// All pages through a remote Docling processor (this package's server/):
const { html } = await convertPDFToHTML(buffer, {
  processor: "http://localhost:3000",
});

// Hybrid with the OCR offloaded to a remote processor:
const { html } = await convertPDFToHTML(buffer, {
  processor: "hybrid",
  processorUrl: "http://localhost:3000",
});

OCR'd pages are emitted as <section class="ocr-page" id="page-N">…</section> (tables become real <table> markup, captions <figcaption>, formulas <code class="formula">, …); pages whose OCR fails keep their JS-parsed HTML.

Running Docling in-process requires the optional @huggingface/transformers dependency (the ONNX model onnx-community/granite-docling-258M-ONNX, ~1 GB, downloads on first use) and — in Node.js — @napi-rs/canvas for page rasterization (browsers/Workers use OffscreenCanvas).

The regex scanner is also exported standalone, and so are the OCR helpers:

import { scanPagesForOCR, doctagsToHtml, ocrImageWithDocling } from "extract-pdf";

const scan = scanPagesForOCR(pageTexts); // string[] of per-page text
// { needsOcr, pagesNeedingOcr: number[], pages: [{ page, reasons, captions }] }

Running the Docling processor server

The server/ folder contains the Hono HTTP API (formerly the extract-pdf-docling package) exposing the Granite Docling model:

bun run serve:docling   # or dev:docling for --watch; port 3000
  • Swagger UI: http://localhost:3000/docs, spec at /openapi.json
  • POST /api/v1/convert{ imageUrl, prompt?, maxTokens? }
  • POST /api/v1/convert-base64{ imageBase64, mimeType?, prompt?, maxTokens? }
  • POST /api/v1/convert-stream — SSE token stream
  • GET /health

A server/wrangler.jsonc is included for deploying it as a Cloudflare Worker. Any deployment of it (or any API with the same contract) can be passed as the processor/processorUrl option above.

Detecting whether a PDF needs OCR

Before committing to a full (and potentially slow) OCR parse, detectPdfNeedsOcr runs a cheap, text-layer-only pass and reports whether each page needs OCR or other heavy parsing — useful for routing documents to different pipelines (fast path vs. OCR vs. screenshots vs. a heavier parser like LlamaParse or Docling):

import { detectPdfNeedsOcr, convertPDFToHTML } from "extract-pdf";

const assessment = await detectPdfNeedsOcr(buffer);
// { needsOcr: boolean, pages: PageComplexityStats[], reasons: string[] }

if (!assessment.needsOcr) {
  const { html } = await convertPDFToHTML(buffer, { method: "liteparse" });
} else {
  console.log("Needs OCR:", assessment.reasons); // e.g. ["scanned", "sparse-text"]
  // Route to an OCR-enabled pipeline, e.g.:
  const { html } = await convertPDFToHTML(buffer, {
    method: "liteparse",
    liteParseOptions: { ocrEnabled: true },
  });
}

reasons collects every distinct signal found across pages: "scanned", "no-text", "sparse-text", "embedded-images", "garbled", or "vector-text". Like method: "liteparse", this is Node.js only and requires @llamaindex/liteparse.

Pipeline

The conversion runs a sequential chain of transformations on a ParseResult (pages → items):

Raw pdfjs text spans
  → CalculateGlobalStats   — font heights, distances, format map
  → CompactLines           — merge spans on the same y-line into LineItems
  → RemoveRepetitiveElements — strip recurring page headers/footers
  → VerticalToHorizontal   — rotate vertical character runs
  → DetectTOC              — identify Table of Contents pages, link headings
  → DetectHeaders          — classify items as H1–H6 by font height
  → DetectListItems        — detect bullet/numbered list items
  → GatherBlocks           — group adjacent same-type lines into blocks
  → DetectCodeQuoteBlocks  — mark indented blocks as CODE
  → DetectListLevels       — add indentation for nested list levels
  → ToTextBlocks           — flatten blocks to { category, text } pairs
  → ToHTML                 — render pairs as <p>, <h1>–<h6>, <ul>, <code>

Folder structure

src/
  pdf-to-html.ts          — main entry point (convertPDFToHTML)
  liteparse-to-html.ts    — "liteparse" method (native napi addon)
  liteparse-wasm-to-html.ts — "liteparse-wasm" method (WASM, browser/edge)
  ocr-page-scan.ts        — regex scan flagging infographic/table pages for OCR
  docling-ocr.ts          — Granite Docling OCR (local ONNX or remote URL),
  │                         page rasterization, doctags → HTML
  detect-needs-ocr.ts     — LiteParse-based OCR routing (Node.js only)
  models/                 — data classes: Page, ParseResult, TextItem,
  │                         LineItem, LineItemBlock, Word, BlockType, …
  transforms/
  │  base/                — abstract Transformation, ToLineItem*, ToLineItemBlock*
  │  line-item/           — per-line-item transformations
  │  block/               — per-block transformations
  │  calculate-global-stats.ts
  │  to-text-blocks.ts
  │  to-html.ts
  utils/
     load-pdfjs.ts        — lazy CDN loader for pdfjs-serverless
     string-functions.ts
     page-item-functions.ts
     page-number-functions.ts
server/                   — Granite Docling HTTP processor (Hono):
   server.js, routes.js, schemas.js, model.js, wrangler.jsonc