@okrapdf/pdfdom
v0.2.0
Published
Local CSS-like queries over tagged PDF structure trees
Maintainers
Readme
PDF DOM
A small, fully local query layer over a tagged PDF's native structure tree.
PDF DOM reads StructTreeRoot, preserves the semantic hierarchy, resolves
marked-content IDs (MCIDs) to text and bounding boxes, and exposes familiar
CSS/jQuery-like selectors. It does not upload documents or guess structure.
Scope
This first version is intentionally narrow:
- Input must be a properly tagged/accessibility-annotated PDF.
- Native roles and role mappings are preserved, including
Document,Part,Sect,H1-H6,P, lists, tables, figures, captions, links, notes, and form/field roles when they exist in the tree. /Alt,/ActualText,/Lang, MCIDs, pages, native attributes, and bounding boxes are exposed without a service or API key.- It is not OCR, an untagged-PDF layout classifier, an editor, or a cloud extraction service.
Command line
The public query command lives in the complementary pdfquery package. It
loads PDF bytes and calls this library's native entry point directly:
npx --yes pdfquery@latest ./report.pdf 'H1'
npx --yes pdfquery@latest ./report.pdf 'Sect > P' -o text
npx --yes pdfquery@latest ./report.pdf 'Table TD' -o size
npx --yes pdfquery@latest ./report.pdf 'Figure[alt*="chart"]' -a alt
cat ./report.pdf | npx --yes pdfquery@latest 'P:contains("revenue")' -o textpdfdom owns the reusable document/layer model and selector semantics;
pdfquery owns file and stdin I/O, CLI arguments, output formatting, and exit
codes. The native path contains no parser scheduling, registry, plugin, OCR,
inference, cloud, or host abstraction.
Selector grammar
The MVP implements the smallest coherent structural subset:
| Form | Example | Meaning |
|---|---|---|
| role/type | H1 | Native or RoleMap-resolved structure role |
| descendant | Table TD | TD anywhere below Table |
| child | Sect > P | P directly below Sect |
| comma group | H1, H2 | Ordered, de-duplicated union |
| equality | [lang=en-US] | Exact attribute value |
| contains | [alt*="chart"] | Attribute contains text |
| prefix | [lang^=en] | Attribute starts with text |
| text predicate | P:contains("revenue") | Aggregate node text contains text |
| page scope | page[page=4] H2 | Semantic node touching page 4 |
Role names are matched case-insensitively. Attribute and :contains() values
are case-sensitive, matching CSS/jQuery expectations. Page handles are virtual
query scopes; they do not reparent or duplicate the native semantic tree.
TypeScript API
import { readFile } from 'node:fs/promises';
import { openTaggedPdf } from '@okrapdf/pdfdom/native';
const bytes = new Uint8Array(await readFile('./report.pdf'));
const document = await openTaggedPdf(bytes);
const headings = document.query('page[page=4] H2');
for (const heading of headings) {
console.log({
role: heading.role,
text: heading.text,
page: heading.page, // null when the node spans pages
pages: heading.pages,
mcids: heading.mcids,
bbox: heading.bbox, // null when the node spans pages
bboxes: heading.bboxes,
parent: heading.parent?.role,
children: heading.children.map((child) => child.role),
alt: heading.altText,
actualText: heading.actualText,
language: heading.language,
raw: heading.rawAttributes,
});
}document.root is the canonical native hierarchy. document.pages contains
virtual page handles for page-scoped selectors. The same native symbols remain
re-exported from the package root for compatibility, but /native is the
focused dependency boundary for new direct-PDF consumers.
Current limitations
- Untagged PDFs fail with
UntaggedPdfError; there is deliberately no fallback OCR or inferred hierarchy. - Native
/A/BBoxis preferred. When it is absent, text boxes are approximate normalized axis-aligned unions derived from PDF.js text items. - A node's singular
pageandbboxarenullwhen it spans multiple pages; usepagesandbboxesinstead. - Structure object/annotation references are preserved, but referenced widget values and annotation appearance text are not expanded in this MVP.
- PDF.js's public marked-content stream identifies page + MCID, not the
/Stmreference. PDFs that reuse one MCID across multiple form XObjects on the same page may have ambiguous text resolution. - Password-protected PDFs are not currently accepted.
Development
npm test
npm run typecheck
npm run buildThe native test suite generates a deterministic tagged PDF in memory, so StructTreeRoot traversal, MCID/text, metadata, bounding boxes, and selector behavior are covered without a network fixture.
License
MIT
