@boxpdf/reader
v0.2.2
Published
A streaming, memory-bounded PDF reader and structured-data extractor for JavaScript.
Maintainers
Readme
@boxpdf/reader
A streaming, memory-bounded PDF reader and structured-data extractor for JavaScript.
Defining contract
The reader parses and extracts content from large PDFs without buffering the complete file. Explicit byte and object cache budgets govern memory use.
peak parser-owned memory <= configured caches
+ active page window
+ largest permitted decoded object
+ measured fixed overheadEvery input implements a random-access source:
interface PdfSource {
readonly size: number;
read(offset: number, length: number): Promise<Uint8Array>;
}Package boundaries
@boxpdf/reader
├── source random-access file, HTTP, Blob, and memory inputs
├── store bounded sparse byte cache
├── syntax PDF lexer, objects, xref, and page tree
├── content graphics state, fonts, CMaps, and text operations
└── structure optional reading order, paragraphs, lists, and tables
separate output packages
├── @boxpdf/html-writer streaming visual or semantic HTML output
└── render future Canvas, SVG, or bitmap renderingThe core result preserves positioned text and provenance. Semantic structure is inferred separately and includes confidence and diagnostic reasons. A future renderer will consume page operations without becoming a dependency of text or table extraction.
HTML writer
@boxpdf/html-writer streams PDF pages to HTML through an awaited write
callback. The default visual profile preserves display presentation; the
semantic profile turns page geometry into reflowable reading order, nested
sections, paragraphs, lists, cards, label/value groups, and tables. A bounded
document window joins tables continued across pages and removes repeated page
furniture without buffering the complete PDF. See packages/html-writer.
Usage
Read a local file in Node:
import { openPdf } from "@boxpdf/reader";
import { fileSource } from "@boxpdf/reader/node";
const source = await fileSource("large.pdf");
const pdf = await openPdf(source, {
chunkSize: 64 * 1024,
maxBytes: 16 * 1024 * 1024,
maxXrefCacheBytes: 16 * 1024 * 1024,
});
try {
for await (const page of pdf.pages()) {
console.log(page.number, page.spans);
}
console.log(pdf.stats);
} finally {
pdf.close();
await source.close();
}HTTP sources prefer byte-range support. Servers that return a complete HTTP 200 response are also supported: the reader keeps only the requested slice in memory and emits one transfer-efficiency warning per source.
import { httpSource, openPdf } from "@boxpdf/reader";
const pdf = await openPdf(await httpSource("https://example.com/large.pdf"));
const firstPage = await pdf.getPage(0);v1 support
The v1 parser supports:
- classic cross-reference tables, cross-reference streams, and
/Prevchains - compressed object streams and linearized PDFs
- lazy page-tree lookup with inherited media boxes, rotation, and resources
- unfiltered, Flate, and ASCII-hex streams with decoded-size limits
- literal and hexadecimal strings, common text operators, graphics transforms,
WinAnsi text, UTF-16 strings, Type 0 fonts, and common
ToUnicodemappings - Standard 14, explicit, embedded TrueType, and PFA/PFB Type 1 horizontal metrics
- horizontal and vertical CID widths, vertical origins,
TJdisplacement, and top-to-bottom structure grouping - bounded sparse byte caching with source-read and resident-byte telemetry
- a configurable packed-xref byte ceiling with resident-byte telemetry
- semantic HTML with heuristic reading-order and table inference based on repeated text alignment
Encrypted PDFs, damaged-file repair, inline images in content streams, uncommon
stream filters, Type1C/CFF width recovery, named CMap collections without a
ToUnicode map or usable embedded TrueType cmap, OCR, and rendering are outside
the v1 support surface. Unsupported filters and configured resource limits fail
with explicit errors.
Compatibility oracle
PDF.js is the primary behavioral oracle for page geometry, decoded text, text direction, and positioning. Tests compare normalized positioned characters and allow different text-span boundaries. Poppler is an independent secondary oracle for the HTML writer. Its corpus gate preserves exact geometry and decoded-text agreement on every fixture known to agree across both engines. The report records RTL and font-decoding differences between the engines.
The full corpus gate accepts all 118 supported, unencrypted fixtures (100%).
Of those, 115 match PDF.js and three named fixtures intentionally diverge where
the visual oracle confirms improved .notdef, rotated-origin, or UTF-16 behavior.
The two remaining fixtures in the fixed 120-file denominator are encrypted and
intentionally unsupported. This score covers page count,
dimensions, rotation, normalized decoded characters, and the first text origin
on unrotated pages. Focused geometry tests additionally compare span width,
height, font size, and transformed positions. Structure goldens cover reading
order, inferred word boundaries, table rows, and semantic HTML. The scoring limits and the
layout gap found after v0.1.1 are recorded in docs/PARITY_PLAN.md.
Memory tests extract from 10 MB and 1 GB virtual random-access PDFs with the same 64 KiB byte-cache limit. They assert cache residency, maximum individual source reads, total bytes fetched, process RSS, and ArrayBuffer growth.
Memory comparison
pnpm memory:compare runs @boxpdf/reader, raw PDF.js, and unpdf in isolated
processes against the same logical PDF. The default comparison uses 10 MiB and
100 MiB inputs. PDF.js and unpdf receive complete Uint8Array inputs;
@boxpdf/reader receives a random-access source. PDF.js URL and range loading
are separate input modes and are outside this comparison.
One Node 24 run produced these measurements:
| Input | Engine | Peak RSS | RSS growth | ArrayBuffer growth | Source data read |
|---:|---|---:|---:|---:|---:|
| 10 MiB | @boxpdf/reader | 49.26 MiB | 3.64 MiB | 77.06 KiB | 140 KiB |
| 10 MiB | PDF.js | 130.13 MiB | 84.49 MiB | 10.15 MiB | entire input |
| 10 MiB | unpdf | 82.37 MiB | 36.74 MiB | 10.14 MiB | entire input |
| 100 MiB | @boxpdf/reader | 49.32 MiB | 3.64 MiB | 77.06 KiB | 140 KiB |
| 100 MiB | PDF.js | 220.09 MiB | 174.44 MiB | 100.15 MiB | entire input |
| 100 MiB | unpdf | 172.37 MiB | 126.75 MiB | 100.14 MiB | entire input |
Peak RSS includes the Node process baseline, which was about 46 MiB in this run. RSS and allocator behavior vary by operating system and dependency version. The benchmark script is the source of current measurements.
Repository layout
src/source.ts: public random-access source contract and portable sourcessrc/store/: bounded sparse byte storagesrc/syntax/: PDF lexical and object parsingsrc/content/: page content and text interpretationsrc/structure/: reading-order and table inferencesrc/render/: reserved boundary for an optional renderer adaptertest/unit/: focused parser and API teststest/fixtures/: fixture integrity and golden extraction teststest/oracle/: differential PDF.js teststest/memory/: isolated-process memory-bound testsfixtures/: revision-pinned OSS corpus and provenance
Development
pnpm install
pnpm fixtures:fetch
pnpm test:quick
pnpm test:oracle
pnpm test:memory
pnpm quality