pdfminer.six
pdfminer.six reads the text out of PDF files by interpreting the drawing instructions inside them, not by looking at pictures of pages. It walks the content stream, decodes the fonts and character maps, and reports every glyph with its exact position on the page, then runs a layout pass that groups those glyphs into words, lines, and text boxes. You can take the one-line answer (extract_text gives you a string) or take the layout tree and decide yourself what counts as a paragraph, a column, or a table cell, using coordinates and font names. It is a community-maintained fork of the original PDFMiner, written entirely in Python, and it is the engine underneath pdfplumber and a lot of document-processing pipelines.
The dependable pure-Python way to get text with exact coordinates out of digital PDFs, and the foundation the nicer libraries are built on. It is slow, it does nothing for scanned pages, and it will never hand you a table, so choose it when you want control over layout data rather than a finished answer.
Use it if
- You need text plus geometry: every character comes with a bounding box, a font name, and a size, which is what you build invoice and form parsers on
- You want to write your own grouping logic rather than accept someone's paragraph heuristics, because extract_pages hands you the raw LTPage tree
- Your PDFs are encrypted with a password (RC4 or AES) or contain CJK and vertical writing, both of which are handled without extra packages
- You need something that installs into a slim container or a lambda without system PDF libraries, and a pdf2txt.py command line that works immediately
- Your PDFs are scans: pdfminer only reads text drawing operators, so a page of images returns an empty string with no warning and no error. You need OCR first (ocrmypdf or pytesseract), and nothing in the library will tell you that is the problem
- You need tables as rows and columns: you get characters and boxes with coordinates and nothing table-shaped, which is exactly why pdfplumber and camelot exist on top of it
- Throughput matters: it is pure Python doing per-glyph work, so batch extraction over thousands of documents is far slower than pypdfium2 or PyMuPDF, which wrap a C or C++ engine
- You want to write PDFs: this library only reads. Merging, splitting, rotating, watermarking, or filling forms all need pypdf or something else
- You need HTML, XML, or hOCR output you can trust: those output types exist, but the library's own docstring for extract_text_to_fp says only 'text' works properly
- You need bugs fixed on a schedule: the README states plainly that maintainer availability is limited and that submitting a pull request yourself is the best way to get an issue resolved, and 213 issues (223 counting PRs) are open
Setup reality
pip install pdfminer.six on Python 3.10 or newer. The library itself is pure Python, but it pulls cryptography for RC4 and AES decryption and charset-normalizer for encodings, and cryptography is a compiled wheel, so an unusual platform without a prebuilt wheel drags in a Rust toolchain. Image extraction is an extra: pip install 'pdfminer.six[image]' adds Pillow. Two naming traps catch people. The distribution is pdfminer.six with a dot but the import is plain pdfminer, and the abandoned original pdfminer package is still sitting on PyPI, so installing the wrong name gives you dead code from years ago. The console scripts keep their suffix too, so the commands are literally pdf2txt.py and dumppdf.py. Versions are dates like 20260107 rather than semver, which means pinning is exact-match and a release number tells you nothing about whether it breaks you; read the changelog instead. The real configuration cost is LAParams, whose defaults (char_margin 2.0, line_margin 0.5, word_margin 0.1, boxes_flow 0.5) decide where paragraphs and columns land, and getting sensible output on your own documents almost always means tuning them.
Patterns
Get the whole document as a stringextract-all-text
from pdfminer.high_level import extract_text
text = extract_text("example.pdf")
print(text)Accepts a path or an already-open binary file object. An empty or nearly empty result almost always means the pages are scanned images, not that the file is broken.
Read only the pages you needextract-specific-pages
from pdfminer.high_level import extract_text
first_two = extract_text("report.pdf", page_numbers=[0, 1])
cover = extract_text("report.pdf", maxpages=1)page_numbers is zero-indexed, so page 1 of the document is 0. Passing a set instead of a list is faster on long documents because membership is checked once per page.
Fix wrong paragraph and column groupingtune-layout-analysis
from pdfminer.high_level import extract_text
from pdfminer.layout import LAParams
params = LAParams(line_margin=0.3, char_margin=1.5, boxes_flow=0.5)
text = extract_text("two-column.pdf", laparams=params)
# read strictly in position order, no grouping heuristics:
raw = extract_text("odd.pdf", laparams=LAParams(boxes_flow=None))All the margins are relative to character or line size, not absolute points. boxes_flow runs from -1.0 (horizontal position only) to +1.0 (vertical only); setting it to None turns off advanced ordering entirely, which is often what you want for a fixed template.
Walk the layout tree instead of taking a stringiterate-layout-objects
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer
for page_layout in extract_pages("example.pdf"):
for element in page_layout:
if isinstance(element, LTTextContainer):
x0, y0, x1, y1 = element.bbox
print(round(y0), element.get_text().strip())extract_pages is a generator, so pages are parsed lazily and memory stays flat. Coordinates start at the bottom-left of the page in PDF points (72 per inch), so y grows upward, the opposite of screen coordinates.
Read font name and size per charactercharacter-font-info
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer, LTTextLine, LTChar
for page in extract_pages("example.pdf"):
for element in page:
if not isinstance(element, LTTextContainer):
continue
for line in element:
if not isinstance(line, LTTextLine):
continue
for char in line:
if isinstance(char, LTChar):
print(char.get_text(), char.fontname, round(char.size, 1))A text line also contains LTAnno objects for spaces and newlines that the layout pass inserted; those have no font or position, which is why the isinstance check on LTChar matters. Font names arrive with the PDF subset prefix, as in ABCDEF+Helvetica.
Pull a value from a fixed spot on the pagefind-text-by-position
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer
BOX = (350, 680, 560, 720) # x0, y0, x1, y1 in points
def inside(bbox, box):
return bbox[0] >= box[0] and bbox[1] >= box[1] and bbox[2] <= box[2] and bbox[3] <= box[3]
for page in extract_pages("invoice.pdf", page_numbers=[0]):
for el in page:
if isinstance(el, LTTextContainer) and inside(el.bbox, BOX):
print(el.get_text().strip())Check page.mediabox before hardcoding numbers, since A4 and Letter differ and a rotated page swaps width and height. This is how template-based invoice parsing gets built on pdfminer.
Open a password-protected PDFencrypted-pdf
from pdfminer.high_level import extract_text
text = extract_text("protected.pdf", password="hunter2")This handles user passwords on RC4 and AES encrypted files. An owner-password-only file (no password needed to open, but copying is disallowed) still extracts, because pdfminer reads the content rather than honoring the permission flag.
Save embedded images to diskextract-images
# pip install 'pdfminer.six[image]'
from pdfminer.high_level import extract_pages
from pdfminer.image import ImageWriter
from pdfminer.layout import LTImage, LTFigure
writer = ImageWriter("out_images")
for page in extract_pages("scan.pdf"):
for element in page:
if isinstance(element, LTFigure):
for obj in element:
if isinstance(obj, LTImage):
print(writer.export_image(obj))Images usually sit inside an LTFigure rather than at the top level of the page, which is why the nested loop is needed. export_image returns the filename it chose and picks the extension from the image encoding, so you may get .jpg, .png, or a raw .bmp.
Read document metadata and the table of contentsmetadata-and-outlines
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument, PDFNoOutlines
with open("book.pdf", "rb") as fp:
doc = PDFDocument(PDFParser(fp))
print(doc.info) # list of dicts: Title, Author, CreationDate, ...
try:
for level, title, dest, a, se in doc.get_outlines():
print(" " * level, title)
except PDFNoOutlines:
print("no bookmarks")Values in doc.info come back as raw bytes, so decode them yourself and expect encoding surprises. get_outlines raises PDFNoOutlines rather than yielding nothing when the document has no bookmarks.
Tell a scanned PDF from a digital one before you parse itdetect-scanned-pdf
from pdfminer.high_level import extract_text
def needs_ocr(path, sample_pages=3, min_chars=100):
text = extract_text(path, maxpages=sample_pages)
return len(text.strip()) < min_chars
if needs_ocr("unknown.pdf"):
print("run OCR first, pdfminer will return nothing useful")There is no built-in flag for this, and no exception is raised, so a scan silently produces empty output. Sampling the first few pages keeps the check cheap on long documents.
Extract from the command linecli-extraction
pdf2txt.py example.pdf > out.txt
pdf2txt.py -p 1,2 -o out.txt example.pdf
pdf2txt.py -P hunter2 protected.pdf
# inspect the PDF object structure
dumppdf.py -a example.pdf | head -40The .py suffix is part of the installed command name, which trips up anyone typing pdf2txt. dumppdf.py is the tool to reach for when extraction returns nothing and you need to see what objects the file actually contains.
Write output through the low-level entry pointstream-output-to-file
from io import StringIO
from pdfminer.high_level import extract_text_to_fp
from pdfminer.layout import LAParams
with open("example.pdf", "rb") as fin:
out = StringIO()
extract_text_to_fp(fin, out, laparams=LAParams(), output_type="text")
print(out.getvalue())This is what pdf2txt.py calls, and it takes options extract_text does not, including rotation, scale, and output_dir for images. Note that omitting laparams here is not the same as passing LAParams(): None skips layout analysis entirely.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pdfplumber | PyPI | You want tables, word-level extraction, cropping, and page images without writing layout code; it wraps this exact library and pins pdfminer.six==20260107. |
| pypdfium2 | PyPI | You need speed or page rendering to images and want permissive licensing, since it binds Google's pdfium under BSD and Apache terms. |
| pymupdf | PyPI | You want the fastest and most complete extraction available and your project can accept AGPL, or you are willing to buy the commercial license. |
| pypdf | PyPI | Your job is manipulating files (merge, split, rotate, encrypt) with rough text extraction on the side, and you want a pure-Python package with light dependencies. |