pdf-inspector 1.25.2

Fast PDF inspection, classification, and text extraction with smart scanned vs text-based detection
Documentation

pdf-inspector

Fast PDF classification and text extraction. The default build detects whether a PDF is text-based or scanned, extracts text with position awareness, and converts to clean Markdown without OCR. It is pure Rust, has no ML models or external services, and uses lopdf for PDF parsing. Native Rust and CLI consumers can opt into selective OCR. Also available for Python and Node.js.

Built by Firecrawl to handle text-based PDFs locally in under 200ms, skipping expensive OCR services for the ~54% of PDFs that don't need them.

Features

  • Smart classification — TextBased / Scanned / ImageBased / Mixed in ~10–50ms, with a confidence score and per-page OCR routing.
  • Markdown conversion — headings, lists, code blocks, bold/italic, URL linking, and dual-mode table detection (PDF drawing ops + text-alignment heuristics).
  • Layout-aware extraction — multi-column reading order, position and font info per text item, RTL support.
  • Robust text decoding — CID/Type0 fonts via ToUnicode CMaps, plus automatic flagging of broken encodings so callers can fall back to OCR.
  • Lightweight — pure Rust, no ML models, no external services; single PDF dependency (lopdf).

Benchmark

opendataloader-bench corpus (200 PDFs), local engines without model-based PDF parsing; OCR disabled. Scores 0–1, higher is better:

Engine Overall Reading order Tables (TEDS) Headings Speed
pdf-inspector 0.875 0.915 0.814 0.788 0.470s
liteparse 0.873 0.913 0.693 0.811 0.750s
opendataloader 0.831 0.902 0.489 0.739 2.569s
pymupdf4llm 0.735 0.886 0.401 0.424 17.117s
markitdown 0.589 0.844 0.273 0.000 16.165s

Refreshed July 31, 2026, on Apple M4 Pro; speed is the median of five complete corpus runs after an excluded warm-up. Full methodology and versions are in the repo README, with raw timings and artifacts in the results branch.

Install

cargo add pdf-inspector

For the latest unreleased changes, use the git dependency instead:

[dependencies]
pdf-inspector = { git = "https://github.com/firecrawl/pdf-inspector" }

The crate also ships CLI binaries — pdf2md (PDF → Markdown, with --json, --pages, --select-pages, and the opt-in token-saving --compact profile) and detect-pdf (classification, with --analyze --json):

cargo install pdf-inspector

Usage

Detect and extract in one call:

use pdf_inspector::process_pdf;

let result = process_pdf("document.pdf")?;

println!("Type: {:?}", result.pdf_type);       // TextBased, Scanned, ImageBased, Mixed
println!("Confidence: {:.0}%", result.confidence * 100.0);
println!("Pages: {}", result.page_count);

if let Some(markdown) = &result.markdown {
    println!("{}", markdown);
}

Fast metadata-only detection (no text extraction or markdown generation):

use pdf_inspector::detect_pdf;

let info = detect_pdf("document.pdf")?;

match info.pdf_type {
    pdf_inspector::PdfType::TextBased => {
        // Extract locally — fast and free
    }
    _ => {
        // Route to OCR service
        // info.pages_needing_ocr tells you exactly which pages
    }
}

Customize processing with PdfOptions:

use pdf_inspector::{process_pdf_with_options, PdfOptions, ProcessMode, DetectionConfig, ScanStrategy};

// Analyze layout without generating markdown
let result = process_pdf_with_options(
    "document.pdf",
    PdfOptions::new().mode(ProcessMode::Analyze),
)?;

// Full extraction with custom detection strategy
let result = process_pdf_with_options(
    "large.pdf",
    PdfOptions::new().detection(DetectionConfig {
        strategy: ScanStrategy::Sample(5),
        ..Default::default()
    }),
)?;

// Process only specific pages
let result = process_pdf_with_options(
    "document.pdf",
    PdfOptions::new().pages([1, 3, 5]),
)?;

Process from a byte buffer (no filesystem needed):

use pdf_inspector::process_pdf_mem;

let bytes = std::fs::read("document.pdf")?;
let result = process_pdf_mem(&bytes)?;

Vision extension contracts

The native-only vision feature exposes the stable seam used by OCR integrations without selecting or embedding an inference runtime. The separate model-cache feature adds pinned artifact management:

  • PageRenderer and OcrEngine traits;
  • renderer-neutral owned page buffers and affine pixel↔PDF transforms;
  • OcrOptions and opt-in Off/Auto/Force routing modes;
  • positioned OCR results and per-page provenance types; and
  • a versioned PP-OCRv6 Small manifest with checksum-verified, locked, atomic model-cache installation and explicit offline-directory overrides.
[dependencies]
pdf-inspector = { version = "1", features = ["vision", "model-cache"] }

The OCR contracts preserve existing behavior by default: OCR is Off and model resolution is never reached. ModelStore itself does not access the network. The optional model-download feature provides an HTTPS downloader that streams pinned artifacts into the checksum-verified cache only after routing has selected OCR work. Offline consumers set an explicit model directory and ModelDownloadPolicy::Offline. Renderer-only consumers do not enable model-cache or model-download and therefore do not compile their filesystem, hashing, or HTTP dependencies.

use pdf_inspector::vision::{
    ModelDownloadPolicy, ModelStore, OcrMode, OcrOptions, PP_OCR_V6_SMALL,
};

let ocr = OcrOptions::new()
    .mode(OcrMode::Auto)
    .model_directory("/opt/firecrawl/models/pp-ocrv6-small")
    .model_downloads(ModelDownloadPolicy::Offline);
// Verifies exact sizes and SHA-256 digests before an engine opens the files.
let models = ModelStore::from_options(&ocr)?.resolve(&PP_OCR_V6_SMALL)?;
println!("using {} at {}", models.manifest_id(), models.revision());

Optional native page rendering

The render-pdfium feature adds a native-only page renderer backed by firecrawl-pdfium. It is the rendering boundary for OCR pipelines; enabling it does not include an OCR model or change the existing extraction functions. It implies vision, and PdfiumRenderer implements the renderer-neutral PageRenderer trait.

[dependencies]
pdf-inspector = { version = "1", features = ["render-pdfium"] }

PDFium is loaded at runtime and is not bundled into the crate. Set PDFIUM_LIB_PATH to the platform shared library, place that library next to the executable, or use another discovery route supported by firecrawl-pdfium. A load failure reports this prerequisite directly.

use pdf_inspector::vision::{PdfiumRenderer, RenderOptions};

let renderer = PdfiumRenderer::load()?;
let bytes = std::fs::read("document.pdf")?;
let pages = renderer.render_pages(
    &bytes,
    &[1, 3], // 1-indexed, matching pages_needing_ocr
    None,    // optional PDF password
    &RenderOptions::new().dpi(150.0),
)?;

for page in pages {
    // Owned RGB pixels can leave the PDFium critical section and be sent to
    // an OCR worker. OCR pixel boxes can be mapped back to PDF coordinates.
    let rect = page.pixel_rect_to_pdf_rect(20.0, 30.0, 100.0, 24.0);
    println!("page {}: {}x{}, rect={rect:?}", page.page(), page.width(), page.height());
}

Browser WASM remains on the default text-only path and does not expose native PDFium rendering.

Optional OCR engine

The native-only ocr-oar feature adds a CPU PP-OCRv6 Small implementation of OcrEngine backed by OAR and ONNX Runtime. It implies model-cache, but does not enable model auto-download, ONNX Runtime download, or PDF rendering. Model files remain external, must match the pinned manifest, and are opened only after ModelStore verifies their exact size and SHA-256 digest. Install an ONNX Runtime shared library separately and set ORT_DYLIB_PATH to its full path when it is not available through the platform library search path. The runtime is resolved only when an OCR engine is first constructed; clean Auto requests do not require it. The feature currently requires Rust 1.95 or newer, matching OAR 0.9.1's MSRV.

[dependencies]
pdf-inspector = { version = "1", features = ["ocr-oar", "render-pdfium"] }

Direct engine invocation is intentionally separate from extraction routing and native/OCR fusion:

use pdf_inspector::vision::{
    ModelDownloadPolicy, ModelStore, OarOcrEngine, OcrEngine, OcrMode,
    OcrOptions, PdfiumRenderer, RenderOptions, PP_OCR_V6_SMALL,
};

let options = OcrOptions::new()
    .mode(OcrMode::Force)
    .minimum_confidence(0.45)
    .model_directory("/opt/firecrawl/models/pp-ocrv6-small")
    .model_downloads(ModelDownloadPolicy::Offline);
let models = ModelStore::from_options(&options)?.resolve(&PP_OCR_V6_SMALL)?;
let engine = OarOcrEngine::from_models(&models)?;

let renderer = PdfiumRenderer::load()?;
let bytes = std::fs::read("scan.pdf")?;
let pages = renderer.render_pages(&bytes, &[1], None, &RenderOptions::new())?;
let ocr_pages = engine.recognize(&pages, &options)?;

for span in &ocr_pages[0].spans {
    println!("{:.3}: {}", span.confidence, span.text);
}

The engine accepts renderer-neutral RGB, RGBA, and grayscale pages, preserves OAR's positioned quadrilaterals in bitmap coordinates, filters spans using minimum_confidence, and records the pinned model revision in every OcrPage. OcrMode::Off is rejected at the engine boundary so default options cannot run inference accidentally.

Selective routing and lazy model acquisition

route_ocr_pages applies the existing detector/text-quality recommendations to the configured mode. Auto processes only recommended pages, Force processes all pages (or an explicit page selection), and Off always returns an empty route. run_ocr_pages renders only that route, checks that both dependencies preserve its order, and retains each bitmap's PDF transform for fusion.

[dependencies]
pdf-inspector = { version = "1", features = [
  "render-pdfium",
  "ocr-oar",
  "model-download",
] }
use pdf_inspector::vision::{
    route_ocr_pages, run_ocr_pages, HttpModelDownloader, ModelStore,
    OarOcrEngine, OcrMode, OcrOptions, PdfiumRenderer, RenderOptions,
    PP_OCR_V6_SMALL,
};

let bytes = std::fs::read("scan.pdf")?;
let extraction = pdf_inspector::extract_pages_markdown_mem(&bytes, None)?;
let options = OcrOptions::new().mode(OcrMode::Auto);
let routed = route_ocr_pages(
    options.mode,
    extraction.pages.len() as u32,
    &extraction.pages_needing_ocr,
    None,
)?;

if !routed.is_empty() {
    // No HTTP request or model initialization occurs before this point.
    let store = ModelStore::from_options(&options)?;
    let models = store.resolve_or_download(
        &PP_OCR_V6_SMALL,
        options.model_downloads,
        &HttpModelDownloader::default(),
    )?;
    let run = run_ocr_pages(
        &PdfiumRenderer::load()?,
        &OarOcrEngine::from_models(&models)?,
        &bytes,
        &routed,
        None,
        &RenderOptions::new(),
        &options,
    )?;
    println!("OCR processed {} pages", run.pages.len());
}

The downloader accepts HTTPS only, checks a declared content length, caps the response stream to the pinned size plus one byte, and delegates final size and SHA-256 verification to ModelStore. The store serializes installation across processes and publishes completed artifacts atomically. Warm caches make no network calls; offline mode and explicit model directories never download.

OCR Markdown assembly and native fusion

fuse_ocr_pages maps OCR polygons back into PDF coordinates and sends the result through pdf-inspector's existing deterministic reading-order, table, and Markdown pipeline. Pages whose native extraction was rejected use OCR output. When Force runs on a clean native page, normalized duplicate OCR blocks are removed and only additional image-backed text is retained.

use pdf_inspector::vision::{fuse_ocr_pages, OcrFusionOptions};

let fused = fuse_ocr_pages(
    &extraction.pages,
    &run,
    extraction.pages.len() as u32,
    &OcrFusionOptions::new().render_dpi(150.0),
)?;

for page in &fused.pages {
    println!("{}", page.markdown);
    if page.provenance.hosted_recommended {
        eprintln!(
            "page {} needs the hosted document pipeline",
            page.page_number,
        );
    }
}

Each page carries Native, Ocr, or Fused provenance, the exact OCR model revision, accepted-page confidence, local stage timings, and non-fatal warnings. A page that required OCR recommends the hosted pipeline when local OCR is missing, empty, or below the configurable page-confidence threshold. This keeps the lightweight path explicit about cases it cannot finish well.

Complete OCR API

The ocr convenience feature enables the renderer, OCR engine, verified model acquisition, routing, and fusion layers together. It is the intended downstream application integration boundary; lower-level features remain available for consumers that bring their own renderer, model package manager, or engine.

[dependencies]
pdf-inspector = { version = "1", features = ["ocr"] }
use pdf_inspector::vision::{process_pdf_with_ocr, OcrPdfOptions};

let result = process_pdf_with_ocr(
    "document.pdf",
    OcrPdfOptions::auto().page_numbers([1, 2, 3]),
)?;

println!("{}", result.markdown);
println!("OCR pages: {:?}", result.pages_routed_to_ocr);
println!(
    "Hosted fallback pages: {:?}",
    result.pages_recommending_hosted,
);

Native extraction always runs first. In Auto, a clean PDF returns before PDFium loading, model-cache access, HTTP, or OAR initialization. Model files remain external and the default crate feature set remains unchanged. Off provides the same native-only behavior through the OCR result/provenance shape; Force renders every selected page. OCR uses the existing deterministic table, column, reading-order, and Markdown assembly path; no learned layout model is included.

The OCR runtime setup guide lists the pinned PDFium and ONNX Runtime builds, environment variables, model cache behavior, and the error boundary downstream hosted fallbacks should use.

For ambiguous mixed pages, Auto privately retains clean native fragments instead of discarding them when OCR is selected. After recognition it compares script-agnostic text quality, OCR confidence, character overlap, and material new coverage. Exact native text wins over a duplicate or weak OCR hypothesis; complementary image-backed text is fused; and pages where both candidates are weak recommend the hosted document pipeline. A page routed because native coverage appeared incomplete also recommends hosted processing when confident OCR only duplicates the retained fragment: the agreement preserves trustworthy text, but neither hypothesis proves full-page coverage. Public native-only extraction continues to suppress pages marked unreliable, and clean text documents pay no renderer or model-initialization cost.

In Auto, pages routed only for suspicious font encoding or vectorized text first get a bounded positioned-text probe through PDFium. A credible recovered text layer with sufficient geometric page coverage skips rasterization and model loading for that page; garbled, partial, or insubstantial recovery continues through OCR. Recovered tables are reflected in the same document metadata as tables found by the primary extractor.

The one-call API keeps the most recently used verified OCR engine in process. Long-lived workers therefore verify the pinned artifacts and build the ONNX sessions once, then reuse those loaded sessions across documents. The cache is bounded to one model configuration and keyed by normalized model/runtime paths plus the pinned manifest revision and artifact digests; switching the model directory, runtime library, or compiled manifest replaces it. An active engine owns the model data it already verified, so mutating artifacts in place does not hot-reload a running process; restart the process when intentionally replacing files at the same paths. CPU inference uses at most four intra-op threads per ONNX session so a single small page does not oversubscribe larger hosts, and recognizes variable-width line crops individually to avoid padding-heavy CPU batches. The high-level pipeline renders and fuses at most four routed pages at a time, bounding bitmap memory on long documents.

Build the CLI with the same opt-in feature:

cargo install pdf-inspector --features ocr --bin pdf2md
cargo build --release --features ocr --bin pdf2md
pdf2md document.pdf --ocr auto --raw
pdf2md document.pdf --ocr auto --json
pdf2md document.pdf --ocr auto --ocr-offline --ocr-model-dir /opt/models/pp-ocrv6-small

CLI controls include --ocr-dpi, --ocr-min-confidence, --ocr-hosted-threshold, --select-pages, and the existing encrypted-PDF --password option. JSON output has schema_version: 1 and includes per-page Markdown, source/model provenance, confidence, timings, warnings, routed pages, and hosted-fallback recommendations. Page numbers in OcrPdfResult and its per-page provenance are 1-indexed, matching the PDF page numbers accepted by OcrPdfOptions::page_numbers.

Extract per-page Markdown (one string per page, plus document-wide layout metadata):

use pdf_inspector::extract_pages_markdown;

// Pass `None` for every page in document order, or a slice of 0-indexed
// pages to restrict the output (caller-supplied order is preserved).
let result = extract_pages_markdown("document.pdf", None)?;

for page in &result.pages {
    if page.needs_ocr {
        // Route this page to OCR
    } else {
        println!("Page {}: {}", page.page, page.markdown);
    }
}

println!("Complex layout? {}", result.is_complex);

Extract structure-tree elements from tagged PDFs, and join them against extract_text_with_positions to attach semantic roles (heading levels, paragraphs, table cells) to extracted text:

use pdf_inspector::{extract_structure_elements, extract_text_with_positions};
use std::collections::HashMap;

// One entry per marked-content reference, sorted by (page, mcid); empty for
// untagged PDFs. Pages are 1-indexed to match `TextItem::page`, so the
// (page, mcid) pair is a direct join key.
let elements = extract_structure_elements("tagged.pdf", None)?;
let roles: HashMap<(u32, i64), &str> = elements
    .iter()
    .map(|e| ((e.page, e.mcid), e.role.as_str()))
    .collect();

for item in extract_text_with_positions("tagged.pdf")? {
    if let Some(mcid) = item.mcid {
        if let Some(role) = roles.get(&(item.page, mcid)) {
            if role.starts_with('H') {
                println!("{}: {}", role, item.text);
            }
        }
    }
}

Coordinate frame

Positioned items and region inputs share one reference box: PDF points relative to the page's visible page box — CropBox ∩ MediaBox when the page has a CropBox, else the MediaBox (a CropBox that does not overlap the MediaBox is ignored, and a page without a MediaBox is measured against US Letter). TextItem.x/y use the box's lower-left corner as origin with y growing upward; region and crop bboxes (extract_text_in_regions_mem, extract_tables_in_regions_mem, detect_vector_grid_in_region_mem, TsrTableInput) use its top-left corner with y growing downward, the axis convention of a rendered page image. Converting between the two only needs the box height h: a positioned y becomes h - y. For text items y is the baseline and height the font size, so [x, h - y - height, x + width, h - y] covers the glyph band above the baseline (descenders fall below it); for image, link and form-field items y is the rect bottom and that box is exact. Pages whose CropBox equals the MediaBox with a (0, 0) origin are unaffected by this convention. Pages whose text is drawn rotated by 90° are normalized into a synthetic landscape frame before the shift, and /Rotate is not applied.

That is the sheet frame (PositionFrame::Sheet), the default of every position and region API. Its coordinates coincide with a rendered page image only for pages with /Rotate 0 whose text is not predominantly rotated; for any other page, boxes taken from a rendered image belong in the display frame (PositionFrame::Display) that the _in_frame variants offer: the rendered page, i.e. the visible page box turned clockwise by the page's inheritable /Rotate, with the same origin conventions — items from the lower-left corner with y up, regions from the top-left corner with y down — and with the synthetic turn of a predominantly rotated page undone first (individual runs keep their own rotation). Items then sit where a renderer draws them, rotation reads 0 for text that renders horizontally, and region rects can be taken straight from a rendered page image. /Rotate is read inheritably and snapped to a right angle (-90 is 270, 450 is 90).

Processing modes

Mode What it does Returns
ProcessMode::Full (default) Detect + extract + convert to Markdown Everything populated
ProcessMode::Analyze Detect + extract + layout analysis (no Markdown) markdown is None, layout is populated
ProcessMode::DetectOnly Classification only (fastest) markdown is None, layout is default

Functions

Function Description
process_pdf(path) Full processing with defaults
detect_pdf(path) Fast metadata-only detection (no extraction)
process_pdf_with_options(path, options) Process with custom PdfOptions
process_pdf_mem(bytes) Full processing from a byte buffer
detect_pdf_mem(bytes) Fast detection from a byte buffer
process_pdf_mem_with_options(bytes, options) Process from bytes with custom options
extract_text(path) Plain text extraction
extract_text_with_positions(path) Text with its axis-aligned box (visible-page-box frame, see above), rotation, and font info
extract_text_with_positions_mem_in_frame(bytes, pages, frame) Positioned text from bytes, limited to 1-indexed pages, in the sheet or display frame (PositionFrame)
extract_text_with_positions_mem_with_options(bytes, pages, options) The same with every option given as a PositionOptions (frame, bold_from_weight, bold_weight_threshold)
extract_text_with_positions_and_rotations_mem(bytes) Positioned text plus the PageRotation of every page whose text was predominantly rotated
extract_text_with_positions_and_rotations_mem_in_frame(bytes, pages, frame) The same with a page filter and a frame choice
extract_text_with_positions_and_rotations_mem_with_options(bytes, pages, options) The same with a PositionOptions
extract_text_in_regions_mem(bytes, page_regions) Text inside top-left region rects (sheet frame)
extract_text_in_regions_mem_in_frame(bytes, page_regions, frame) The same with region rects read in the sheet or display frame
extract_text_in_regions_mem_with_options(bytes, page_regions, options) The same with a PositionOptions
extract_tables_in_regions_mem(bytes, page_regions) Markdown tables inside region rects (sheet frame)
extract_tables_in_regions_mem_in_frame(bytes, page_regions, frame) The same with region rects read in the sheet or display frame
extract_tables_in_regions_mem_with_options(bytes, page_regions, options) The same with a PositionOptions
collect_text_in_region_in_frame(items, x1, y1, x2, y2, page_height, rotation) Region text with the page's coordinate frame given explicitly (page_height is the visible page box height)
to_markdown(text, options) Convert plain text to Markdown
to_markdown_from_items(items, options) Markdown from pre-extracted TextItems
to_markdown_from_items_with_rects(items, options, rects) Markdown with rectangle-based table detection
extract_pages_markdown(path, pages) Per-page Markdown + layout metadata (file)
extract_pages_markdown_mem(bytes, pages) Per-page Markdown from bytes
extract_structure_elements(path, pages) Structure-tree elements from tagged PDFs (page, mcid, role)
extract_structure_elements_mem(bytes, pages) Structure-tree elements from bytes
widen_degenerate_form_bboxes_mem(bytes) Rust only (not in the Python, Node.js or WebAssembly bindings). The document re-serialized with the /BBox of its Form XObjects repaired — a zero-area box widened, numerals too large for any parser saturated — for rendering elsewhere; None when no form needs it and for an encrypted document, whose protection a plain serialization would drop

Low-level detection functions are also available via the detector module (detect_pdf_type, detect_pdf_type_with_config, etc.) for callers who need PdfTypeResult instead of PdfProcessResult.

Types

Type Description
PdfOptions Builder for processing configuration (mode, detection, markdown, page filter)
ProcessMode DetectOnly, Analyze, Full
PdfType TextBased, Scanned, ImageBased, Mixed
PdfProcessResult Full result: pdf_type, markdown, page_count, confidence, layout, has_encoding_issues, cmap_gaps, timing, and the document information dictionary's entries title, author, subject, keywords, creator, producer, creation_date and mod_date (each decoded as a PDF text string — UTF-16 or UTF-8 after a byte order mark, PDFDocEncoding otherwise — the dates as written; None when missing or not a string)
FontCMapGaps A font whose ToUnicode CMap (or, without one, the embedded program's cmap table) had no entry for some of the codes the document shows through it: font (the /BaseFont name, or the resource name without one), codes shown, interpolated (read from the mapped codes around them: a CMap mapping code 36 to A and code 38 to C says code 37 is B, for a run of digits or of letters of one case in alphabetical glyph order) and unmapped (could not be read; each is a U+FFFD in the text). PdfProcessResult::cmap_gaps lists such fonts; it is always empty in ProcessMode::DetectOnly, which decodes no text, and otherwise empty when every code shown through a CMap had an entry
PdfTypeResult Low-level detection result: type, confidence, page count, pages needing OCR, and the document information entries PdfProcessResult reports
DetectionConfig Configuration for detection: scan strategy, thresholds
ScanStrategy EarlyExit, Full, Sample(n), Pages(vec)
LayoutComplexity Layout analysis: is_complex, pages_with_tables, pages_with_columns
TextItem Text with its axis-aligned box (PDF points from the visible page box's lower-left corner), baseline rotation in degrees (a vertical run is tall and thin, never zero-width), advance_known (false when the font has no width metrics or an ActualText span's advance could not be recovered), baseline_shift (non-zero for super/subscript glyph runs; line_y() gives the body baseline), font info including font_weight (the 100..=900 weight class from the embedded font's OS/2 table, /FontWeight or a weight word in the name; None when unknown), bold_source (where is_bold came from, a BoldSource; None when not bold) and fixed_pitch (Some(true) when the FontDescriptor's FixedPitch flag or the embedded program's post table says so, else measured from the width table: Some(true) when a dozen or more glyphs share one advance, Some(false) when two differ, None when neither holds), the paint the run was shown with — fill_color and stroke_color as 8-bit sRGB [r, g, b] (DeviceRGB as sRGB, DeviceGray as equal components, DeviceCMYK as the specification converts it, ICCBased by its component count, Indexed through its palette; None for other colour spaces) and render_mode, the Tr mode 0..=7 (3 invisible and 7 clip-only runs paint no glyphs), all three None for items not shown by a text operator and none of them changing what is extracted — page number, and optional structure-tree mcid
PageRotation Upright, Ccw, Cw: how a predominantly rotated page's coordinate frame was turned so its text reads left-to-right
PositionFrame Sheet (default): the visible page box as laid out in the content stream, /Rotate not applied, predominantly rotated pages turned; Display: the rendered page, the visible box turned clockwise by the inheritable /Rotate, with only the synthetic turn of a predominantly rotated page undone first while individual runs keep their own rotation (see Coordinate frame)
PositionOptions Options of the _with_options position and region functions: frame (a PositionFrame), bold_from_weight, which also reads TextItem::is_bold from a font_weight at or above bold_weight_threshold (600 by default, valid 100..=900) and merges adjacent runs by that verdict rather than by their weight classes; off by default, where is_bold and merging are unchanged
BoldSource Where TextItem::is_bold came from: FontName, FontFlags, WeightClass (with bold_from_weight) or Painted; the first of them in that order when more than one says bold
StructureElement Tagged-PDF structure reference: page (1-indexed), mcid, role ("H1".."H6", "P", …)
MarkdownOptions Configuration for Markdown formatting (page numbers, etc.)
PageMarkdown Per-page result: page (0-indexed), markdown, needs_ocr
PagesExtractionResult Per-page output + 1-indexed pages_with_tables / pages_with_columns / pages_needing_ocr, is_complex
PdfError Io, Parse, Encrypted, InvalidStructure, NotAPdf