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, no form (AcroForm) editing, no advanced annotation editing. 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"}]

# 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)
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_text(option="text") Text extraction; "words" / "blocks" / "dict" return positioned layout
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)
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.8.0.tar.gz (71.8 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.8.0-cp310-abi3-win_amd64.whl (4.2 MB view details)

Uploaded CPython 3.10+Windows x86-64

pylopdf-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.7 MB view details)

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

pylopdf-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.5 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

pylopdf-0.8.0-cp310-abi3-macosx_11_0_arm64.whl (4.0 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

pylopdf-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl (4.4 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

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

File metadata

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

File hashes

Hashes for pylopdf-0.8.0.tar.gz
Algorithm Hash digest
SHA256 1a04327ffd332affdf8b674154e8dcbdc1e1a22c8d6fac293e49e510c301bdf7
MD5 b8999c836a7270db9474a5ef23ec363b
BLAKE2b-256 85fbeb14afac1beba4ffa99a992b166d7c6f5730657ce82b53b09f48dc472873

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.8.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.8.0-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: pylopdf-0.8.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.8.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 8def05fd7130df455cc703633727a23133ac1ffe2ae29a6b6d5e7bfe9a00b83b
MD5 438fed30e1d28e903cf4bd01b6bc16f8
BLAKE2b-256 caa0bd175f6e438a6bf8f0d38921157f10c2bf1c773a0a7a2ffaa4944c95cefc

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.8.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.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for pylopdf-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 935c3802200577b1107698baf36a0b474f80be7bbac4cbbf20767dd0ecd316ae
MD5 d38814951781e317b644ecf708ff3a7f
BLAKE2b-256 a1a8005b735160a6adffc882de787292680e9a4b4521dd05c14dbd217ce9af16

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.8.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.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for pylopdf-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 6a12df1de900fb8efde325c68dc7c690f3896a1379e6988807ff909e722a9526
MD5 99e2eba287112e8c22fe2f1f22dca227
BLAKE2b-256 427905fd95219d403326973f90388c291e020fbcea9208364b03f80d8e3f6f16

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.8.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.8.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pylopdf-0.8.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 37f81c945c5dd5a4d345ae535918f2f6d485192625bc9146f605ae4a2fe82d66
MD5 f40081475f270842c24e30a0e10ab2e9
BLAKE2b-256 7071a6f9355ecd506c38c754aea90f192f7801a2308dd12f94d2760991ce9f34

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.8.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.8.0-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for pylopdf-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 adfcc31d4fc4b813aca729ea04045c502903044b012596e68ede8220812b2204
MD5 b311ee6ac3bc7a3b5f6cb01a06913b6f
BLAKE2b-256 6779f0ed89175a5c1eb3f7d0c949d50614557cb6549426c65376e39bf1d37cdb

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylopdf-0.8.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