PDF — Python Manipulation Guide

SkillFiles & storage

How to programmatically create, modify, and verify PDF files using Python PyMuPDF (fitz), pikepdf, and reportlab. For setup-gen and reward-gen agents.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the PDF — Python Manipulation Guide skill

What this skill tells your AI

The instructions your AI receives, as published by xlang-ai/cua-gym in .claude/skills/pdf/SKILL.md and read by ahel’s review.

This skill teaches setup-gen (create/modify PDFs) and reward-gen (read/verify PDF properties) how to work with PDF files using pure Python code.

  • Libraries: PyMuPDF (fitz), pikepdf, reportlab
  • Install: pip3 install PyMuPDF pikepdf reportlab
  • File formats: .pdf
  • PDF viewers on VM: evince (GNOME default), okular, xdg-open

Library roles:

LibraryStrengthUse for
PyMuPDF (fitz)Read/write/annotate/renderMost setup & all reward tasks
pikepdfLow-level PDF structure, encryption, metadataEncryption, metadata, merge/split
reportlabCreate PDFs from scratch with complex layoutsRich document generation

0. GUI Startup on VM (for setup-gen)

After generating /home/user/<task_id>_initial.pdf, setup-gen should open the PDF in Evince for the GUI agent.

CRITICAL VM LIMIT: GUI launches must set DISPLAY=:0.

import os
import shlex
import subprocess
import time

def launch_gui(command: str, delay_sec: float = 1.0):
    env = os.environ.copy()
    env["DISPLAY"] = ":0"
    subprocess.Popen(
        shlex.split(command),
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        env=env,
    )
    time.sleep(delay_sec)

# Open initial PDF in Evince (GNOME default PDF viewer)
launch_gui('evince "/home/user/<task_id>_initial.pdf"', delay_sec=2.0)

# Open at a specific page
launch_gui('evince --page-index=3 "/home/user/<task_id>_initial.pdf"', delay_sec=2.0)

# Open in presentation mode
launch_gui('evince --presentation "/home/user/<task_id>_initial.pdf"', delay_sec=2.0)

# Alternative: use xdg-open (system default)
launch_gui('xdg-open "/home/user/<task_id>_initial.pdf"', delay_sec=2.0)

Guidelines:

  • Open *_initial.pdf, never *_golden.pdf.
  • Use non-blocking launch (Popen) and short delays.
  • Evince is preferred over okular for GNOME desktops.

1. Creating & Modifying PDFs (setup-gen)

1.1 PyMuPDF (fitz) — Primary Tool

Creating a New PDF
import pymupdf  # or: import fitz
import shutil

# Create blank PDF with one page
doc = pymupdf.open()  # new empty PDF
page = doc.new_page(width=595, height=842)  # A4 size in points (72 pts/inch)
# Letter size: width=612, height=792
doc.save("/home/user/Desktop/blank.pdf")
doc.close()
Page Size Constants
# Common sizes in points (72 pts = 1 inch)
A4_WIDTH, A4_HEIGHT = 595, 842          # 210mm x 297mm
LETTER_WIDTH, LETTER_HEIGHT = 612, 792  # 8.5" x 11"
A3_WIDTH, A3_HEIGHT = 842, 1191         # 297mm x 420mm
LEGAL_WIDTH, LEGAL_HEIGHT = 612, 1008   # 8.5" x 14"
Inserting Text
doc = pymupdf.open()
page = doc.new_page(width=595, height=842)

# Simple text insertion at a point (x, y from top-left)
page.insert_text(
    pymupdf.Point(72, 72),      # position (1 inch from top-left)
    "Hello, World!",
    fontsize=16,
    fontname="helv",             # Helvetica (built-in)
    color=(0, 0, 0),             # black, RGB floats 0-1
)

# Text with different fonts
page.insert_text(pymupdf.Point(72, 100), "Bold Helvetica", fontsize=12, fontname="hebo")  # Helvetica-Bold
page.insert_text(pymupdf.Point(72, 120), "Italic Times", fontsize=12, fontname="tiit")    # Times-Italic
page.insert_text(pymupdf.Point(72, 140), "Courier", fontsize=12, fontname="cour")         # Courier

# Text in a bounded rectangle (auto-wraps)
rect = pymupdf.Rect(72, 200, 523, 400)  # (x0, y0, x1, y1)
excess = page.insert_textbox(
    rect,
    "This is a long paragraph that will automatically wrap within the rectangle boundaries. "
    "The function returns excess text that didn't fit.",
    fontsize=11,
    fontname="helv",
    color=(0, 0, 0),
    align=pymupdf.TEXT_ALIGN_JUSTIFY,  # LEFT=0, CENTER=1, RIGHT=2, JUSTIFY=3
)

doc.save("/home/user/Desktop/text.pdf")
doc.close()
Built-in Font Names
helv     = Helvetica              hebo = Helvetica-Bold
heit     = Helvetica-Oblique      hebi = Helvetica-BoldOblique
tiro     = Times-Roman            tibo = Times-Bold
tiit     = Times-Italic           tibi = Times-BoldItalic
cour     = Courier                cobo = Courier-Bold
coit     = Courier-Oblique        cobi = Courier-BoldOblique
symb     = Symbol                 zadb = ZapfDingbats
Using Custom/External Fonts
import pymupdf

doc = pymupdf.open()
page = doc.new_page()

# Register an external TrueType font
font = pymupdf.Font(fontfile="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
# Or use a built-in font by name:
# font = pymupdf.Font("helv")

tw = pymupdf.TextWriter(page.rect)
tw.append(pymupdf.Point(72, 72), "Custom font text", font=font, fontsize=14)
tw.write_text(page, color=(0, 0, 0))

doc.save("/home/user/Desktop/custom_font.pdf")
doc.close()
Inserting HTML Content (Stories)
import pymupdf

doc = pymupdf.open()

# Create formatted content from HTML
html = """
<h1 style="color: navy;">Quarterly Report</h1>
<p style="font-size: 12px;">This report covers Q1 2024 performance metrics.</p>
<table border="1">
    <tr><th>Month</th><th>Revenue</th><th>Growth</th></tr>
    <tr><td>January</td><td>$50,000</td><td>+5%</td></tr>
    <tr><td>February</td><td>$55,000</td><td>+10%</td></tr>
    <tr><td>March</td><td>$60,000</td><td>+9%</td></tr>
</table>
<p><b>Summary:</b> Strong growth trajectory across all metrics.</p>
"""

story = pymupdf.Story(html=html)
body = story.body

# Layout story onto pages
writer = pymupdf.DocumentWriter(doc)
content_rect = pymupdf.Rect(72, 72, 523, 770)  # margins: ~1 inch

more = True
while more:
    dev = writer.begin_page(pymupdf.Rect(0, 0, 595, 842))
    more, _ = story.place(content_rect)
    story.draw(dev)
    writer.end_page()

writer.close()
doc.save("/home/user/Desktop/html_report.pdf")
doc.close()
Drawing Shapes
doc = pymupdf.open()
page = doc.new_page()
shape = page.new_shape()

# Rectangle
rect = pymupdf.Rect(100, 100, 300, 200)
shape.draw_rect(rect)
shape.finish(color=(0, 0, 1), fill=(0.8, 0.8, 1), width=2)  # blue border, light blue fill

# Circle
shape.draw_circle(pymupdf.Point(400, 150), 50)  # center, radius
shape.finish(color=(1, 0, 0), fill=(1, 0.8, 0.8), width=1.5)

# Line
shape.draw_line(pymupdf.Point(72, 300), pymupdf.Point(523, 300))
shape.finish(color=(0, 0, 0), width=1, dashes="[3 3]")  # dashed line

# Polygon (triangle)
shape.draw_polyline([
    pymupdf.Point(200, 400),
    pymupdf.Point(150, 500),
    pymupdf.Point(250, 500),
    pymupdf.Point(200, 400),  # close the polygon
])
shape.finish(color=(0, 0.5, 0), fill=(0, 1, 0), width=1)

# Bezier curve
shape.draw_bezier(
    pymupdf.Point(300, 400),  # start
    pymupdf.Point(350, 350),  # control 1
    pymupdf.Point(450, 450),  # control 2
    pymupdf.Point(500, 400),  # end
)
shape.finish(color=(0.5, 0, 0.5), width=2)

shape.commit()  # CRITICAL: must commit all shapes to page
doc.save("/home/user/Desktop/shapes.pdf")
doc.close()
Inserting Images
doc = pymupdf.open()
page = doc.new_page()

# Insert image from file into a rectangle
img_rect = pymupdf.Rect(72, 72, 300, 250)
page.insert_image(img_rect, filename="/home/user/Desktop/photo.png")

# Insert image from bytes
with open("/home/user/Desktop/logo.png", "rb") as f:
    img_data = f.read()
page.insert_image(pymupdf.Rect(350, 72, 523, 200), stream=img_data)

# Insert with rotation (0, 90, 180, 270)
page.insert_image(pymupdf.Rect(72, 300, 250, 500), filename="/home/user/Desktop/photo.png", rotate=90)

# Insert keeping aspect ratio — use Rect.fit() to calculate
img_doc = pymupdf.open("/home/user/Desktop/photo.png")
img_page = img_doc[0]
img_w, img_h = img_page.rect.width, img_page.rect.height
target = pymupdf.Rect(72, 500, 300, 700)
# Scale image rect to fit within target maintaining aspect ratio
scale = min(target.width / img_w, target.height / img_h)
img_doc.close()

doc.save("/home/user/Desktop/images.pdf")
doc.close()
Adding Annotations
doc = pymupdf.open("/home/user/Desktop/document.pdf")
page = doc[0]

# Highlight text (search first, then highlight)
text_instances = page.search_for("important")
for inst in text_instances:
    highlight = page.add_highlight_annot(inst)
    highlight.set_colors(stroke=(1, 1, 0))  # yellow
    highlight.update()

# Underline text
for inst in page.search_for("underline this"):
    annot = page.add_underline_annot(inst)
    annot.update()

# Strikeout text
for inst in page.search_for("delete this"):
    annot = page.add_strikeout_annot(inst)
    annot.update()

# Sticky note (text annotation)
annot = page.add_text_annot(
    pymupdf.Point(100, 100),
    "This is a comment note",
    icon="Note"  # "Comment", "Key", "Note", "Help", "NewParagraph", "Paragraph", "Insert"
)
annot.set_colors(stroke=(1, 0.8, 0))  # orange icon
annot.update()

# FreeText annotation (text directly on page)
annot = page.add_freetext_annot(
    pymupdf.Rect(72, 600, 300, 640),
    "This is an inline comment",
    fontsize=10,
    fontname="helv",
    text_color=(1, 0, 0),     # red text
    fill_color=(1, 1, 0.8),   # light yellow background
    border_color=(0, 0, 0),   # black border
)
annot.update()

# Rectangle annotation (box around area)
annot = page.add_rect_annot(pymupdf.Rect(350, 200, 500, 300))
annot.set_colors(stroke=(1, 0, 0))  # red border
annot.set_border(width=2)
annot.update()

# Ink annotation (freehand drawing)
annot = page.add_ink_annot([
    [pymupdf.Point(100, 400), pymupdf.Point(150, 380), pymupdf.Point(200, 420)],
])
annot.set_colors(stroke=(0, 0, 1))  # blue ink
annot.set_border(width=2)
annot.update()

# Stamp annotation
annot = page.add_stamp_annot(
    pymupdf.Rect(350, 400, 500, 460),
    stamp=0  # 0=Approved, 1=AsIs, 2=Confidential, 3=Departmental, etc.
)
annot.update()

doc.save("/home/user/Desktop/annotated.pdf")
doc.close()
Adding Links
doc = pymupdf.open("/home/user/Desktop/document.pdf")
page = doc[0]

# Link to external URL
link_rect = pymupdf.Rect(72, 700, 250, 720)
page.insert_link({
    "kind": pymupdf.LINK_URI,
    "from": link_rect,
    "uri": "https://www.example.com",
})

# Link to another page within PDF (internal link)
page.insert_link({
    "kind": pymupdf.LINK_GOTO,
    "from": pymupdf.Rect(72, 730, 250, 750),
    "page": 2,           # target page number (0-indexed)
    "to": pymupdf.Point(72, 72),  # position on target page
})

# Add visible text for the link
page.insert_text(pymupdf.Point(72, 715), "Click here for example.com",
                 fontsize=10, color=(0, 0, 1))

doc.save("/home/user/Desktop/links.pdf")
doc.close()
Table of Contents (Bookmarks)
doc = pymupdf.open("/home/user/Desktop/document.pdf")

# Get existing TOC
toc = doc.get_toc()  # returns [[level, title, page_num], ...]

# Set new TOC
new_toc = [
    [1, "Chapter 1: Introduction", 1],
    [2, "1.1 Background", 1],
    [2, "1.2 Objectives", 2],
    [1, "Chapter 2: Methods", 3],
    [2, "2.1 Data Collection", 3],
    [2, "2.2 Analysis", 4],
    [1, "Chapter 3: Results", 5],
]
doc.set_toc(new_toc)

doc.save("/home/user/Desktop/with_toc.pdf")
doc.close()
Page Manipulation
doc = pymupdf.open("/home/user/Desktop/document.pdf")

# Rotate a page (0, 90, 180, 270)
doc[0].set_rotation(90)

# Delete pages
doc.delete_page(2)            # delete page at index 2
doc.delete_pages(from_page=5, to_page=8)  # delete pages 5-8

# Move page (page 5 to position after page 1)
doc.move_page(5, 1)

# Copy page (copy page 0 to end)
doc.copy_page(0)              # appends copy to end
doc.copy_page(0, 3)           # inserts copy before page 3

# Insert blank pages
doc.new_page(pno=2, width=595, height=842)  # insert A4 blank at index 2
doc.new_page(pno=-1)  # append to end

# Select specific pages (keep only these, remove rest)
doc.select([0, 2, 4, 6])     # keep pages 0, 2, 4, 6 only

# Rearrange pages (reverse order)
page_count = doc.page_count
doc.select(list(range(page_count - 1, -1, -1)))

doc.save("/home/user/Desktop/modified.pdf")
doc.close()
Merging PDFs
doc1 = pymupdf.open("/home/user/Desktop/file1.pdf")
doc2 = pymupdf.open("/home/user/Desktop/file2.pdf")

# Append all pages of doc2 to doc1
doc1.insert_pdf(doc2)

# Insert specific pages from doc2
doc1.insert_pdf(doc2, from_page=0, to_page=2, start_at=1)  # insert doc2 pages 0-2 after page 0 of doc1

doc1.save("/home/user/Desktop/merged.pdf")
doc1.close()
doc2.close()
Splitting PDF
doc = pymupdf.open("/home/user/Desktop/big.pdf")

# Split into individual pages
for i in range(doc.page_count):
    new_doc = pymupdf.open()
    new_doc.insert_pdf(doc, from_page=i, to_page=i)
    new_doc.save(f"/home/user/Desktop/page_{i+1}.pdf")
    new_doc.close()

# Split into chunks of N pages
chunk_size = 5
for start in range(0, doc.page_count, chunk_size):
    end = min(start + chunk_size - 1, doc.page_count - 1)
    new_doc = pymupdf.open()
    new_doc.insert_pdf(doc, from_page=start, to_page=end)
    new_doc.save(f"/home/user/Desktop/chunk_{start//chunk_size + 1}.pdf")
    new_doc.close()

doc.close()
Watermarks & Overlays
doc = pymupdf.open("/home/user/Desktop/document.pdf")

for page in doc:
    # Text watermark (diagonal)
    # Insert with rotation using a text writer
    page.insert_text(
        pymupdf.Point(150, 500),
        "CONFIDENTIAL",
        fontsize=60,
        fontname="helv",
        color=(1, 0, 0),       # red
        rotate=45,             # degrees counter-clockwise
        overlay=True,          # on top of existing content
    )

    # Or use opacity for semi-transparent watermark
    shape = page.new_shape()
    shape.insert_text(
        pymupdf.Point(100, 400),
        "DRAFT",
        fontsize=72,
        fontname="hebo",
        color=(0.8, 0.8, 0.8),
    )
    shape.finish()
    shape.commit()

    # Image watermark
    logo_rect = pymupdf.Rect(400, 700, 520, 770)
    page.insert_image(logo_rect, filename="/home/user/Desktop/watermark.png",
                      overlay=True)

doc.save("/home/user/Desktop/watermarked.pdf")
doc.close()
Form Fields (Widgets)
import pymupdf

doc = pymupdf.open()
page = doc.new_page()

# Text field
widget = pymupdf.Widget()
widget.field_type = pymupdf.PDF_WIDGET_TYPE_TEXT
widget.field_name = "full_name"
widget.field_value = "John Doe"
widget.rect = pymupdf.Rect(150, 100, 400, 125)
widget.text_fontsize = 12
widget.text_color = (0, 0, 0)
widget.fill_color = (0.95, 0.95, 0.95)
widget.border_color = (0, 0, 0)
widget.border_width = 1
page.add_widget(widget)

# Multi-line text field
widget = pymupdf.Widget()
widget.field_type = pymupdf.PDF_WIDGET_TYPE_TEXT
widget.field_name = "comments"
widget.field_value = ""
widget.field_flags = pymupdf.PDF_TX_FIELD_IS_MULTILINE
widget.rect = pymupdf.Rect(150, 140, 400, 220)
widget.text_fontsize = 10
widget.fill_color = (1, 1, 1)
widget.border_color = (0.5, 0.5, 0.5)
page.add_widget(widget)

# Checkbox
widget = pymupdf.Widget()
widget.field_type = pymupdf.PDF_WIDGET_TYPE_CHECKBOX
widget.field_name = "agree_terms"
widget.field_value = "Yes"  # "Yes" = checked, "Off" = unchecked
widget.rect = pymupdf.Rect(150, 240, 170, 260)
widget.border_color = (0, 0, 0)
page.add_widget(widget)

# Combo box (dropdown)
widget = pymupdf.Widget()
widget.field_type = pymupdf.PDF_WIDGET_TYPE_COMBOBOX
widget.field_name = "country"
widget.choice_values = ["United States", "Canada", "United Kingdom", "Germany", "Japan"]
widget.field_value = "United States"
widget.rect = pymupdf.Rect(150, 280, 400, 305)
widget.text_fontsize = 11
widget.fill_color = (1, 1, 1)
widget.border_color = (0, 0, 0)
page.add_widget(widget)

# List box
widget = pymupdf.Widget()
widget.field_type = pymupdf.PDF_WIDGET_TYPE_LISTBOX
widget.field_name = "skills"
widget.choice_values = ["Python", "Java", "C++", "JavaScript", "Go", "Rust"]
widget.field_value = "Python"
widget.rect = pymupdf.Rect(150, 320, 400, 420)
widget.text_fontsize = 10
widget.fill_color = (1, 1, 1)
widget.border_color = (0, 0, 0)
page.add_widget(widget)

# Radio button (requires special handling)
widget = pymupdf.Widget()
widget.field_type = pymupdf.PDF_WIDGET_TYPE_RADIOBUTTON
widget.field_name = "priority"
widget.field_value = "High"
widget.rect = pymupdf.Rect(150, 440, 170, 460)
widget.border_color = (0, 0, 0)
page.add_widget(widget)

# Add labels next to fields
page.insert_text(pymupdf.Point(72, 118), "Full Name:", fontsize=12, fontname="hebo")
page.insert_text(pymupdf.Point(72, 158), "Comments:", fontsize=12, fontname="hebo")
page.insert_text(pymupdf.Point(72, 255), "Agree to Terms:", fontsize=12, fontname="hebo")
page.insert_text(pymupdf.Point(72, 298), "Country:", fontsize=12, fontname="hebo")
page.insert_text(pymupdf.Point(72, 338), "Skills:", fontsize=12, fontname="hebo")
page.insert_text(pymupdf.Point(72, 455), "Priority:", fontsize=12, fontname="hebo")

doc.save("/home/user/Desktop/form.pdf")
doc.close()
Modifying Existing Form Field Values
doc = pymupdf.open("/home/user/Desktop/form.pdf")
page = doc[0]

for widget in page.widgets():
    if widget.field_name == "full_name":
        widget.field_value = "Jane Smith"
        widget.update()
    elif widget.field_name == "country":
        widget.field_value = "Canada"
        widget.update()
    elif widget.field_name == "agree_terms":
        widget.field_value = "Yes"
        widget.update()

doc.save("/home/user/Desktop/filled_form.pdf")
doc.close()
Setting Metadata
doc = pymupdf.open("/home/user/Desktop/document.pdf")

doc.set_metadata({
    "title": "Annual Report 2024",
    "author": "John Smith",
    "subject": "Financial Performance",
    "keywords": "finance, annual, report, 2024",
    "creator": "CUA-Gym Setup",
    "producer": "PyMuPDF",
})

doc.save("/home/user/Desktop/with_metadata.pdf")
doc.close()
Page Cropping & CropBox
doc = pymupdf.open("/home/user/Desktop/document.pdf")
page = doc[0]

# Get original mediabox (full page) and cropbox (visible area)
print(page.mediabox)  # Rect(0, 0, 595, 842)
print(page.cropbox)   # Rect(0, 0, 595, 842) — same by default

# Crop page to specific area (trims visible content)
page.set_cropbox(pymupdf.Rect(72, 72, 523, 770))  # 1-inch margins

doc.save("/home/user/Desktop/cropped.pdf")
doc.close()

1.2 pikepdf — Encryption & Low-Level Operations

Encryption
import pikepdf

# Encrypt a PDF
pdf = pikepdf.open("/home/user/Desktop/document.pdf")
pdf.save(
    "/home/user/Desktop/encrypted.pdf",
    encryption=pikepdf.Encryption(
        owner="owner_password_123",  # full permissions password
        user="user_password_456",    # open/view password
        R=6,                         # encryption revision (6 = AES-256)
        allow=pikepdf.Permissions(
            extract=False,           # disallow text extraction
            modify_annotation=True,  # allow annotation editing
            print_lowres=True,       # allow low-res printing
            print_highres=True,      # allow high-res printing
            modify_form=True,        # allow form filling
            modify_other=False,      # disallow other modifications
            modify_assembly=False,   # disallow page assembly
        ),
    ),
)

# Open encrypted PDF
pdf = pikepdf.open("/home/user/Desktop/encrypted.pdf", password="user_password_456")

# Remove encryption (need owner password)
pdf = pikepdf.open("/home/user/Desktop/encrypted.pdf", password="owner_password_123")
pdf.save("/home/user/Desktop/decrypted.pdf")  # save without encryption param = no encryption
Merge & Split with pikepdf
import pikepdf

# Merge PDFs
output = pikepdf.new()
for path in ["/home/user/Desktop/file1.pdf", "/home/user/Desktop/file2.pdf"]:
    src = pikepdf.open(path)
    output.pages.extend(src.pages)

output.save("/home/user/Desktop/merged.pdf")

# Split: extract pages 2-5 (0-indexed)
pdf = pikepdf.open("/home/user/Desktop/big.pdf")
output = pikepdf.new()
output.pages.extend(pdf.pages[1:5])
output.save("/home/user/Desktop/pages_2_to_5.pdf")

# Reverse page order
pdf = pikepdf.open("/home/user/Desktop/document.pdf")
pdf.pages.reverse()
pdf.save("/home/user/Desktop/reversed.pdf")

# Remove specific pages
pdf = pikepdf.open("/home/user/Desktop/document.pdf")
del pdf.pages[2]      # delete page at index 2
del pdf.pages[0:3]    # delete first 3 pages
pdf.save("/home/user/Desktop/trimmed.pdf")
Metadata with pikepdf
import pikepdf

pdf = pikepdf.open("/home/user/Desktop/document.pdf")

# Read metadata via XMP
with pdf.open_metadata() as meta:
    print(meta.get("dc:title", ""))
    print(meta.get("dc:creator", ""))
    print(meta.get("xmp:CreatorTool", ""))

# Write metadata
with pdf.open_metadata() as meta:
    meta["dc:title"] = "Updated Title"
    meta["dc:creator"] = ["Author Name"]
    meta["dc:description"] = "A detailed description"
    meta["xmp:CreatorTool"] = "CUA-Gym"
    meta["pdf:Producer"] = "pikepdf"
    meta["pdf:Keywords"] = "keyword1, keyword2"

pdf.save("/home/user/Desktop/metadata_updated.pdf")

# Remove all metadata
pdf = pikepdf.open("/home/user/Desktop/document.pdf")
if "/Metadata" in pdf.Root:
    del pdf.Root["/Metadata"]
if pdf.docinfo:
    for key in list(pdf.docinfo.keys()):
        del pdf.docinfo[key]
pdf.save("/home/user/Desktop/no_metadata.pdf")
Rotate Pages with pikepdf
import pikepdf

pdf = pikepdf.open("/home/user/Desktop/document.pdf")

# Rotate specific page
pdf.pages[0].Rotate = 90    # clockwise rotation in degrees (90, 180, 270)

# Rotate all pages
for page in pdf.pages:
    page.Rotate = 180

pdf.save("/home/user/Desktop/rotated.pdf")

1.3 reportlab — Creating Rich PDFs from Scratch

Simple Canvas Drawing
from reportlab.lib.pagesizes import A4, letter
from reportlab.pdfgen import canvas
from reportlab.lib.colors import red, blue, black, green, HexColor
from reportlab.lib.units import inch, cm, mm

c = canvas.Canvas("/home/user/Desktop/reportlab_basic.pdf", pagesize=A4)
width, height = A4

# Text
c.setFont("Helvetica", 24)
c.drawString(72, height - 72, "Title Text")

c.setFont("Helvetica", 12)
c.drawString(72, height - 120, "Regular paragraph text goes here.")

# Right-aligned text
c.drawRightString(width - 72, height - 120, "Right aligned")

# Centered text
c.drawCentredString(width / 2, height - 160, "Centered text")

# Shapes
c.setStrokeColor(blue)
c.setFillColor(HexColor("#E0E0FF"))
c.rect(72, height - 300, 200, 100, fill=1)  # filled rectangle

c.setStrokeColor(red)
c.circle(400, height - 250, 50, fill=0)  # circle outline

c.setStrokeColor(black)
c.line(72, height - 350, width - 72, height - 350)  # horizontal line

# Image
c.drawImage("/home/user/Desktop/photo.png", 72, height - 550, width=200, height=150)

c.showPage()  # finish page
c.save()
Platypus Document with Tables
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image, PageBreak
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.colors import black, grey, lightgrey, HexColor
from reportlab.lib.units import inch

doc = SimpleDocTemplate("/home/user/Desktop/report.pdf", pagesize=A4,
                        leftMargin=72, rightMargin=72, topMargin=72, bottomMargin=72)

styles = getSampleStyleSheet()
story = []

# Title
title_style = ParagraphStyle('CustomTitle', parent=styles['Title'],
                              fontSize=24, textColor=HexColor("#003366"), spaceAfter=20)
story.append(Paragraph("Annual Performance Report", title_style))
story.append(Spacer(1, 12))

# Body text
story.append(Paragraph("This report summarizes the key metrics for the fiscal year.", styles['Normal']))
story.append(Spacer(1, 12))

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
197
Forks
18
Last commit
Aug 2026

ahel review

  • K1binfo
    installs-packages

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
pdf-xlang-ai
Source
github.com/xlang-ai/cua-gym