pypdfium2
pypdfium2 is a ctypes binding to PDFium, the C++ PDF engine that Chrome uses to display PDFs. The wheels ship a prebuilt pdfium shared library, so pip install gives you a working PDF renderer with no compiler and no system packages. There are two layers. The helper layer is normal Python: PdfDocument, PdfPage, PdfTextPage, PdfBitmap, with methods for rendering pages to images, pulling out text, reading the table of contents, adding and removing pages, and saving. Underneath, pypdfium2.raw exposes every PDFium C function directly through ctypes for the parts the helpers do not cover yet. The reason people reach for it over the alternatives is licensing: pypdfium2 is Apache-2.0 or BSD-3-Clause and PDFium is BSD-style, so you get Chrome-grade rendering speed without the AGPL obligations that come with PyMuPDF.
The best answer when you need Chrome-quality PDF rendering under a permissive licence, and the wheel coverage means it usually just installs. Pair it with pikepdf or pypdf, because PDFium gives you pixels and characters but no way to edit the document structure underneath.
Use it if
- You need to rasterize PDF pages fast, for thumbnails, OCR preprocessing, or feeding page images to a vision model, and PDFium's renderer is the same one that draws PDFs in Chrome
- You are shipping commercial or closed-source software and cannot take on PyMuPDF's AGPL terms, which is the single most common reason projects land here
- You want a wide platform matrix without building anything: prebuilt wheels cover manylinux and musllinux on x86_64, i686, aarch64, armv7l, ppc64le, s390x and riscv64, plus Windows x86, x64 and arm64, macOS 13 and newer, and Android
- You need PDFium APIs the helpers do not wrap yet, since pypdfium2.raw exposes the full C surface and helper objects auto-convert to their raw handles when passed to it
- You need to touch the PDF object model: PDFium's public interface exposes no way to read or write dictionaries, streams, or name and number trees, so anything structural (rewriting metadata trees, fixing broken xref, surgical object edits) needs pikepdf or pypdf instead
- You use threads: PDFium is not thread-safe, and the project documents this as a known limitation. Rendering a batch in a ThreadPoolExecutor is asking for a crash, so parallelism means separate processes
- You want layout-aware extraction: get_text_bounded returns characters in the document's internal order with no word grouping, column detection, or table reconstruction. pdfplumber and its pdfminer.six base exist for that
- You are on an unlisted platform or need macOS 12 or older: 5.12.0 raised the macOS floor to 13.0, and when no wheel matches, pip falls back to a source setup that wants a C preprocessor, git, a ctypesgen fork, and possibly a full pdfium build with gn, ninja, and a dozen system libraries
- You expect responsive support: the maintainers state plainly that issues may go unanswered or be closed without much feedback, that PRs are merged at the owner's discretion, and that AI-generated issues and PRs are banned outright and get the reporter blocked
- You are nervous about ctypes: object lifetime is your responsibility, and the README acknowledges that unknown lifetime violations may still exist in the helpers, which surface as non-deterministic memory corruption rather than a Python exception
Setup reality
python -m pip install -U pypdfium2 normally grabs a py3-none wheel with the pdfium binary bundled inside, which takes seconds and needs no toolchain. That is the happy path and it covers most machines. If your platform is not in the wheel matrix, pip falls back to the sdist and pypdfium2's own setup.py runs: it wants a C preprocessor, git, the pypdfium2-team fork of ctypesgen, and it will either bind against a system pdfium or try to build pdfium from source with gn and ninja, which is a heavy checkout. Note also that the setup code is explicitly excluded from the project's API stability promises and is driven by environment variables such as PDFIUM_PLATFORM. There are no mandatory runtime dependencies, but bitmap.to_pil() needs Pillow and bitmap.to_numpy() needs NumPy, and neither is installed for you. On the usage side, the two things that bite first are that PDFium is not thread-safe, and that render() takes a scale factor rather than a DPI, so 200 DPI is scale=200/72.
Patterns
Open a PDF and read basic factsopen-document
import pypdfium2 as pdfium
pdf = pdfium.PdfDocument('report.pdf')
print(len(pdf), 'pages')
print('PDF version', pdf.get_version())
print(pdf.get_metadata_dict(skip_empty=True))
page = pdf[0]
width, height = page.get_size()
pdf.close()The constructor takes a path, a pathlib.Path, bytes, or a byte stream, plus password= for encrypted files. PdfDocument is also a context manager, which is the safer form because closing releases the underlying C handle deterministically.
Render one page to a PNG at a chosen DPIrender-page-to-image
import pypdfium2 as pdfium
DPI = 200
with pdfium.PdfDocument('report.pdf') as pdf:
page = pdf[0]
bitmap = page.render(scale=DPI / 72)
image = bitmap.to_pil()
image.save('page1.png')scale is pixels per PDF canvas unit, not DPI, and one canvas unit is 1/72 inch, so divide your DPI by 72. to_pil() needs Pillow installed; pypdfium2 declares no runtime dependencies, so pip will not have pulled it in.
Rasterize a whole documentrender-all-pages
import pypdfium2 as pdfium
with pdfium.PdfDocument('report.pdf') as pdf:
for i, page in enumerate(pdf):
bitmap = page.render(scale=2, draw_annots=False, fill_color=(255, 255, 255, 255))
bitmap.to_pil().save(f'page-{i:03d}.png')Iterating the document yields PdfPage objects in order. Do not spread this loop over a ThreadPoolExecutor: PDFium is not thread-safe, so use multiprocessing with one document opened per worker process.
Pull the text out of a pageextract-text
import pypdfium2 as pdfium
with pdfium.PdfDocument('report.pdf') as pdf:
page = pdf[0]
textpage = page.get_textpage()
all_text = textpage.get_text_bounded()
first_50 = textpage.get_text_range(index=0, count=50)
print(textpage.count_chars(), 'characters')get_text_bounded() with no arguments covers the whole page; pass left, bottom, right, top in canvas units to restrict it to a region. Order follows the content stream, so a two-column layout comes back interleaved unless you extract each column by its own bounding box.
Find every occurrence of a phrase on a pagesearch-text
import pypdfium2 as pdfium
with pdfium.PdfDocument('report.pdf') as pdf:
textpage = pdf[0].get_textpage()
searcher = textpage.search('net revenue', match_case=False, match_whole_word=False)
while (match := searcher.get_next()) is not None:
index, count = match
print(index, textpage.get_text_range(index, count))get_next() returns a (char_index, char_count) tuple or None when there is nothing left, and the searcher keeps position between calls so you loop rather than re-search. get_prev() walks backwards from wherever you are.
Walk the table of contentsread-toc
import pypdfium2 as pdfium
with pdfium.PdfDocument('book.pdf') as pdf:
for bookmark in pdf.get_toc(max_depth=15):
dest = bookmark.get_dest()
page_index = dest.get_index() if dest else None
print(' ' * bookmark.level, bookmark.get_title(), '->', page_index)get_toc() is a generator that flattens the tree, so use bookmark.level for indentation. get_dest() returns None for bookmarks that point at an action rather than a page, and get_index() can be None when the destination does not resolve.
Build a new document from scratchcreate-pdf
import pypdfium2 as pdfium
pdf = pdfium.PdfDocument.new()
A4_WIDTH, A4_HEIGHT = 595, 842
page = pdf.new_page(A4_WIDTH, A4_HEIGHT)
pdf.save('blank.pdf', version=17)
pdf.close()Page dimensions are in canvas units, so A4 is 595 by 842, not millimetres. version=17 writes a PDF 1.7 header. There is no text drawing helper, so for generating text-heavy documents use ReportLab and keep pypdfium2 for rendering.
Place a JPEG on a pageinsert-image
import pypdfium2 as pdfium
pdf = pdfium.PdfDocument.new()
image = pdfium.PdfImage.new(pdf)
image.load_jpeg('photo.jpg')
width, height = image.get_px_size()
image.set_matrix(pdfium.PdfMatrix().scale(width, height))
page = pdf.new_page(width, height)
page.insert_obj(image)
page.gen_content()
pdf.save('photo.pdf')A page object with no matrix renders at one canvas unit square, so the scale() call is what makes the image visible at all. gen_content() must be called after adding objects or the changes never reach the saved file.
Combine several PDFs into onemerge-documents
import pypdfium2 as pdfium
sources = [pdfium.PdfDocument(p) for p in ('a.pdf', 'b.pdf', 'c.pdf')]
merged = pdfium.PdfDocument.new()
for src in sources:
merged.import_pages(src)
merged.save('merged.pdf')
merged.close()
for src in sources:
src.close()The source documents have to stay alive and open until after save(), because imported pages still reference their originals. Building the list up front rather than opening inside the loop is what keeps them from being garbage collected mid-merge. Pass pages=[0, 2, 4] to import a subset.
Drop pages and write the resultdelete-and-reorder-pages
import pypdfium2 as pdfium
with pdfium.PdfDocument('scan.pdf') as pdf:
for index in sorted([1, 4, 7], reverse=True):
del pdf[index]
pdf.save('trimmed.pdf')Deleting shifts every later index down, which is why the loop runs in reverse. There is no move-page API, so reordering means creating a new document and calling import_pages with the page order you want.
Get a zero-copy array for OpenCV or OCRbitmap-to-numpy
import pypdfium2 as pdfium
import pypdfium2.raw as pdfium_c
with pdfium.PdfDocument('scan.pdf') as pdf:
bitmap = pdf[0].render(
scale=300 / 72,
grayscale=True,
force_bitmap_format=pdfium_c.FPDFBitmap_BGRA,
rev_byteorder=True,
)
array = bitmap.to_numpy()
print(array.shape, array.dtype)to_numpy() is a view over PDFium's buffer, not a copy, so the array is only valid while the bitmap object is alive. Forcing BGRA plus rev_byteorder avoids a format conversion and keeps rendering off PDFium's slower transparency path.
Call a PDFium function the helpers do not wrapraw-c-api
import ctypes
import pypdfium2 as pdfium
import pypdfium2.raw as pdfium_c
with pdfium.PdfDocument('locked.pdf') as pdf:
flags = pdfium_c.FPDF_GetDocPermission(pdf) # helper auto-casts to pdf.raw
c_version = ctypes.c_int()
ok = pdfium_c.FPDF_GetFileVersion(pdf, c_version)
version = c_version.value if ok else NoneRaw calls take positional arguments only, with no defaults and no keywords. Output parameters are ctypes objects you allocate and then read via .value, and any Python buffer you pass must stay referenced for as long as PDFium holds it or you get a crash instead of an error.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pymupdf | PyPI | You want the richest feature set and the fastest text extraction in one package and the AGPL or a commercial licence is acceptable for your project |
| pikepdf | PyPI | You need real access to PDF objects: editing dictionaries and streams, repairing damaged files, linearizing, or encrypting, none of which PDFium exposes |
| pypdf | PyPI | You only need to split, merge, rotate, or read metadata and want a pure-Python dependency with no bundled binary |
| pdfplumber | PyPI | The job is pulling structured text, words with coordinates, or tables out of a page rather than drawing it |