PDFLoader
On this page
Module: pdf_loader · Extends BaseFileLoader
Overview
Loads PDF documents using a three-backend fallback chain, trying each in order until one succeeds. This is the only loader in the package with multi-backend redundancy, which makes it resilient to missing dependencies and to backend-specific parsing failures on malformed PDFs.
Why it exists: different PDF libraries have different strengths and weaknesses — PyMuPDF is fast and has the best Arabic/RTL text extraction, pdfplumber extracts tables well, and pypdf is a pure-Python fallback with no native dependencies. Rather than forcing a single choice, PDFLoader tries the best option first and degrades gracefully.
Signature
class PDFLoader(BaseFileLoader):
SUPPORTED_EXTENSIONS = [".pdf"]
def __init__(
self,
file_path: str,
config: Optional[PDFLoaderConfig] = None,
per_page: bool = True,
password: Optional[str] = None,
): ...
def load(self) -> List[LoadedDocument]: ...
Parameters
See PDFLoaderConfig for the full field set including start_page, end_page, and page_separator (only settable via config=, not as direct kwargs here).
Returns
List[LoadedDocument] — one per page if per_page=True (pages with no extractable text are skipped, not returned as empty documents), or a single document containing all pages joined by page_separator if per_page=False.
Raises
FileNotFoundError/ValueError— at construction, fromBaseFileLoader._validate_file()ValueError— "PDF is password-protected..." / "Wrong password for PDF." (fitz backend) if encrypted and no/wrong password is givenValueError— "PDF is encrypted..." (pypdf backend, equivalent case)RuntimeError— if all three backends fail (none installed, or all raised non-ImportErrorexceptions); the message includes the last error encountered and install instructions
Internal workflow — backend fallback chain
backends = [
("fitz", self._load_with_fitz),
("pdfplumber", self._load_with_pdfplumber),
("pypdf", self._load_with_pypdf),
]
for backend_name, load_fn in backends:
try:
return load_fn()
except ImportError:
continue # try next backend
except Exception as e:
last_error = e
continue # try next backend even on a parse error
Key behavior: any exception from a backend — not just ImportError — causes a fallback to the next backend. A backend that's installed but fails to parse a specific malformed PDF will not abort the whole operation; the next backend gets a chance. Only if every backend fails does load() raise.
See also the PDF backend fallback diagram in the architecture page.
Backend details
1. PyMuPDF (fitz) — tried first
- Fastest backend, best Arabic/RTL text extraction.
- Checks
pdf.needs_passand authenticates withconfig.passwordif needed. - Extracts PDF metadata (
title,author,subject,total_pages) into every document's metadata. per_page=True: iteratesrange(start_page, end_page or page_count), callingpage.get_text("text").strip(); pages whose extracted text is empty are skipped entirely (no placeholder document is created for blank/image-only pages).per_page=False: joins all non-empty page texts withpage_separator.
2. pdfplumber — tried second
- Good table extraction (though
PDFLoaderitself doesn't extract tables — seeTableLoaderfor that). - Password passed as
password=self.config.password or "". - Same per-page skip-if-empty and slicing behavior as the fitz backend, using
pdf.pages[start:end].
3. pypdf (or legacy PyPDF2) — tried last
- Pure-Python, no compiled dependencies.
- Imports
pypdf.PdfReader, falling back to the legacyPyPDF2.PdfReadername ifpypdfisn't installed. - Checks
reader.is_encryptedand callsreader.decrypt(password). - Same per-page logic, using
reader.pages[start:end].
Metadata produced
Common across all backends (field names normalized): pdf_title, pdf_author, total_pages, backend (which backend actually succeeded — "fitz", "pdfplumber", or "pypdf"), plus page_number when per_page=True. The fitz backend additionally includes pdf_subject.
Complexity
- Time: O(n) in number of pages/characters extracted; dominated by the underlying C/Rust extraction library, not by this wrapper.
- Space: O(total extracted text) — the entire requested page range is held in memory as Python strings; there is no streaming mode for PDFs.
Thread safety
Not thread-safe for concurrent calls against the same PDFLoader instance (no internal locking), but separate instances loading separate files can safely run in different threads — this is exactly what DirectoryLoader does.
Examples
Basic — load all pages:
from zerda.document_loaders import PDFLoader
docs = PDFLoader("report.pdf").load()
print(f"{len(docs)} non-empty pages loaded")
Whole document as a single string:
docs = PDFLoader("report.pdf", per_page=False).load()
full_text = docs[0].page_content
Page range selection:
from zerda.document_loaders import PDFLoader, PDFLoaderConfig
config = PDFLoaderConfig(start_page=4, end_page=10) # pages 5–10, zero-indexed start
docs = PDFLoader("book.pdf", config=config).load()
Password-protected PDF:
docs = PDFLoader("confidential.pdf", password="secret123").load()
Error handling — no backend available:
try:
docs = PDFLoader("scan.pdf").load()
except RuntimeError as e:
print(f"Install a PDF backend: {e}")
# "All PDF backends failed for scan.pdf. Install at least one of:
# PyMuPDF, pdfplumber, pypdf. Last error: ..."
Production usage — graceful degradation in a pipeline:
from zerda.document_loaders import PDFLoader
def safe_load_pdf(path: str):
try:
return PDFLoader(path).load()
except RuntimeError:
logger.error(f"Skipping unreadable PDF: {path}")
return []
except ValueError as e:
logger.error(f"PDF requires a password: {path} — {e}")
return []
Best practices
- Install PyMuPDF (
pip install pymupdf) as your primary backend if your corpus includes Arabic or other RTL text — it has the strongest extraction quality among the three. - Use
per_page=True(the default) when you plan to chunk/embed per page or need page-number citations; useper_page=Falseonly when you genuinely want one blob per file. - Remember that
config.extract_imagesexists inPDFLoaderConfigbut has no effect inPDFLoader(see Configuration → caveat) — usePDFMultimodalLoaderif you need OCR on embedded images.
Common mistakes
- Assuming a page that produced no
LoadedDocumentmeans extraction failed — it more likely means the page genuinely had no extractable digital text (e.g. a scanned image page). UsePDFMultimodalLoaderwithocr_images=Truefor scanned content. - Setting
extract_images=TrueinPDFLoaderConfigand expecting image text to appear — this field is read nowhere inpdf_loader.py. - Not handling
ValueErrorfor password-protected PDFs separately fromRuntimeErrorfor missing backends — they represent very different failure modes.
Related APIs
PDFLoaderConfigTableLoader— dedicated PDF table extraction via pdfplumber/camelotLoadedDocument