Skip to main content

unpdf

Python bindings for unpdf - High-performance PDF content extraction to Markdown, text, and JSON.

Installation

pip install unpdf-markdown

The distribution is unpdf-markdown; the import name is unpdf.

Quick Start

import unpdf

# Convert PDF to Markdown
markdown = unpdf.to_markdown("document.pdf")
print(markdown)

# Convert PDF to plain text
text = unpdf.to_text("document.pdf")
print(text)

# Convert PDF to JSON
json_data = unpdf.to_json("document.pdf", pretty=True)
print(json_data)

# Get document information
info = unpdf.get_info("document.pdf")
print(info)

# Get page count
pages = unpdf.get_page_count("document.pdf")
print(f"Total pages: {pages}")

# Check if file is a valid PDF
is_valid = unpdf.is_pdf("document.pdf")
print(f"Is valid PDF: {is_valid}")

Input: path or bytes

Every function takes its PDF as PdfSource — a path (str, or any os.PathLike such as pathlib.Path) or the PDF's own bytes. Types are unambiguous: str is always a path, bytes is always content, and bytes go through the native in-memory parser rather than a temporary file.

from pathlib import Path
import unpdf

unpdf.to_markdown("document.pdf")
unpdf.to_markdown(Path("document.pdf"))
unpdf.to_markdown(pdf_bytes)

API Reference

to_markdown(source: PdfSource, flags: int = 0) -> str

Convert a PDF file to Markdown format. flags is a bitwise OR of:

Constant Effect
UNPDF_FLAG_FRONTMATTER Emit YAML frontmatter with document metadata
UNPDF_FLAG_ESCAPE_SPECIAL Escape Markdown special characters
UNPDF_FLAG_PAGE_MARKERS Mark each page boundary with <!-- page N -->
import unpdf

markdown = unpdf.to_markdown(
    "document.pdf",
    unpdf.UNPDF_FLAG_FRONTMATTER | unpdf.UNPDF_FLAG_PAGE_MARKERS,
)

to_text(source: PdfSource) -> str

Convert a PDF file to plain text.

to_json(source: PdfSource, pretty: bool = False) -> str

Convert a PDF file to JSON format.

get_info(source: PdfSource) -> dict

Get document metadata. Keys: section_count (the page count), resource_count, plus title / author only when the document sets them.

get_page_count(source: PdfSource) -> int

Get the number of pages in a PDF file.

is_pdf(source: PdfSource) -> bool

Check if a file is a valid PDF.

version() -> str

Get the version of the native library.

get_extraction_quality(source: PdfSource) -> dict

Document-level extraction diagnostics: char_count, word_count, replacement_char_count, encrypted, is_scan_pdf, suppressed_ocr_pages, suppressed_text_runs, pages_incomplete, declared_page_count, unresolved_page_nodes, skipped_object_count. See "Incomplete extraction" below.

get_page_stats(source: PdfSource, page_number: int) -> dict

Per-page content-stream operator counts (1-indexed): page, text_op_count, image_op_count, ocr_text_suppressed.

Detecting scanned (image-only) PDFs

Empty output can mean a scanned document, a genuinely blank page, or a parse failure. The introspection surface tells them apart:

import unpdf

if unpdf.get_extraction_quality("scan.pdf")["is_scan_pdf"]:
    print("Scanned document - OCR required")

stats = unpdf.get_page_stats("scan.pdf", 1)
if stats["text_op_count"] == 0 and stats["image_op_count"] > 0:
    print("Page 1 is image-only (scanned)")
elif stats["text_op_count"] == 0:
    print("Page 1 is genuinely blank")

Note: a searchable scan (page image plus an invisible OCR text layer) reports text_op_count > 0 — combine the check with ocr_text_suppressed, which flags pages whose unreadable OCR layer was dropped.

Incomplete extraction

A damaged PDF does not always fail. When the cross-reference table survives but the objects it points at do not, the parser returns the pages it could read — a success over an incomplete page set. Check before indexing or archiving, because a page that silently never arrived is indistinguishable from a page that never existed:

quality = unpdf.get_extraction_quality("document.pdf")
if quality["pages_incomplete"]:
    print(f"incomplete - document declares {quality['declared_page_count']} page(s)")
Field Meaning
pages_incomplete Pages are known to be missing. The one field to branch on.
declared_page_count Page count the document declares, or None if unreadable.
unresolved_page_nodes Unreadable page-tree nodes — non-zero means incomplete, not a page count.
skipped_object_count Objects that could not be loaded. Most cost no page.

Also note that get_info()["resource_count"] counts the extracted-resource inventory, which this binding's parse path leaves empty (resource extraction is off by default to bound peak memory) — it is not a count of images on the page. Use get_page_stats for scan detection.

Handling failures

UnpdfError carries a kind so you can branch on the reason a call failed instead of matching on message text:

from unpdf import to_text, ErrorKind, UnpdfError

try:
    text = to_text("document.pdf")
except UnpdfError as e:
    if e.kind == ErrorKind.ENCRYPTED:
        print("Password required")
    elif e.kind in (ErrorKind.CORRUPTED, ErrorKind.PDF_PARSE):
        print("The file is damaged")
    else:
        print(f"Extraction failed ({e.kind.name}): {e}")

UnpdfError subclasses RuntimeError, so existing except RuntimeError handlers keep working. ErrorKind values are part of the native ABI: new reasons take new numbers and existing ones are never renumbered, so treat an unrecognised value as a generic failure.

License

MIT License

Metadata

Release files for unpdf-markdown 0.14.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for unpdf-markdown 0.14.0
File Size Uploaded
unpdf_markdown-0.14.0.tar.gz 9.5 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for unpdf-markdown 0.14.0
File Interpreter ABI Platform
unpdf_markdown-0.14.0-py3-none-any.whl Python 3 none any Details

Total release size: 19.0 MB

Release files / unpdf_markdown-0.14.0.tar.gz

Download URL unpdf_markdown-0.14.0.tar.gz
Size 9.5 MB
Tags Source
SHA-256 checksum
How to use checksums
9f8b6d795c249103175d62c412627a2171867c020aa37c5214ebca3e6044f7af
BLAKE2b-256 checksum
How to use checksums
7ccf3d6774597a207dd12f1f92d12c4d07bbefa00550e557c15cde1158b34924
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / unpdf_markdown-0.14.0-py3-none-any.whl

Download URL unpdf_markdown-0.14.0-py3-none-any.whl
Size 9.5 MB
Tags Python 3
SHA-256 checksum
How to use checksums
a1190ecba9ebe612089b275b3d407b791b6ab9a6e3dcf238c5da96e4042169a7
BLAKE2b-256 checksum
How to use checksums
7dcc5fc99a7cd4154c7166cb36e6f4160485de0b7351a6a2c63ce10f7d277874
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

0.22.0

2 release files

0.21.0

2 release files

0.20.0

2 release files

0.19.0

2 release files

0.17.0

2 release files

0.15.0

2 release files

This release

0.14.0 This release

2 release files

0.13.0

2 release files

0.12.2

1 release file

0.12.1

1 release file

0.12.0

1 release file

0.11.0

1 release file

0.10.0

1 release file

0.9.0

1 release file

0.8.0

1 release file

0.7.1

1 release file

0.7.0

1 release file

0.6.4

1 release file

0.6.3

1 release file

0.6.2

1 release file

0.6.1

1 release file

0.6.0

1 release file

0.5.0

1 release file

0.4.6

1 release file

0.4.5

1 release file

0.4.4

1 release file

0.4.3

1 release file

0.4.2

1 release file

0.4.1

1 release file

0.4.0

1 release file

0.2.3

1 release file

0.2.1

1 release file

0.1.10

1 release file

0.1.9

1 release file

0.1.8

1 release file

0.1.6

1 release file

0.1.5

1 release file

0.1.1

1 release file

0.1.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page