Skip to main content

PDF editing and rendering for Python, powered by Rust (lopdf + hayro). MIT, zero dependencies, lightweight wheels.

Project description

pylopdf

PyPI CI Python License: MIT

日本語版 README はこちら

PDF editing and rendering for Python, powered by Rust — lopdf for editing and hayro (the pure-Rust PDF renderer adopted by typst) for rendering.

MIT licensed, zero runtime dependencies, lightweight wheels. Covers the common pymupdf use cases without the AGPL.

Why pylopdf?

pylopdf pymupdf pypdf pypdfium2 pdf_oxide pikepdf
License MIT AGPL / commercial BSD Apache/BSD MIT/Apache-2.0 MPL-2.0
Wheel size ~3.5 MB ~40 MB+ small (pure Python) ~8 MB ~10–11 MB ~2–5 MB
Editing (merge / split / rotate / outlines) limited ✅ (structure-focused)
Rendering (PNG / SVG) ✅ (PNG) ❌ (docs point to other tools)
Text extraction ✅ (basic) ✅ (advanced) ✅ (advanced, table detection / Markdown) ❌ (docs point to other tools)
Encryption (AES-256) ✅ read & write undocumented ✅ (via qpdf)
CJK font fallback ✅ ([cjk] extra) manual
Implementation pure Rust C Python C++ (PDFium) Rust C++ (qpdf)
  • Fits size-constrained environments such as AWS Lambda
  • Safe for commercial projects that need to avoid the AGPL
  • abi3: one wheel covers Python 3.10–3.14
  • API modeled after pymupdf

Limitations: no precise layout analysis, and no appearance-stream regeneration for forms and annotations (form filling uses NeedAppearances — viewers draw the values). Use pymupdf if you need those. Typesetting, PDF/A output, and digital signatures are covered by the ecosystem recipes below.

Install

pip install pylopdf

To render Japanese PDFs without embedded fonts, install the optional CJK fonts (Noto Sans/Serif JP, auto-detected at render time):

pip install pylopdf[cjk]

Building from source (requires a Rust toolchain):

uv sync

Usage

import pylopdf

# Open from a path or bytes
doc = pylopdf.open("input.pdf")
doc = pylopdf.open(stream=pdf_bytes)

# Page count
print(doc.page_count)  # same as len(doc)

# Metadata
print(doc.metadata["title"])
doc.set_metadata({"title": "Monthly Report", "author": "Alice"})

# Text extraction (0-based page numbers)
text = doc.get_page_text(0)

# Positioned text and search (pymupdf-style, top-left origin)
words = doc[0].get_text("words")     # (x0, y0, x1, y1, word, block, line, word_no)
layout = doc[0].get_text("dict")     # blocks -> lines -> spans with bboxes
rects = doc[0].search_for("tax")     # case-insensitive, list[Rect]
images = doc[0].get_images()         # [{"width", "height", "bbox", "ext", "image"}]
pix = doc[0].get_pixmap(dpi=144)     # RGBA8 pixels for NumPy / PIL (pix.samples)

# Rendering
png: bytes = doc.render_page(0)             # 72 dpi
png2x: bytes = doc.render_page(0, scale=2)  # 144 dpi
png300 = doc.render_page(0, dpi=300)        # by resolution
png_bg = doc.render_page(0, background=(255, 255, 255))  # white background (default: transparent)
svg: str = doc.render_page_svg(0)

# Delete pages (split)
doc.delete_page(0)
doc.delete_pages([1, 2])

# Keep/reorder pages (repeating a page duplicates it)
doc.select([2, 0])

# Page objects (0-based; negative counts from the end)
page = doc[0]
for page in doc:
    print(page.number, page.rect)
page.set_rotation(90)                # display rotation (multiples of 90)
page.set_mediabox((0, 0, 300, 400))  # page boxes

# Insert / copy pages
doc.new_page()          # blank A4 appended
doc.copy_page(0, to=1)  # duplicate page 0 in front of page 1

# Drawing (coordinates are the same top-left display space as search_for / get_text)
page.insert_image((72, 72, 200, 200), filename="logo.png")   # JPEG passthrough, PNG with alpha
page.insert_image(page.search_for("Approved")[0], stream=stamp_png)  # stamp at a search hit
page.show_pdf_page(page.rect, letterhead)  # overlay another PDF page as vectors (watermark / letterhead)
page.replace_text("DRAFT", "FINAL")        # text replacement (simple-encoded fonts only)

# Headers / footers / page numbers (standard-14 fonts, WinAnsi range; CJK via the typst recipe)
for i, p in enumerate(doc):
    p.insert_text((p.rect.width - 90, p.rect.height - 30), f"Page {i + 1}", fontsize=9)

# Annotations: search & highlight / link
page.add_highlight_annot(page.search_for("important"))  # appearance stream included (visible everywhere)
page.add_link_annot(page.search_for("Example")[0], "https://example.com/")
print(page.annots())  # [{"type", "rect", "contents", "uri"}]

# Make scanned PDFs searchable (write external OCR results as an invisible text layer)
page.insert_ocr_text_layer(ocr_words)  # sequence of (x0, y0, x1, y1, text, ...); near-zero size cost, CJK included

# Markdown conversion (RAG / LLM preprocessing; size-based headings, CJK-aware line joining)
md = doc.to_markdown()
md_p1 = doc[0].to_markdown()

# Read the PDF/A self-declaration (validation belongs to veraPDF)
print(doc.get_pdfa_claim())  # e.g. (2, "B") for PDF/A-2b; None if absent

# Forms (AcroForm): read and fill
print(doc.get_form_fields())        # [{"name", "type", "value"}]
doc.set_form_field("customer", "Taro Yamada")
doc.set_form_field("agree", True)   # checkboxes take bool or a state name

# Page labels (display numbers: roman front matter + decimal body, etc.)
doc.set_page_labels([{"startpage": 0, "style": "r"}, {"startpage": 3, "style": "D"}])
print(doc[4].get_label())  # "2"

# File attachments (e.g. attach the XML data to an invoice PDF)
doc.embfile_add("invoice.xml", xml_bytes, filename="invoice-data.xml")
print(doc.embfile_names())  # ["invoice.xml"]
xml = doc.embfile_get("invoice.xml")

# Table of contents (page numbers are 1-based here, pymupdf-compatible)
doc.set_toc([[1, "Chapter 1", 1], [2, "Section 1.1", 2]])
print(doc.get_toc())

# Merge (with ranges, reversed order, and an insertion position)
merged = pylopdf.Document()
merged.insert_pdf(pylopdf.open("a.pdf"))
merged.insert_pdf(pylopdf.open("b.pdf"), from_page=0, to_page=2, start_at=0)

# Save
merged.save("merged.pdf")
data: bytes = merged.tobytes()

# Optimized save (prune unreferenced objects + compress + object streams)
merged.save("small.pdf", garbage=True, deflate=True, object_streams=True)

# Encrypted save (AES-256; owner_pw alone = open freely, restricted permissions)
merged.save("locked.pdf", user_pw="secret", permissions=pylopdf.Permissions.PRINT)

# Fast metadata probe without parsing the whole file
info = pylopdf.peek_metadata("input.pdf")
print(info["title"], info["page_count"], info["encrypted"])

# Context manager
with pylopdf.open("input.pdf") as doc:
    print(doc.metadata)

# Encrypted PDFs (RC4-40/128, AES-128, AES-256; empty user passwords open transparently)
doc = pylopdf.open("locked.pdf", password="secret")
doc = pylopdf.open("locked.pdf")
if doc.needs_pass:
    doc.authenticate("secret")  # 0=failed, 2=user, 4=owner, 6=both

# CJK fallback font for PDFs without embedded fonts
# (automatic with pylopdf[cjk]; or bring your own font)
doc.set_fallback_font("NotoSansJP-Regular.otf")
doc.set_fallback_font(font_bytes, kind="serif")

Ecosystem recipes (typesetting, PDF/A, signatures)

pylopdf stays a lightweight core for editing, extraction, and rendering; adjacent concerns are solved by pairing it with established libraries. The recipes below are covered by integration tests (tests/test_interop.py).

Typesetting / creating new documents = typst (via typst-py). Typeset reports with typst and feed the bytes straight into pylopdf:

import typst
import pylopdf

pdf_bytes = typst.compile("report.typ")   # typesetting: typst
doc = pylopdf.open(stream=pdf_bytes)      # editing / extraction / merging: pylopdf

PDF/A for new documents is also typst's job (validated export via krilla; PDF/A-1b through 4 and PDF/UA-1):

pdf_a: bytes = typst.compile("report.typ", pdf_standards="a-2b")

CJK watermarks / headers / footers combine typst with pylopdf: typeset a one-page stamp with typst (fonts get subset-embedded), then burn it onto every page as vectors with show_pdf_page:

from pylopdf_fonts_cjk import sans_path  # pip install pylopdf[cjk] (reuses the Noto fonts)

stamp_typ = """
#set page(width: 595pt, height: 842pt, fill: none)
#set text(font: "Noto Sans JP", size: 48pt, fill: rgb(255, 0, 0, 40%))
#align(center + horizon)[社外秘]
"""
stamp = pylopdf.open(stream=typst.compile(stamp_typ.encode(), font_paths=[str(sans_path().parent)]))
for page in doc:
    page.show_pdf_page((0, 0, page.rect.width, page.rect.height), stamp)

Converting or validating existing PDFs against PDF/A is a different problem; veraPDF (Java) is the de-facto validator.

Digital signatures (PAdES) = pyHanko (MIT). pyHanko signs with an incremental update, so the bytes produced by pylopdf remain untouched as a prefix of the signed file:

import io
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign import signers

signer = signers.SimpleSigner.load("key.pem", "cert.pem")
out = signers.sign_pdf(
    IncrementalPdfFileWriter(io.BytesIO(doc.tobytes())),
    signers.PdfSignatureMetadata(field_name="Signature1"),
    signer=signer,
)
signed_pdf: bytes = out.getvalue()

API

pylopdf.Document (pylopdf.open() is an alias constructor):

Method / property Description
Document(filename=None, stream=None, password=None, max_decompressed_size=None) Open from a path or bytes; empty document if both are None. max_decompressed_size guards against decompression bombs
doc[i] / load_page(pno) / for page in doc Get a Page view (negative indices count from the end; re-fetch after structural changes)
needs_pass / is_encrypted Encryption status (pymupdf-compatible semantics)
authenticate(password) Decrypt with a password (returns 0/1/2/4/6, pymupdf-compatible)
page_count / len(doc) Number of pages
metadata Metadata dict (title, author, subject, keywords, creator, producer, creationDate, modDate, format)
set_metadata(dict) Set metadata (empty string deletes the entry)
get_page_text(pno, option="text") Extract text (or positioned layout: "words" / "blocks" / "dict")
render_page(pno, scale=1.0, dpi=None, background=None) Render a page to PNG bytes; dpi replaces scale, background is an RGB(A) fill (max 65,535 px per side / 64 MP total)
render_page_svg(pno) Render a page to an SVG string
set_fallback_font(font, kind="sans", index=0) Set a fallback font (path/bytes) for non-embedded CJK fonts; None disables auto-detection
select(page_numbers) Keep only the given pages, in the given order (repeats duplicate the page)
delete_page(pno) / delete_pages(iterable) Delete pages
insert_pdf(other, from_page=0, to_page=-1, start_at=-1) Merge a page range (negative / reversed ranges; start_at sets the insertion position)
new_page(pno=-1, width=595, height=842) / copy_page(pno, to=-1) Insert a blank page / duplicate a page
get_toc() / set_toc(toc) Read/write outlines as [[level, title, page], ...] (page numbers are 1-based here)
to_markdown(pages=None) Markdown conversion (size-inferred headings, CJK-aware joining, bullet normalization; no bold/tables/multi-column)
get_form_fields() / set_form_field(name, value) List and fill AcroForm fields (NeedAppearances approach; checkboxes take bool)
get_pdfa_claim() Read the XMP PDF/A declaration (part, conformance) (a self-claim read, not validation)
embfile_add(name, data, filename=, desc=) / embfile_names() / embfile_get(name) / embfile_del(name) Add / list / read / delete file attachments (EmbeddedFiles)
get_page_labels() / set_page_labels(labels) Read/write page label ranges ({"startpage", "style", "prefix", "firstpagenum"})
save(filename, garbage=, deflate=, object_streams=, user_pw=, owner_pw=, permissions=) / tobytes(same) Save; prune / compress / object streams, or AES-256 encryption via user_pw / owner_pw (the in-memory document stays plain)
close() Close (supports with)

pylopdf.Page (obtained via doc[i]):

Method / property Description
number / parent 0-based page number and owning Document
get_label() Display label of the page ("iv", "A-2", …; empty string if undefined)
get_text(option="text") Text extraction; "words" / "blocks" / "dict" return positioned layout
to_markdown() Markdown conversion of this page
search_for(needle) Case-insensitive text search returning list[Rect]
get_images() Extract page images (original JPEG bytes passed through; others as PNG)
get_pixmap(scale, dpi=, background=) Render to a Pixmap (straight RGBA8: samples / width / height / stride / tobytes())
insert_image(rect, filename=/stream=, keep_proportion=True, overlay=True) Draw an image (JPEG without recompression, PNG with alpha; rect in display coordinates)
show_pdf_page(rect, src, pno=0, keep_proportion=True, overlay=True) Overlay a page from another document as vectors (watermarks / stamps / letterheads)
insert_text(point, text, fontsize=11, fontname="helv", color=(0,0,0)) Print text with a standard-14 font (WinAnsi range; \n for multiple lines; upright on rotated pages)
insert_ocr_text_layer(words) Write OCR results as an invisible text layer (searchable PDFs; no font embedding, near-zero size)
annots() Read annotations ({"type", "rect", "contents", "uri"} dicts; rect in display coordinates)
add_highlight_annot(rects, color=(1,1,0), opacity=0.4, content=None) Highlight annotation; feed search_for results directly; appearance stream included
add_link_annot(rect, uri) URI link annotation (no border)
replace_text(search, replacement, default_char=None) Replace text (simple-encoded fonts only; returns the count; no CJK)
render(scale, dpi=, background=) / render_svg() Rendering
rotation / set_rotation(deg) Display rotation (multiples of 90, inheritance-resolved)
mediabox / cropbox / rect Page boxes (Rect); rect is the rotation-aware visible rectangle
set_mediabox(rect) / set_cropbox(rect) Set page boxes

Module level:

Name Description
peek_metadata(filename/stream, password=None) Fast metadata / page-count / encryption probe without parsing the whole file
Permissions Encryption permission flags (IntFlag)
Rect Rectangle NamedTuple with width / height
Exceptions PdfError (ValueError-compatible base), PasswordError, DocumentClosedError, EncryptedDocumentError, StalePageError

For low-level access, use pylopdf.pylopdf_core._Document (a thin lopdf wrapper) directly.

Architecture

Follows the division of labor in the 2026 Rust PDF ecosystem:

pylopdf.Document (Python, pymupdf-style API)
   └─ _Document (PyO3)
        ├─ lopdf 0.44   … editing: open → modify → save
        └─ hayro 0.7    … rendering: PNG / SVG (standard fonts embedded)
rust/          # PyO3 bindings
src/pylopdf/   # Python high-level API
tests/         # pytest (Rust behavior is verified through Python tests)
uv sync                    # build + install dependencies
uv run pytest              # tests
uv run ruff check .        # lint
uv run mypy src tests      # type check
uv build --wheel           # build a wheel

uv sync detects Rust source changes and rebuilds automatically (via tool.uv.cache-keys).

License

MIT (lopdf is MIT; hayro is MIT/Apache-2.0)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pylopdf-0.9.0.tar.gz (87.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pylopdf-0.9.0-cp310-abi3-win_amd64.whl (4.2 MB view details)

Uploaded CPython 3.10+Windows x86-64

pylopdf-0.9.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.8 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ x86-64

pylopdf-0.9.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.6 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

pylopdf-0.9.0-cp310-abi3-macosx_11_0_arm64.whl (4.1 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

pylopdf-0.9.0-cp310-abi3-macosx_10_12_x86_64.whl (4.5 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file pylopdf-0.9.0.tar.gz.

File metadata

  • Download URL: pylopdf-0.9.0.tar.gz
  • Upload date:
  • Size: 87.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pylopdf-0.9.0.tar.gz
Algorithm Hash digest
SHA256 6d4c5b15a17495eac111c904737920c112b4465320d7e507f3f059aa6e8817fe
MD5 7fcc8698d7903b5b51d9d2cd0ee00e9d
BLAKE2b-256 093c0f917191e78bfccf969d14b806e35b6f7566f8cf72f85438b94a7077330a

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.9.0.tar.gz:

Publisher: release.yml on yhay81/pylopdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylopdf-0.9.0-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: pylopdf-0.9.0-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 4.2 MB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pylopdf-0.9.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 60c04b1529cdbd0707dd9357b2c335f6422313cd3828e9345b459f80e7919c90
MD5 171a3730e8b7311ecb77610e81667cec
BLAKE2b-256 ba84aaafc9e2463bc9dfdc05187bf275685655c88517d361fe8a8695708ea3fa

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.9.0-cp310-abi3-win_amd64.whl:

Publisher: release.yml on yhay81/pylopdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylopdf-0.9.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for pylopdf-0.9.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1f352560c6e70798706a68b609464acd1d716f570d71637161e4e0b89e8612e7
MD5 7cb9121d6425c66412696347a475d3a4
BLAKE2b-256 3cfbd5fac71edcfdf91f21ccfc6471bfd3685b8544e8a1a1747a94d17d6c180b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.9.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on yhay81/pylopdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylopdf-0.9.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for pylopdf-0.9.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 e87d9fb87a53dced2d1cd23d9345c61e32e56e92eac62a1c82fdb04d3ddabbe6
MD5 5c63a70098756ba79ffb427515231cb3
BLAKE2b-256 026e7c733feebf213af59af81194e362c06b646a37e7205f6b161fd5e8c0f5b6

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.9.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on yhay81/pylopdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylopdf-0.9.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pylopdf-0.9.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 027eabb2846b73e659fd300538381c2d4dee8803e732281e2983c6bc315860e1
MD5 9540c428cee4639b383a6552188d13cc
BLAKE2b-256 49e38f83310fbbcccd119406bd3d129825f40511870a793185f5320696ad541e

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.9.0-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on yhay81/pylopdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylopdf-0.9.0-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for pylopdf-0.9.0-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 6b99b6f5cfbca1e2391ae9564c97c65438767d2ddbbb674939c5d2205e7c5772
MD5 5f8265b04b9d613322f44be34edac0fc
BLAKE2b-256 026fba86a5a1d615de29933696f8a67922184723f5e2444218126e1428c94364

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.9.0-cp310-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on yhay81/pylopdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page