@captain-sdk/pdf-parser
v0.4.0
Published
Fast, self-contained PDF layout extraction (Rust + PDFium via WebAssembly). Parses PDFs into structured text, tables (as HTML), and figure placements with normalized bounding boxes, fully in-process: no network, no temp files.
Readme
@captain-sdk/pdf-parser
Fast, self-contained PDF layout extraction for Node.js. A Rust + PDFium engine compiled to WebAssembly turns a PDF into structured blocks: text, tables (as HTML), and figure placements, each with a normalized bounding box. Everything runs in-process: the PDF bytes are handed straight to WebAssembly and never leave your process. No network calls, no temp files, no external service.
Requirements
- Node.js >= 18.3
Install
npm install @captain-sdk/pdf-parserQuick start
const fs = require('fs');
const { initParser, parsePdf } = require('@captain-sdk/pdf-parser');
(async () => {
// Load the engine once at startup (see "Performance" below). Optional but recommended.
await initParser();
const result = await parsePdf(fs.readFileSync('document.pdf'));
console.log(result.pages); // number of pages
console.log(result.has_text_layer); // true if the PDF carries real text
console.log(result.producer); // e.g. "Billing System 2.0"
console.log(result.blocks.length); // extracted layout blocks
})();ESM:
import { initParser, parsePdf } from '@captain-sdk/pdf-parser';Output shape
interface ParseResult {
pages: number;
has_text_layer: boolean;
producer: string | null;
blocks: Block[];
}
interface Block {
type: string; // see block types below
content: string; // text, or HTML for tables, or "" for figures
page: number; // 1-indexed page
left: number; // bounding box, all normalized 0..1 from the top-left
top: number;
width: number;
height: number;
}parsePdf accepts a Uint8Array, Buffer, or ArrayBuffer. It rejects with an
Error when the bytes are not a PDF the engine can open, so you can show a clean
message instead of crashing.
Block types
- Text, Header, Section Header, List Item, Key Value, Footer — text regions, classified by role.
contentis the text. - Table —
contentis HTML (<table><tr><td>...). Both ruled (lined) and unruled (whitespace-aligned) tables are reconstructed. - Figure — marks where a graphic sits, with a bounding box.
contentis empty: figures are located, not rasterized, and there is no OCR. Treat the count as approximate (vector art and raster images can both surface as figures).
Performance
The first call loads ~6 MB of WebAssembly and pays PDFium's one-time init, so it is the slow one (up to a couple of seconds cold). Every parse after that is fast (a few milliseconds for a simple page, ~100–200 ms for a dense multi-page doc).
Call initParser() once at startup so your first real parsePdf is not the call
that eats the init cost. If you skip it, parsePdf initializes lazily on first
use.
Bundlers and serverless
This package ships two runtime assets it loads from disk at call time:
pdfium/pdfium.wasm and lib/globeparse_wasm_bg.wasm. In a plain Node process
(node app.js) this just works.
Bundlers (webpack, esbuild, Next.js) and serverless packagers (AWS Lambda, Vercel)
often do not trace or copy .wasm and sibling assets automatically. If you
bundle or deploy and see a "file not found" or "PDFium engine" error at runtime,
the asset folders were not included in your build. Fixes:
Keep the package external so it loads from
node_modulesat runtime:- esbuild:
--external:@captain-sdk/pdf-parser - Next.js: add
@captain-sdk/pdf-parsertoserverExternalPackages - webpack: mark it in
externals
- esbuild:
Or copy the assets into your deployment and point the parser at them:
await initParser({ assetsDir: '/var/task/pdfium' }); // where you copied pdfium/*assetsDiralso works as the second argument toparsePdf(bytes, { assetsDir }).
PII masking (basic, off by default)
{ maskPii: true } runs a fast, pattern-based redaction over the common fields on
standardized telecom bills (names, phone and account numbers, addresses, TINs). It
is a lightweight local pre-filter, not a comprehensive or compliance-grade PII
engine: it is tuned to specific bill formats and will miss values it has no pattern
for (arbitrary names, non-standard phone or account formats, other document types).
For production-grade PII coverage, run the parsed text through a dedicated PII service such as Google Cloud DLP, or use Captain's server-side masking at index time.
Turn it on inline with the maskPii flag when parsing:
const { parsePdf, parsePdfBatch } = require('@captain-sdk/pdf-parser');
const result = await parsePdf(bytes, { maskPii: true }); // sanitized result
const batch = await parsePdfBatch(items, { maskPii: true }); // each entry sanitizedOr mask standalone with the maskPii primitive, so you can run your own
processing first (or benchmark against another masker) and sanitize after:
const { maskPii } = require('@captain-sdk/pdf-parser');
const masked = maskPii(result); // a ParseResult in, a sanitized ParseResult out
const maskedText = maskPii(someText); // also accepts a raw stringmaskPii returns a sanitized copy and never mutates its input.
Pass types to mask only some entity types (default masks all):
maskPii(result, { types: ['PHONE_NUMBER', 'ACCOUNT_NUMBER'] });Values are replaced with Presidio-style angle-bracket tokens. The types and their
tokens: PERSON → <PERSON>, EMAIL_ADDRESS → <EMAIL_ADDRESS>,
PHONE_NUMBER → <PHONE_NUMBER>, LOCATION → <LOCATION>,
ACCOUNT_NUMBER → <ACCOUNT_NUMBER>, TIN → <TIN>,
INVOICE_NUMBER → <INVOICE_NUMBER>, BARCODE → <BARCODE>. Charge
descriptions, amounts, dates, plan names, and generic labels are preserved.
Licensing
MIT (see LICENSE). The bundled PDFium engine is distributed under its own
BSD-3-Clause license, included as PDFIUM-LICENSE.
