Skip to content

07-05: Working with PDF Files

PDFs are everywhere — reports, invoices, research papers. Python can read text from them, merge/split files, and even create new ones.

pip install pypdf          # modern PDF reading/manipulation (successor to PyPDF2)
pip install reportlab      # creating PDFs from scratch
pip install pdfplumber     # better text extraction (handles tables)
pip install pymupdf        # (fitz) — fast, powerful, handles images

Reading PDF Text with pypdf

from pypdf import PdfReader

# Open a PDF
reader = PdfReader("document.pdf")

# Basic info
print(f"Pages: {len(reader.pages)}")
print(f"Encrypted: {reader.is_encrypted}")

# Metadata
meta = reader.metadata
print(f"Title:   {meta.title}")
print(f"Author:  {meta.author}")
print(f"Creator: {meta.creator}")

# Extract text from a page
page = reader.pages[0]       # first page (0-indexed)
text = page.extract_text()
print(text)

# Extract text from all pages
full_text = ""
for page in reader.pages:
    full_text += page.extract_text() + "\n"

print(full_text[:500])   # first 500 chars

Extracting Text Page by Page

from pypdf import PdfReader

def extract_pdf_text(path):
    """Extract text from all pages, return as list of strings."""
    reader = PdfReader(path)
    pages_text = []
    for i, page in enumerate(reader.pages):
        text = page.extract_text()
        if text:
            pages_text.append(text.strip())
        print(f"Extracted page {i+1}/{len(reader.pages)}")
    return pages_text

pages = extract_pdf_text("report.pdf")
for i, text in enumerate(pages, 1):
    print(f"--- Page {i} ---")
    print(text[:200])
    print()

Merging PDFs

from pypdf import PdfWriter, PdfReader

def merge_pdfs(input_files, output_file):
    """Merge multiple PDFs into one."""
    writer = PdfWriter()

    for path in input_files:
        reader = PdfReader(path)
        for page in reader.pages:
            writer.add_page(page)

    with open(output_file, "wb") as f:
        writer.write(f)

    print(f"Merged {len(input_files)} files into {output_file}")

merge_pdfs(
    ["chapter1.pdf", "chapter2.pdf", "chapter3.pdf"],
    "full_book.pdf"
)

Splitting a PDF

from pypdf import PdfWriter, PdfReader

def split_pdf(input_file, pages_per_chunk, output_prefix="chunk"):
    """Split a PDF into chunks of N pages."""
    reader = PdfReader(input_file)
    total = len(reader.pages)
    chunk_num = 0

    for start in range(0, total, pages_per_chunk):
        writer = PdfWriter()
        end = min(start + pages_per_chunk, total)
        for i in range(start, end):
            writer.add_page(reader.pages[i])
        out_path = f"{output_prefix}_{chunk_num+1}.pdf"
        with open(out_path, "wb") as f:
            writer.write(f)
        print(f"Created {out_path} (pages {start+1}-{end})")
        chunk_num += 1

split_pdf("large_report.pdf", pages_per_chunk=10)

Extract specific pages

from pypdf import PdfWriter, PdfReader

def extract_pages(input_file, page_numbers, output_file):
    """Extract specific pages (0-indexed) into a new PDF."""
    reader = PdfReader(input_file)
    writer = PdfWriter()

    for n in page_numbers:
        writer.add_page(reader.pages[n])

    with open(output_file, "wb") as f:
        writer.write(f)

# Extract pages 0, 2, 4 (1st, 3rd, 5th)
extract_pages("document.pdf", [0, 2, 4], "selected.pdf")

Rotating Pages

from pypdf import PdfWriter, PdfReader

reader = PdfReader("sideways.pdf")
writer = PdfWriter()

for page in reader.pages:
    page.rotate(90)    # 90, 180, or 270 degrees clockwise
    writer.add_page(page)

with open("rotated.pdf", "wb") as f:
    writer.write(f)

Adding Watermark

from pypdf import PdfWriter, PdfReader

def add_watermark(input_pdf, watermark_pdf, output_pdf):
    """Overlay watermark on every page."""
    reader    = PdfReader(input_pdf)
    watermark = PdfReader(watermark_pdf).pages[0]
    writer    = PdfWriter()

    for page in reader.pages:
        page.merge_page(watermark)    # merge watermark on top
        writer.add_page(page)

    with open(output_pdf, "wb") as f:
        writer.write(f)

add_watermark("original.pdf", "watermark.pdf", "watermarked.pdf")

Encrypting and Decrypting

from pypdf import PdfWriter, PdfReader

# Encrypt a PDF
def encrypt_pdf(input_file, output_file, password):
    reader = PdfReader(input_file)
    writer = PdfWriter()
    for page in reader.pages:
        writer.add_page(page)
    writer.encrypt(password)
    with open(output_file, "wb") as f:
        writer.write(f)

encrypt_pdf("document.pdf", "secure.pdf", "MySecret123")

# Decrypt / read a protected PDF
def read_protected(path, password):
    reader = PdfReader(path)
    if reader.is_encrypted:
        success = reader.decrypt(password)
        if not success:
            raise ValueError("Wrong password")
    return "\n".join(p.extract_text() for p in reader.pages)

text = read_protected("secure.pdf", "MySecret123")
print(text[:200])

Better Text Extraction with pdfplumber

pdfplumber is better at preserving layout and extracting tables:

import pdfplumber

# Basic text extraction
with pdfplumber.open("document.pdf") as pdf:
    for i, page in enumerate(pdf.pages):
        text = page.extract_text()
        print(f"--- Page {i+1} ---")
        print(text)

# Extract tables
with pdfplumber.open("report_with_tables.pdf") as pdf:
    page = pdf.pages[0]
    tables = page.extract_tables()
    for table in tables:
        for row in table:
            print(row)   # list of strings

# Crop a region of the page
with pdfplumber.open("document.pdf") as pdf:
    page = pdf.pages[0]
    # Crop (x0, top, x1, bottom) in points
    region = page.crop((50, 100, 400, 300))
    print(region.extract_text())

Creating PDFs with reportlab

from reportlab.lib.pagesizes import A4, letter
from reportlab.lib.units import cm, inch
from reportlab.pdfgen import canvas
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table
from reportlab.lib.styles import getSampleStyleSheet

# Simple canvas approach
def create_simple_pdf(filename):
    c = canvas.Canvas(filename, pagesize=A4)
    width, height = A4

    # Title
    c.setFont("Helvetica-Bold", 24)
    c.drawCentredString(width/2, height - 100, "Student Report")

    # Subtitle
    c.setFont("Helvetica", 14)
    c.setFillColor(colors.gray)
    c.drawCentredString(width/2, height - 130, "Academic Year 2024")

    # Line
    c.setStrokeColor(colors.black)
    c.line(50, height - 150, width - 50, height - 150)

    # Content
    c.setFont("Helvetica", 12)
    c.setFillColor(colors.black)
    y = height - 200

    students = [("Alice", 88.5, "A"), ("Bob", 92.0, "A"), ("Charlie", 79.3, "C")]
    for name, score, grade in students:
        c.drawString(100, y, f"{name}")
        c.drawString(250, y, f"Score: {score:.1f}")
        c.drawString(380, y, f"Grade: {grade}")
        y -= 25

    # Page number
    c.setFont("Helvetica", 10)
    c.drawRightString(width - 50, 30, "Page 1")

    c.save()

create_simple_pdf("report.pdf")

High-level document with Platypus

from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors

def create_report(filename):
    doc = SimpleDocTemplate(filename, pagesize=A4)
    styles = getSampleStyleSheet()
    story = []

    # Title
    story.append(Paragraph("Student Performance Report", styles["Title"]))
    story.append(Spacer(1, 20))

    # Body text
    story.append(Paragraph(
        "This report summarizes student performance for the current semester.",
        styles["BodyText"]
    ))
    story.append(Spacer(1, 10))

    # Table
    table_data = [
        ["Name", "Score", "Grade"],
        ["Alice",   "88.5", "A"],
        ["Bob",     "92.0", "A"],
        ["Charlie", "79.3", "C"],
    ]
    t = Table(table_data, colWidths=[200, 100, 100])
    t.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), colors.blue),
        ("TEXTCOLOR",  (0, 0), (-1, 0), colors.white),
        ("FONTNAME",   (0, 0), (-1, 0), "Helvetica-Bold"),
        ("ALIGN",      (0, 0), (-1, -1), "CENTER"),
        ("GRID",       (0, 0), (-1, -1), 0.5, colors.grey),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
    ]))
    story.append(t)

    doc.build(story)

create_report("styled_report.pdf")

Quick Summary

Task Library Code
Read text from PDF pypdf PdfReader + page.extract_text()
Better text/tables pdfplumber pdfplumber.open()
Merge PDFs pypdf PdfWriter + add_page()
Split PDF pypdf PdfWriter per chunk
Rotate pages pypdf page.rotate(90)
Watermark pypdf page.merge_page()
Encrypt pypdf writer.encrypt(password)
Create PDF reportlab canvas.Canvas() or SimpleDocTemplate

Exercises: 07-05: Exercises — PDF Files


⬅️ Previous: 07-04: Working with Excel Files ➡️ Next: 07-06: pathlib and File System Operations