Python API reference¶
The exhaustive reference for the distillpdf package. Every public symbol exported from
distillpdf.__init__.__all__ is documented here, plus the public surface of the
distillpdf.ocr module. For task-oriented walkthroughs see the guide pages
(Rendering, Raw extraction,
The .dpdf model, Searching,
OCR).
The package re-exports the compiled Rust core (Pdf and the module-level functions) and the
Python workflow layer (Document, Doc, the ocr module). Source of truth: the Rust
#[pymethods] in src/lib.rs and the Python modules under python/distillpdf/.
Two entry points, two surfaces
open / from_bytes read a source PDF and return a
Document. load reads a distilled .dpdf and returns a
Doc. They are different objects with different methods — a Document extracts
and renders a live PDF; a Doc queries and re-renders a frozen model.
Module functions¶
open¶
distillpdf.open(path: str) -> Document
Open a PDF from a filesystem path and return a Document. Equivalent to
Document.open(path). Raises ValueError (open failed: …) when the file cannot be read or
parsed.
import distillpdf
doc = distillpdf.open("paper.pdf")
doc.to_html("paper.html")
from_bytes¶
distillpdf.from_bytes(data: bytes) -> Document
Open a PDF from raw bytes and return a Document. There is no source path, so
writing output without an explicit path is an error — pass a path to the render methods.
Raises ValueError (parse failed: …) on a malformed PDF.
load¶
distillpdf.load(path: str) -> Doc
Load a distilled .dpdf document model and return a Doc. The counterpart to open:
produce the .dpdf first with distillpdf.open("x.pdf").distill("x.dpdf"). Raises
DpdfError when path is not a readable .dpdf container (e.g. a PDF or HTML
file passed by mistake).
load_model¶
distillpdf.load_model(path: str) -> str
Load a .dpdf container and return its model.json as a JSON string — the low-level handle
under Doc. The returned JSON is the canonical, sorted-key form, so
distill → load_model → re-save is byte-stable. Raises ValueError when path is not a
readable container. Most callers want load() / Doc instead.
save_dpdf¶
distillpdf.save_dpdf(
src_path: str,
dst_path: str,
model_json: str,
extra_members: dict[str, bytes],
) -> None
Write a new .dpdf to dst_path from a model_json string, carrying over the asset members
of the container at src_path. extra_members are extra named members (e.g.
embeddings/<id>.bin) to add; a same-named member overwrites the carried one. This is the
write path Doc.embed uses internally; you rarely call it directly. Raises
ValueError on a bad source container or unparseable model_json.
read_dpdf_member¶
distillpdf.read_dpdf_member(path: str, member: str) -> bytes | None
Read the raw bytes of a single container member (e.g. an embeddings/<id>.bin vector matrix)
from a .dpdf, or None if the member isn't present. Lets the semantic-search path pull a
space's f32 matrix without re-implementing the zip reader.
render_html¶
distillpdf.render_html(path: str, mode: str = "section", toc: bool = True) -> str
Render a .dpdf model to HTML with no source PDF present — the model-only re-render. mode
("section" / "page") and toc match Pdf.to_html. The born-digital model
drops figure bytes, so figures render as the image_mode="drop" shape; the result is
byte-identical to to_html(..., image_mode="drop") on the source PDF. This is what
Doc.to_html delegates to.
render_markdown¶
distillpdf.render_markdown(
path: str, mode: str = "section", toc: bool = True, image_mode: str = "external",
) -> str
Render a .dpdf model to Markdown with no source PDF present — the HTML→Markdown transform
over the model-only HTML. mode / toc match to_html; image_mode matches
Pdf.to_markdown (the model has no figure bytes, so "external" degrades
to caption placeholders). Backs Doc.to_markdown.
render_text¶
distillpdf.render_text(path: str) -> str
Plain text of a .dpdf model in reading order (one page per line) — the model-only analogue
of Pdf.extract_text. Backs Doc.text.
__version__¶
distillpdf.__version__: str
The installed package version string, set by the compiled core.
Pdf¶
The pure-Rust core class, defined via #[pymethods] in src/lib.rs. You normally hold a
Document rather than a Pdf directly — Document wraps a Pdf and forwards
every method below by delegation (so doc.page_count(), doc.extract_tables(), etc. all
work). The methods are documented here once, on Pdf; Document overrides only
to_html / to_markdown /
to_pdf to add the OCR workflow.
Pdf.open¶
Pdf.open(path: str) -> Pdf # staticmethod
Load and parse a PDF container from a path. Rendering/extraction happens lazily in the output
methods. Raises ValueError on a read/parse failure.
Pdf.from_bytes¶
Pdf.from_bytes(data: bytes) -> Pdf # staticmethod
Load and parse a PDF from raw bytes. With no source path, the output methods require an
explicit path (writing the default <source>.ext has nowhere to go).
Pdf.page_count¶
Pdf.page_count() -> int
The number of pages.
Pdf.extract_text¶
Pdf.extract_text() -> str
Plain text from all pages, concatenated in page order (one trailing newline per page). Hybrid: the ToUnicode-aware content-stream extractor is primary (CID fonts, diacritics, reading order), with a per-page fallback to lopdf's extractor when ours yields little.
Pdf.extract_page_text¶
Pdf.extract_page_text(page: int) -> str
Text of a single 1-indexed page (same hybrid strategy). Raises ValueError (no page N)
when the page does not exist.
Pdf.extract_images¶
Pdf.extract_images() -> list[dict]
Images from all pages as a list of dicts including the raw image bytes.
Pdf.extract_fonts¶
Pdf.extract_fonts() -> list[dict]
Per-page font information as a list of dicts.
Pdf.extract_tables¶
Pdf.extract_tables() -> list[dict]
Detected tables from all pages, each a dict with its cell grid.
Pdf.extract_links¶
Pdf.extract_links() -> list[dict]
Hyperlinks from all pages. Each dict:
| Key | Type | Meaning |
|---|---|---|
page |
int | 1-based page number |
rect |
list[float] | [x0, y0, x1, y1] link rectangle |
kind |
str | "uri" or "internal" |
uri |
str | None | target URL (URI links) |
dest_page |
int | None | target page (internal links) |
dest_name |
str | None | named destination (internal links) |
Pdf.to_html¶
Pdf.to_html(
path: str | None = None,
return_string: bool = False,
mode: str = "section",
toc: bool = True,
image_mode: str = "embed",
)
Render to AI-ready HTML. By default this writes a file and returns 1 — to path if
given (a file, or a directory to place <source-stem>.html in), otherwise <source>.html
beside the opened PDF. With return_string=True it returns the HTML string and writes nothing.
| Parameter | Default | Notes |
|---|---|---|
mode |
"section" |
"section" or "page" structure |
toc |
True |
False drops the <nav> table of contents |
image_mode |
"embed" |
"embed" (inline base64 data: URIs), "external" (figures to a sibling img/ folder), "drop" (<image N> placeholders) |
A returned string has no folder to write into, so "external" falls back to "embed".
Note
Document.to_html overrides this with the same signature plus
ocr / engine / backend for the OCR workflow.
Pdf.to_markdown¶
Pdf.to_markdown(
path: str | None = None,
return_string: bool = False,
mode: str = "section",
toc: bool = True,
image_mode: str = "external",
)
Render to Markdown (a transform over the same HTML to_html emits). File output works like
to_html: by default writes <source>.md (or path) and returns 1; return_string=True
returns the string. image_mode defaults to "external"; a returned string falls back to
caption-only placeholders.
Pdf.to_pdf¶
Pdf.to_pdf(
path: str,
ocr: dict[int, str] | None = None,
remove_raster: bool = False,
) -> int
Write a searchable PDF from OCR results. ocr is a {1-based page: DocTags} map from one
model pass; when omitted, results cached on this object via set_ocr are used.
remove_raster=False(default) — keep the original scan and add the OCR text as an invisible, selectable layer over it. OCR errors never destroy content (best for archival/legal use).remove_raster=True— clean reflow: rebuild OCR'd pages as visible text + cropped figure regions and drop the page raster (a much smaller file).
Non-OCR'd pages are kept verbatim. Returns 1. Most users call
Document.to_pdf, which runs OCR for you.
Pdf.distill¶
Pdf.distill(path: str | None = None, assets: str = "figures") -> str
Distill the document into a .dpdf container — the durable analysis model (a zip of
model.json plus img/ assets) you can re-render in milliseconds. Returns the written path.
path: an explicit *.dpdf file, a directory (→ <source-stem>.dpdf inside it), or None
(→ <source>.dpdf beside the opened PDF).
assets — the asset save profile:
| Value | Meaning |
|---|---|
"figures" (default) |
embed figure image bytes; page rasters dropped-with-stub (regenerable) |
"full" |
figures and (eventually) page rasters; equals "figures" on the born-digital path for now |
"none" |
text + structure only; all asset bytes dropped, regenerable stubs kept (emailable) |
Experimental schema
The model is schema_version = 0 and experimental until the first downstream cutover. A
dropped asset always keeps a stub (hash/dims/regen) — a reversible hole, re-extractable
from the hash-bound source PDF. See the .dpdf format.
Pdf.toc¶
Pdf.toc(mode: str = "section") -> list[tuple[int, str, int, str]]
The detected-heading outline: (level, title, page, anchor_id) per heading, reading order.
level 1 is the title, 2 a section, 3 a subsection, …. anchor_id matches an id= in
to_html (link with #anchor_id). mode="page" carries real page numbers; "section"
yields 0.
Pdf.outline¶
Pdf.outline() -> list[tuple[int, str, int, str]]
The PDF's own /Outlines bookmarks as (level, title, page, anchor) tuples. level is
1-based nesting depth; page is the 1-indexed target page (0 if unresolved); anchor is the
#page-N fragment to_html(mode="page") exposes. Empty when the PDF has no outline. Distinct
from toc, which is built from detected headings.
Pdf.section¶
Pdf.section(name: str, mode: str = "section", image_mode: str = "embed") -> str | None
HTML of a single section: the heading matching name (its sec-… slug, an id prefix, or a
case-insensitive title substring) plus its content up to the next same-or-higher heading.
None if no match. The result is a string, so image_mode="external" behaves like "embed".
Pdf.metadata¶
Pdf.metadata() -> dict
Structured front matter parsed from page 1:
{
"title": str,
"authors": [{"name": str, "affiliation": str | None}],
"affiliations": list,
"abstract": str | None,
"keywords": list[str],
}
Fields are empty/None when not detected.
Pdf.ocr_plan¶
Pdf.ocr_plan() -> list[dict]
Per page, whether OCR is needed and (if so) the page's main raster as image bytes for a
backend. Drives the distillpdf.ocr orchestrators. Each dict: page, needs_ocr (bool),
reason (str), width_pts, height_pts, image (bytes | None).
Pdf.set_ocr¶
Pdf.set_ocr(ocr: dict[int, str]) -> int
Cache OCR results on this object: ocr is a {1-based page: DocTags} map from one model pass.
Once set, to_pdf and the distillpdf.ocr orchestrators reuse it, so the model runs once
regardless of how many outputs are produced. Merges into any existing cache. Returns the cached
page count.
Pdf.get_ocr¶
Pdf.get_ocr() -> dict[int, str]
The cached {page: DocTags} results, empty if set_ocr was never called.
Pdf.has_ocr¶
Pdf.has_ocr() -> bool
True when OCR results have been cached on this object.
Document¶
class distillpdf.Document
A loaded PDF — the Rust Pdf core plus the OCR workflow as methods on the document.
Returned by open / from_bytes. Every core method
(page_count, extract_tables, toc, metadata, …) is delegated to the wrapped Pdf; only
the three output methods below are overridden to fold OCR in. repr shows page count and OCR
state, e.g. <distillpdf.Document pages=12 ocr=no>.
Document.open¶
Document.open(path: str) -> Document # classmethod
Construct from a path (what distillpdf.open calls).
Document.from_bytes¶
Document.from_bytes(data: bytes) -> Document # classmethod
Construct from raw bytes (what distillpdf.from_bytes calls).
Document.run_processing¶
Document.run_processing() -> Document
Analyze the document — detect which pages are scanned / image-only and need OCR. Cheap and
cached; the render methods call it automatically. Chainable (returns self).
Document.run_ocr¶
Document.run_ocr(
backend: OcrBackend | None = None,
*,
engine: str | None = None,
only: set | None = None,
progress: Callable[[int, int, int], None] | None = None,
**config,
) -> Document
OCR every scanned page once, caching the result on this document. Afterwards the render methods fold the recovered text into their output. Chainable.
| Parameter | Default | Notes |
|---|---|---|
backend |
None |
a constructed OcrBackend (power users); usually leave unset |
engine |
None |
None/"fast" → bundled Tesseract; "accurate"/"granite" → granite-docling VLM; or a specific backend name |
only |
None |
restrict to a set of 1-based page numbers |
progress |
None |
progress(page, done, total) callback; None shows a tqdm bar on a TTY, False silences |
**config |
— | any OcrConfig field (model_dir, hf_token, device, languages, prompt, …) |
The default fast engine needs no extra, no download, and runs offline. The accurate engine
needs a runtime you install yourself — print the per-OS commands with
distillpdf.ocr.install_help("granite") (see OCR runtime setup).
doc.run_ocr() # fast (Tesseract)
doc.run_ocr(engine="granite", languages=["por"])
doc.run_ocr(engine="granite", model_dir=r"D:\models").to_html("out.html")
Document.to_html¶
Document.to_html(
path: str | None = None,
return_string: bool = False,
mode: str = "section",
toc: bool = True,
image_mode: str = "embed",
ocr: bool = False,
engine: str | None = None,
backend: OcrBackend | None = None,
)
Render to HTML. path / return_string / mode / toc / image_mode behave exactly as
Pdf.to_html. Additions:
ocr=Truerunsrun_ocrfirst if it hasn't been (choose the engine withengine=, default fast/Tesseract).- When the document has OCR results, scanned pages are rendered from the recovered text (page
mode); otherwise this is the core extractor verbatim and un-OCR'd scanned pages emit a
warnings.warnpointing atrun_ocr(). - A large
image_mode="embed"output (> 50 MB) emits a warning suggesting"external"or"drop".
Document.to_markdown¶
Document.to_markdown(
path: str | None = None,
return_string: bool = False,
mode: str = "section",
toc: bool = True,
image_mode: str = "external",
ocr: bool = False,
engine: str | None = None,
backend: OcrBackend | None = None,
)
Render to Markdown. As Pdf.to_markdown, plus ocr / engine / backend
as on to_html. OCR-augmented output only when ocr=True or run_ocr was
already called.
Document.to_pdf¶
Document.to_pdf(
path: str,
remove_raster: bool = False,
ocr: bool = False,
engine: str | None = None,
backend: OcrBackend | None = None,
) -> int
Write a searchable PDF. ocr=True OCRs scanned pages first (pick the engine with engine=,
default fast/Tesseract). remove_raster matches Pdf.to_pdf: default keeps the
scan and adds an invisible text layer; True reflows to clean text and drops the raster. Warns
if no pages have been OCR'd (the result would otherwise just be the original).
Doc¶
class distillpdf.Doc
A loaded .dpdf document model — the single-document reading surface. Returned by
load (or Doc.load). A thin wrapper over the internal Model; reading logic lives
in one place, so the Python API and the distillpdf <file>.dpdf … CLI cannot drift. Doc is
immutable (re-distill the source PDF to upgrade), except for embed, which
writes a new embedding space back into the file. repr shows the source, page/section/block
counts, and schema version.
Query view vs fidelity view
section / block_markdown are the query view —
block slices as simple markdown, addressable per section / block / page (what the CLI
read verb prints). to_markdown / to_html /
text are the fidelity view — the whole-document model-only re-render,
byte-identical to render_*. See The .dpdf model.
Doc.load¶
Doc.load(path: str) -> Doc # classmethod
Load a .dpdf from disk. Raises DpdfError with an actionable message when
path is not a readable .dpdf container.
Doc.from_model¶
Doc.from_model(model: Model, path: str | None = None) -> Doc # classmethod
Wrap an already-parsed Model (tests, in-memory pipelines). Pass path to keep the
fidelity renders (to_html / to_markdown /
text) and embed / search working; without it they
raise DpdfError.
Doc.model¶
Doc.model -> Model # property
The underlying Model. An escape hatch for ad-hoc traversal; the accessors below
cover the documented surface.
Doc.schema_version¶
Doc.schema_version -> int # property
The model schema version. 0 is experimental.
Doc.source¶
Doc.source -> dict # property
The source binding: {file, sha256, pages, distillpdf, generated_at} — which PDF this model
was distilled from, by which distillpdf version, when.
Doc.metadata¶
Doc.metadata -> dict # property
Parsed front matter: {title, authors, abstract_text, keywords, …} (fields the extractor
found; empty when none).
Doc.pages¶
Doc.pages -> list[dict] # property
Per-page records: n (1-based), width_pts / height_pts, labels (the /PageLabels map,
e.g. {"pdf": "xii"}), and OCR fields when present.
Doc.sections¶
Doc.sections -> list[dict] # property
The flat section list in document order: each {id, level, title, parent, page_start,
page_end}. The id threads into section / find.
Doc.toc¶
Doc.toc() -> list[tuple[int, str, int, str]]
The section tree as (level, title, page_start, id) tuples — the .dpdf analogue of
Pdf.toc, but the trailing element is the section id (not an HTML anchor),
because that is what reads back through section.
Doc.page_label¶
Doc.page_label(n: int, scheme: str = "pdf") -> str | None
The label physical page n carries under scheme (default the /PageLabels "pdf" label,
e.g. "xii"), or None when the page has no such label.
Doc.resolve_pages¶
Doc.resolve_pages(spec: str) -> list[int]
Resolve a page spec ("5", "3-7", a label like "xii", or a label range "xii-xv") to
physical page numbers. Raises DpdfError on an unresolvable token — a page that
isn't here is a loud error, never a silent empty.
Doc.section¶
Doc.section(sid: str) -> str
The section (and its whole subtree) as query-markdown — byte-identical to what
distillpdf <file>.dpdf read <sid> prints. Headings by level, paragraphs, list items, tables
as pipe-form, figure/caption placeholders. Raises DpdfError (with the closest
section ids) on an unknown id.
Doc.blocks¶
Doc.blocks(
kind: str | None = None,
section: str | None = None,
pages: list[int] | None = None,
) -> list[dict]
The blocks (reading-order units — the source of truth) matching every filter given. No filter →
every block. Each block is the raw model dict (id, kind, text, page, section,
confidence, ocr_pass, …).
| Parameter | Notes |
|---|---|
kind |
"para" / "heading" / "table" / "figure" / … |
section |
a section id; includes its subtree. Raises DpdfError on an unknown id |
pages |
physical page numbers |
Doc.block¶
Doc.block(bid: str) -> dict
One block by id. Raises DpdfError on an unknown id.
Doc.block_markdown¶
Doc.block_markdown(bid: str) -> str
A single block as query-markdown (what read <block-id> prints).
Doc.tables¶
Doc.tables() -> list[dict]
The table index: one {id, label, page, page_label, caption} per table, document order. The
id reads back via block / block_markdown.
Doc.figures¶
Doc.figures() -> list[dict]
The figure index: one {id, label, page, page_label, caption} per figure.
Doc.find¶
Doc.find(
query: str,
*,
regex: bool = False,
section: str | None = None,
kind: str | None = None,
pages: list[int] | None = None,
limit: int | None = None,
) -> FindResult
Lexical search over block text — case-insensitive substring by default, regex=True for a
regex. Scope with section / kind / pages. Returns a FindResult carrying
the hits and the coverage accounting, so "not found" is a measured statement. limit
truncates the returned hits list (the coverage counts still reflect the full search); pass
limit=None (default) for every hit. Raises DpdfError on an unknown section
id or an invalid regex. See Searching documents.
Doc.embed¶
Doc.embed(
space_id: str = "e1",
*,
cache_dir: str | None = None,
batch_size: int = 32,
progress: bool = True,
) -> dict
Derive chunks (if needed), embed every chunk's text with BAAI/bge-m3, write the vectors into
this .dpdf as embedding space space_id, then save the file. The document must have been
loaded from disk (it needs a path to write back). Returns
{space, model, chunks, dimension, backend, dropped_stale}.
- Atomic: vectors are computed in full before anything is written, so a mid-batch failure leaves the file untouched.
- Re-embedding an existing
space_idoverwrites it. A stale space (chunks changed since a prior embed) is dropped as the chunks are re-derived. - Raises
distillpdf.embed.EmbedDependencyError(with the install line) when the ONNX runtime is missing; raisesDpdfErrorwhen the document was built in memory (no path) or has no text blocks to embed.
The ~2.3 GB weights download on first use; see
distillpdf.embed.install_help.
Doc.search¶
Doc.search(query: str, *, k: int = 8, space: str | None = None) -> SearchResult
Semantic search: embed query with the same model and rank this document's chunks by cosine
similarity, returning the top k as a SearchResult. space selects an
embedding space by id (default: the first). Raises DpdfError (pointing at
embed) when no embedding space exists or the named space is unknown, and
distillpdf.embed.EmbedDependencyError when the runtime is missing.
Doc.info¶
Doc.info() -> dict
A compact roll-up of the model — the info CLI verb as data: schema_version, source
(file, sha256_prefix, distillpdf, generated_at), counts (pages, sections, blocks,
tables, figures, footnotes), ocr state, the assets storage profile, index coverage,
and an embeddings summary (per-space model + chunk count, and whether the chunking is
stale).
Doc.ocr_passes¶
Doc.ocr_passes() -> list[dict]
The recorded OCR passes (append-only history): each {id, engine, params, results, …}. Empty
for a born-digital model.
Doc.ocr_status¶
Doc.ocr_status() -> dict
Per-page OCR decisions and per-pass outcomes — the ocr-status CLI verb as data:
{
"pages": [{"page", "decision", "active_pass"}],
"passes": [{"pass", "engine", "page", "outcome", "legible_chars", "confidence"}],
}
Both lists empty for a born-digital model.
Doc.to_html¶
Doc.to_html(
path: str | None = None,
return_string: bool = False,
*,
mode: str = "section",
toc: bool = True,
) -> str
Re-render the fidelity HTML from the model — byte-identical to
render_html. return_string=True returns the HTML and writes nothing;
otherwise it writes a file (path, or <stem>.html beside the .dpdf) and returns the path.
Figures render as drop-placeholders (the born-digital model carries no figure bytes). Raises
DpdfError if the Doc was built in memory with no path.
Doc.to_markdown¶
Doc.to_markdown(
path: str | None = None,
return_string: bool = False,
*,
mode: str = "section",
toc: bool = True,
image_mode: str = "external",
) -> str
Re-render the fidelity Markdown from the model — byte-identical to
render_markdown. return_string=True returns the string; otherwise writes
<stem>.md (or path) and returns the path. The model has no figure bytes, so "external"
degrades to caption placeholders. Raises DpdfError if built in memory.
Doc.text¶
Doc.text() -> str
The document's plain text in reading order (one page per line) — byte-identical to
render_text. Raises DpdfError if built in memory.
SearchResult¶
class distillpdf.doc.SearchResult
The result of Doc.search — the ranked chunk hits plus honest-coverage
accounting. Attributes (__slots__):
| Attribute | Type | Meaning |
|---|---|---|
query |
str | the search query |
hits |
list[dict] | ranked hits (see below) |
space |
str | the embedding space id searched |
model |
str | the embedding model (BAAI/bge-m3) |
searched |
int | number of chunks searched |
stale |
bool | True when the chunking drifted from the current blocks since embedding — scores reflect an out-of-date chunking; refresh with embed |
Each hit is a dict:
{
"chunk_id": str, "score": float, # cosine, rounded to 6 dp
"section": str | None,
"page_start": int, "page_end": int,
"page_label_start": str | None, "page_label_end": str | None,
"block_ids": list[str], # thread into read / Doc.block
"snippet": str, # recomposed chunk text
}
FindResult¶
class distillpdf.FindResult
The result of Doc.find — the matches plus the coverage accounting, so find
never silently truncates and "not found" is paired with how much was searched. Attributes
(__slots__):
| Attribute | Type | Meaning |
|---|---|---|
hits |
list[Hit] | the matches (see Hit) |
searched_blocks |
int | how many blocks were searched |
searched_pages |
int | how many distinct pages were spanned |
no_text_pages |
list[int] | pages that were in scope but carried no searchable text |
Hit¶
class distillpdf.dpdf.Hit
One find match (in FindResult.hits). Attributes (__slots__):
| Attribute | Type | Meaning |
|---|---|---|
block |
dict | the block the match occurred in |
page |
int | None | the block's page |
start |
int | match start offset in the block's searchable text |
end |
int | match end offset |
text |
str | the block's searchable text |
block_id |
str | property → block["id"] |
Model and DpdfError¶
distillpdf.dpdf is the lower-level reader under Doc. FindResult (above) lives
here too. Most code should use Doc; reach for Model only for direct model traversal.
Model¶
class distillpdf.dpdf.Model
A parsed .dpdf model. Construct with Model.load(path) (calls the Rust loader; raises
DpdfError on a bad container) or Model.from_dict(data) (an already-parsed
dict; raises DpdfError when required keys — source, pages, sections,
blocks, indexes — are missing).
Public properties: raw, schema_version, source, metadata, pages, sections,
blocks, indexes, assets, ocr_passes, chunks, embedding_spaces.
Public methods (selected — these back the Doc accessors):
| Method | Returns | Notes |
|---|---|---|
section_by_id(sid) |
dict | None | |
block_by_id(bid) |
dict | None | |
section_ids() / block_ids() |
list[str] | |
blocks_for_section(sid) |
list[dict] | the subtree's blocks, reading order |
blocks_for_pages(pages) |
list[dict] | |
page_label(n, scheme="pdf") |
str | None | |
resolve_page_range(spec) |
list[int] | raises DpdfError on an unresolvable token |
find(query, *, regex=False, section=None, kind=None, pages=None) |
FindResult | |
derive_chunks() |
dict | a fresh {policy, items} chunk derivation |
chunks_stale() |
bool | stored chunks differ from a fresh derive |
chunk_text(chunk) |
str | recompose a chunk's text from its blocks |
space_by_id(space_id) |
dict | None | |
section_markdown(sid) / blocks_markdown(blocks) |
str | the query-markdown emitter |
closest_section_ids(sid, n=3) |
list[str] | for actionable errors |
Module-level helpers: derive_chunks(blocks), and the constants CHUNK_TARGET_TOKENS (400)
and CHUNK_POLICY ("sec-contig-v1:tgt400").
DpdfError¶
class distillpdf.DpdfError(ValueError)
Raised when a .dpdf could not be read as a model (not a container, wrong shape) or a lookup
fails (unknown section/block id, unresolvable page spec, semantic search with no embedding
space). Subclass of ValueError.
The ocr module¶
distillpdf.ocr is always importable; the heavy per-engine dependencies load lazily inside a
backend, so a base install gives a precise, actionable error the moment OCR is used. The
package re-exports OcrBackend, OcrConfig,
OcrDependencyError, available_backends, and
get_backend at the top level; install_help lives on the
module. See OCR for scanned PDFs and
OCR runtime setup.
OcrConfig¶
@dataclass
class distillpdf.ocr.OcrConfig
Configuration common to every backend. Fields an engine doesn't use are ignored, so this stays one shared dataclass.
| Field | Type | Default | Meaning |
|---|---|---|---|
model_id |
str | None | None |
backend-specific model id (e.g. a HF repo id) |
model_dir |
str | None | None |
download directory; None → engine default (the granite GGUF engine uses a visible ./ocr_model) |
hf_token |
str | None | None |
HF token for gated/private models; default public models need none |
env_file |
str | None | None |
.env file to read HF_TOKEN from; None → auto-detect ./.env (only HF_TOKEN is read) |
store_token |
bool | False |
if True with an hf_token, persist it to the .env (perms 0600, plaintext) |
device |
str | "auto" |
"auto" / "cpu" / "metal" / "cuda" |
prompt |
str | "Convert this page to docling." |
model instruction |
max_tokens |
int | 4096 |
generation cap per page (a backstop; stop_strings normally terminate) |
stop_strings |
list[str] | [] |
strings that end generation |
languages |
list[str] | [] |
ISO codes (e.g. ["eng", "por"], Tesseract); empty = engine default |
dpi |
int | None | None |
render-resolution hint; None = engine default |
OcrBackend¶
class distillpdf.ocr.OcrBackend
Abstract OCR backend: a page image → DocTags string. The Rust core renders the DocTags. Build your own by subclassing and registering it. Class-level capability attributes are deliberately cheap to read (inspecting them never triggers the engine's heavy imports):
| Attribute | Type | Default | Meaning |
|---|---|---|---|
name |
str | "abstract" |
registry name, shown in errors |
output |
str | "doctags" |
the wire contract dialect |
tier |
str | "accurate" |
"fast" or "accurate" |
structure_aware |
bool | True |
emits tables (OTSL) / tagged headings vs flat text (advisory only) |
bundled |
bool | False |
works on a base install — no extra, no download |
offline |
bool | False |
no network / model download at use time |
languages |
tuple | () |
ISO codes it can read; () = any/unspecified |
detail |
str | "" |
one-line human description |
Constructor and methods:
OcrBackend(config: OcrConfig | None = None, **kwargs) # **kwargs build an OcrConfig
backend.ocr_page(image: bytes) -> str # DocTags for one page image (override this)
backend.prepare(samples: list[bytes]) -> None # one-time setup (default no-op)
backend.close() -> None # release held resources
backend.is_available() -> bool # classmethod; can it run here?
backend.descriptor() -> OcrCapabilities # classmethod; capabilities without heavy imports
OcrBackend is a context manager (__enter__ / __exit__ calls close).
OcrCapabilities¶
@dataclass(frozen=True)
class distillpdf.ocr.OcrCapabilities
What an engine is and can do, read straight off the backend class (no heavy imports, no model
download). Fields: name, tier, structure_aware, bundled, offline, languages,
available, output ("doctags"), detail. Returned by OcrBackend.descriptor() and
distillpdf.ocr.backend_descriptors().
OcrDependencyError¶
class distillpdf.ocr.OcrDependencyError(ImportError)
Raised when an OCR backend's optional dependencies are not installed. The message carries the
exact per-OS install commands (see install_help).
available_backends¶
distillpdf.ocr.available_backends() -> list[str]
The sorted names of every registered backend.
get_backend¶
distillpdf.ocr.get_backend(
name: str | None = None, *, tier: str | None = None, **kwargs,
) -> OcrBackend
Construct a backend. Precedence: explicit name > tier > the platform default (the fast
tier). **kwargs build an OcrConfig. Raises
OcrDependencyError when the chosen engine's optional deps are missing
(with the exact install command), or ValueError on an unknown backend name.
backend_for¶
distillpdf.ocr.backend_for(engine=None, **kwargs) -> OcrBackend
Resolve an engine into a backend — the entry point behind to_html(ocr=True, engine=...) and
run_ocr(engine=...). engine may be None/"fast" (bundled Tesseract, the default),
"accurate" or "granite" (the granite-docling VLM), a specific registered backend name, or
an already-constructed OcrBackend (returned as-is).
install_help¶
distillpdf.ocr.install_help(engine: str | None = None) -> str
What to install to use an OCR engine — the user-facing helper. Accepts a selector:
None/"fast" (bundled, nothing to install), "accurate"/"granite", or a backend name.
Returns the pip commands for your platform (accurate tiers) or a "nothing to install" line (the
bundled fast Tesseract engine).
print(distillpdf.ocr.install_help("granite"))
Orchestration functions¶
The distillpdf.ocr module also exposes the orchestrators that Document uses
(call these directly only if you hold a raw Pdf):
distillpdf.ocr.run(pdf, backend=None, *, only=None, progress=None) -> dict[int, str]
distillpdf.ocr.to_html(pdf, backend=None, *, path=None, return_string=True, image_mode="embed") -> str
distillpdf.ocr.to_markdown(pdf, backend=None, *, path=None, return_string=True, toc=True, image_mode="drop") -> str
distillpdf.ocr.to_pdf(pdf, backend=None, path=None, *, remove_raster=False) -> str
run OCRs every scanned page once and caches the {page: DocTags} map on the pdf; the
others reuse that cache (running backend once if nothing is cached). For the accurate tier the
bundled Tesseract engine acts as a cheap gate — the heavy granite model only runs on pages
Tesseract confirms hold text.
Fast tier is flat text only
The bundled Tesseract engine emits flat text (no table structure or tagged headings). For structure-aware output (OTSL tables) use the accurate granite engine.
Embedding dependency helpers¶
Semantic search (Doc.embed / Doc.search) uses
distillpdf.embed:
distillpdf.embed.install_help() -> str # pip line for the ONNX embedding runtime
distillpdf.embed.runtime_available() -> bool # is the runtime importable?
class distillpdf.embed.EmbedDependencyError(ImportError)
distillpdf.embed.MODEL_ID = "BAAI/bge-m3"
distillpdf.embed.DIMENSION = 1024
EmbedDependencyError is raised (with the exact pip install line) when the optional runtime
(onnxruntime / tokenizers / huggingface_hub) is missing.