一键导入
How to programmatically create, modify, and verify PDF files using Python PyMuPDF (fitz), pikepdf, and reportlab. For setup-gen and reward-gen agents.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
How to programmatically create, modify, and verify PDF files using Python PyMuPDF (fitz), pikepdf, and reportlab. For setup-gen and reward-gen agents.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
How to programmatically create, modify, and verify Blender 3D scenes using Python bpy (headless background mode, .blend files, render output). For setup-gen and reward-gen agents.
How to programmatically set up, control, and verify Google Chrome browser state using Python (Playwright CDP, SQLite, JSON configs). For setup-gen and reward-gen agents.
How to programmatically set up, manipulate, and verify draw.io diagram state using Python (Playwright CDP, XML injection via Edit Diagram menu, DOM verification). For setup-gen and reward-gen agents.
How to programmatically set up, manipulate, and verify Excalidraw whiteboard state using Python (Playwright CDP, localStorage JSON, scene elements). For setup-gen and reward-gen agents.
How to programmatically create, modify, and verify images using Python Pillow, NumPy, OpenCV, and scikit-image. For setup-gen and reward-gen agents.
How to set up and manipulate Grafana dashboard, alerting, and admin state via HTTP APIs for CUA-Gym tasks. For setup-gen and reward-gen agents.
| name | |
| description | How to programmatically create, modify, and verify PDF files using Python PyMuPDF (fitz), pikepdf, and reportlab. For setup-gen and reward-gen agents. |
| user-invocable | false |
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.
PyMuPDF (fitz), pikepdf, reportlabpip3 install PyMuPDF pikepdf reportlab.pdfevince (GNOME default), okular, xdg-openLibrary roles:
| Library | Strength | Use for |
|---|---|---|
| PyMuPDF (fitz) | Read/write/annotate/render | Most setup & all reward tasks |
| pikepdf | Low-level PDF structure, encryption, metadata | Encryption, metadata, merge/split |
| reportlab | Create PDFs from scratch with complex layouts | Rich document generation |
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:
*_initial.pdf, never *_golden.pdf.Popen) and short delays.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()
# 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"
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()
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
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()
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()
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()
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()
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()
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()
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()
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()
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()
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()
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()
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()
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()
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()
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()
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
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")
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")
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")
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()
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))
# Table
data = [
["Quarter", "Revenue", "Expenses", "Profit"],
["Q1", "$120,000", "$80,000", "$40,000"],
["Q2", "$150,000", "$90,000", "$60,000"],
["Q3", "$130,000", "$85,000", "$45,000"],
["Q4", "$170,000", "$95,000", "$75,000"],
]
table = Table(data, colWidths=[80, 100, 100, 100])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), HexColor("#003366")),
('TEXTCOLOR', (0, 0), (-1, 0), HexColor("#FFFFFF")),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 11),
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('GRID', (0, 0), (-1, -1), 0.5, grey),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [HexColor("#FFFFFF"), HexColor("#F0F0F0")]),
('TOPPADDING', (0, 0), (-1, -1), 6),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
]))
story.append(table)
story.append(Spacer(1, 24))
# Page break
story.append(PageBreak())
story.append(Paragraph("Page 2 content", styles['Heading1']))
doc.build(story)
import shutil
# ALWAYS copy-then-modify for golden files
shutil.copy("/home/user/Desktop/<task_id>_initial.pdf", "/home/user/Desktop/<task_id>_golden.pdf")
# Then modify the golden copy
import pymupdf
doc = pymupdf.open("/home/user/Desktop/<task_id>_golden.pdf")
page = doc[0]
# ... apply expected modifications ...
doc.save("/home/user/Desktop/<task_id>_golden.pdf")
doc.close()
import pymupdf
def verify_page_count(pdf_path: str, expected: int) -> bool:
try:
doc = pymupdf.open(pdf_path)
result = doc.page_count == expected
doc.close()
return result
except Exception:
return False
def verify_metadata(pdf_path: str, expected: dict) -> bool:
"""Check PDF metadata fields. expected keys: title, author, subject, keywords, creator, producer."""
try:
doc = pymupdf.open(pdf_path)
meta = doc.metadata
doc.close()
for key, value in expected.items():
actual = meta.get(key, "")
if actual is None:
actual = ""
if str(actual).strip().lower() != str(value).strip().lower():
return False
return True
except Exception:
return False
def verify_toc(pdf_path: str, expected_toc: list) -> bool:
"""expected_toc: [[level, title, page], ...]. page is 1-indexed."""
try:
doc = pymupdf.open(pdf_path)
toc = doc.get_toc()
doc.close()
if len(toc) != len(expected_toc):
return False
for actual, expected in zip(toc, expected_toc):
if actual[0] != expected[0]: # level
return False
if actual[1].strip() != expected[1].strip(): # title
return False
if actual[2] != expected[2]: # page number
return False
return True
except Exception:
return False
def verify_toc_has_entries(pdf_path: str, min_entries: int = 1) -> bool:
"""Verify TOC exists with at least min_entries."""
try:
doc = pymupdf.open(pdf_path)
toc = doc.get_toc()
doc.close()
return len(toc) >= min_entries
except Exception:
return False
def verify_encrypted(pdf_path: str) -> bool:
"""Check if PDF is encrypted (requires password to open)."""
try:
doc = pymupdf.open(pdf_path)
result = doc.is_encrypted
doc.close()
return result
except Exception:
return False
def verify_permissions(pdf_path: str, password: str, expected_perms: dict) -> bool:
"""Verify PDF permission flags after opening with password.
expected_perms keys: print, edit, copy, annotate, form, etc."""
try:
import pikepdf
pdf = pikepdf.open(pdf_path, password=password)
allow = pdf.allow
pdf.close()
for perm, expected in expected_perms.items():
actual = getattr(allow, perm, None)
if actual is None:
# Try alternate names
perm_map = {
"print": "print_lowres",
"edit": "modify_other",
"copy": "extract",
"annotate": "modify_annotation",
"form": "modify_form",
}
actual = getattr(allow, perm_map.get(perm, perm), None)
if actual != expected:
return False
return True
except Exception:
return False
def verify_password_protected(pdf_path: str, password: str) -> bool:
"""Verify that a specific password can open the PDF."""
try:
doc = pymupdf.open(pdf_path)
if not doc.is_encrypted:
doc.close()
return False
result = doc.authenticate(password)
doc.close()
return result > 0 # returns int: 0=failed, 1=user, 2=owner
except Exception:
return False
def verify_page_size(pdf_path: str, page_num: int, expected_width: float,
expected_height: float, tolerance: float = 1.0) -> bool:
"""Verify page dimensions in points. page_num is 0-indexed."""
try:
doc = pymupdf.open(pdf_path)
page = doc[page_num]
w, h = page.rect.width, page.rect.height
doc.close()
return (abs(w - expected_width) <= tolerance and
abs(h - expected_height) <= tolerance)
except Exception:
return False
def verify_page_rotation(pdf_path: str, page_num: int, expected_rotation: int) -> bool:
"""expected_rotation: 0, 90, 180, or 270."""
try:
doc = pymupdf.open(pdf_path)
rotation = doc[page_num].rotation
doc.close()
return rotation == expected_rotation
except Exception:
return False
def verify_page_landscape(pdf_path: str, page_num: int) -> bool:
"""Verify page is landscape (width > height)."""
try:
doc = pymupdf.open(pdf_path)
page = doc[page_num]
result = page.rect.width > page.rect.height
doc.close()
return result
except Exception:
return False
def verify_cropbox(pdf_path: str, page_num: int, expected_rect: tuple) -> bool:
"""expected_rect: (x0, y0, x1, y1) in points."""
try:
doc = pymupdf.open(pdf_path)
page = doc[page_num]
cb = page.cropbox
doc.close()
return (abs(cb.x0 - expected_rect[0]) < 1 and
abs(cb.y0 - expected_rect[1]) < 1 and
abs(cb.x1 - expected_rect[2]) < 1 and
abs(cb.y1 - expected_rect[3]) < 1)
except Exception:
return False
def get_page_text(pdf_path: str, page_num: int) -> str:
"""Extract plain text from a page."""
doc = pymupdf.open(pdf_path)
text = doc[page_num].get_text("text")
doc.close()
return text
def get_all_text(pdf_path: str) -> str:
"""Extract text from all pages."""
doc = pymupdf.open(pdf_path)
texts = []
for page in doc:
texts.append(page.get_text("text"))
doc.close()
return "\n".join(texts)
def verify_text_contains(pdf_path: str, expected_strings: list,
page_num: int = None) -> bool:
"""Verify PDF contains all expected strings."""
try:
if page_num is not None:
text = get_page_text(pdf_path, page_num)
else:
text = get_all_text(pdf_path)
return all(s in text for s in expected_strings)
except Exception:
return False
def verify_text_not_contains(pdf_path: str, forbidden_strings: list) -> bool:
"""Verify PDF does NOT contain any of the forbidden strings."""
try:
text = get_all_text(pdf_path)
return not any(s in text for s in forbidden_strings)
except Exception:
return False
def verify_text_exact(pdf_path: str, page_num: int, expected_text: str,
ignore_whitespace: bool = True) -> bool:
"""Compare page text with expected, optionally normalizing whitespace."""
try:
import re
actual = get_page_text(pdf_path, page_num)
if ignore_whitespace:
actual = re.sub(r'\s+', ' ', actual).strip()
expected_text = re.sub(r'\s+', ' ', expected_text).strip()
return actual == expected_text
except Exception:
return False
def get_text_blocks(pdf_path: str, page_num: int) -> list:
"""Get text blocks with positions: [(x0, y0, x1, y1, text, block_no, block_type), ...]"""
doc = pymupdf.open(pdf_path)
blocks = doc[page_num].get_text("blocks")
doc.close()
return blocks
def get_text_words(pdf_path: str, page_num: int) -> list:
"""Get individual words with positions: [(x0, y0, x1, y1, word, block_no, line_no, word_no), ...]"""
doc = pymupdf.open(pdf_path)
words = doc[page_num].get_text("words")
doc.close()
return words
def verify_text_in_region(pdf_path: str, page_num: int,
rect: tuple, expected_text: str) -> bool:
"""Verify specific text appears within a rectangular region.
rect: (x0, y0, x1, y1) in points."""
try:
doc = pymupdf.open(pdf_path)
page = doc[page_num]
clip = pymupdf.Rect(*rect)
text = page.get_textbox(clip)
doc.close()
return expected_text.strip() in text.strip()
except Exception:
return False
def verify_text_position(pdf_path: str, page_num: int,
search_text: str, expected_region: str) -> bool:
"""Verify text is in expected region: 'top', 'bottom', 'left', 'right', 'center'.
Uses page center as dividing point."""
try:
doc = pymupdf.open(pdf_path)
page = doc[page_num]
instances = page.search_for(search_text)
doc.close()
if not instances:
return False
rect = instances[0]
cx, cy = rect.x0 + rect.width / 2, rect.y0 + rect.height / 2
pw, ph = page.rect.width, page.rect.height
if expected_region == "top":
return cy < ph / 2
elif expected_region == "bottom":
return cy > ph / 2
elif expected_region == "left":
return cx < pw / 2
elif expected_region == "right":
return cx > pw / 2
elif expected_region == "center":
return (pw * 0.25 < cx < pw * 0.75 and ph * 0.25 < cy < ph * 0.75)
return False
except Exception:
return False
def get_page_fonts(pdf_path: str, page_num: int) -> list:
"""Get fonts used on a page: [(xref, ext, type, basefont, name, encoding), ...]"""
doc = pymupdf.open(pdf_path)
fonts = doc[page_num].get_fonts()
doc.close()
return fonts
def verify_font_used(pdf_path: str, page_num: int, font_name: str) -> bool:
"""Check if a specific font is used on the page."""
try:
fonts = get_page_fonts(pdf_path, page_num)
return any(font_name.lower() in str(f).lower() for f in fonts)
except Exception:
return False
def get_text_with_font_info(pdf_path: str, page_num: int) -> list:
"""Extract text spans with font details using dict mode.
Returns list of dicts with keys: text, font, size, color, flags (bold/italic/etc)."""
doc = pymupdf.open(pdf_path)
page = doc[page_num]
data = page.get_text("dict")
doc.close()
spans = []
for block in data["blocks"]:
if block["type"] != 0: # 0=text, 1=image
continue
for line in block["lines"]:
for span in line["spans"]:
spans.append({
"text": span["text"],
"font": span["font"],
"size": span["size"],
"color": span["color"], # integer RGB, convert with (color >> 16 & 0xFF, color >> 8 & 0xFF, color & 0xFF)
"flags": span["flags"], # bitmask: 1=superscript, 2=italic, 4=serif, 8=monospace, 16=bold
"bbox": span["bbox"], # (x0, y0, x1, y1)
})
return spans
def verify_text_style(pdf_path: str, page_num: int, search_text: str,
expected_font: str = None, expected_size: float = None,
expected_bold: bool = None, expected_italic: bool = None,
expected_color: tuple = None) -> bool:
"""Verify text style properties. expected_color is (R, G, B) 0-255."""
try:
spans = get_text_with_font_info(pdf_path, page_num)
for span in spans:
if search_text not in span["text"]:
continue
if expected_font and expected_font.lower() not in span["font"].lower():
return False
if expected_size and abs(span["size"] - expected_size) > 0.5:
return False
if expected_bold is not None:
is_bold = bool(span["flags"] & 16)
if is_bold != expected_bold:
return False
if expected_italic is not None:
is_italic = bool(span["flags"] & 2)
if is_italic != expected_italic:
return False
if expected_color:
c = span["color"]
actual_rgb = (c >> 16 & 0xFF, c >> 8 & 0xFF, c & 0xFF)
if any(abs(a - e) > 10 for a, e in zip(actual_rgb, expected_color)):
return False
return True
return False # text not found
except Exception:
return False
def get_page_images(pdf_path: str, page_num: int) -> list:
"""Get images on a page: [(xref, smask, width, height, bpc, colorspace, ...), ...]"""
doc = pymupdf.open(pdf_path)
images = doc[page_num].get_images()
doc.close()
return images
def verify_image_count(pdf_path: str, page_num: int, expected: int) -> bool:
try:
return len(get_page_images(pdf_path, page_num)) == expected
except Exception:
return False
def verify_total_image_count(pdf_path: str, expected: int) -> bool:
try:
doc = pymupdf.open(pdf_path)
total = sum(len(page.get_images()) for page in doc)
doc.close()
return total == expected
except Exception:
return False
def extract_image(pdf_path: str, page_num: int, image_index: int,
output_path: str) -> bool:
"""Extract an image from PDF and save it."""
try:
doc = pymupdf.open(pdf_path)
images = doc[page_num].get_images()
if image_index >= len(images):
doc.close()
return False
xref = images[image_index][0]
img_data = doc.extract_image(xref)
doc.close()
with open(output_path, "wb") as f:
f.write(img_data["image"])
return True
except Exception:
return False
from PIL import Image
import numpy as np
def verify_image_matches(pdf_path: str, page_num: int, image_index: int,
reference_path: str, threshold: float = 0.9) -> bool:
"""Extract image from PDF and compare with reference using SSIM."""
try:
import tempfile
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
tmp_path = tmp.name
if not extract_image(pdf_path, page_num, image_index, tmp_path):
return False
from skimage.metrics import structural_similarity as ssim
img1 = np.array(Image.open(tmp_path).convert("RGB"))
img2 = np.array(Image.open(reference_path).convert("RGB"))
if img1.shape != img2.shape:
img2_pil = Image.open(reference_path).convert("RGB").resize(
(img1.shape[1], img1.shape[0]), Image.Resampling.LANCZOS)
img2 = np.array(img2_pil)
score = ssim(img1, img2, win_size=7, channel_axis=2)
import os
os.unlink(tmp_path)
return score >= threshold
except Exception:
return False
def get_annotations(pdf_path: str, page_num: int) -> list:
"""Get all annotations on a page with their properties."""
doc = pymupdf.open(pdf_path)
page = doc[page_num]
annots = []
for annot in page.annots():
annots.append({
"type": annot.type[1], # "Highlight", "Text", "FreeText", "Square", etc.
"type_code": annot.type[0], # integer type code
"content": annot.info.get("content", ""),
"title": annot.info.get("title", ""),
"rect": tuple(annot.rect), # (x0, y0, x1, y1)
"colors": {
"stroke": annot.colors.get("stroke"),
"fill": annot.colors.get("fill"),
},
"opacity": annot.opacity,
})
doc.close()
return annots
def verify_annotation_count(pdf_path: str, page_num: int,
expected: int, annot_type: str = None) -> bool:
"""Verify number of annotations, optionally filtered by type."""
try:
annots = get_annotations(pdf_path, page_num)
if annot_type:
annots = [a for a in annots if a["type"] == annot_type]
return len(annots) == expected
except Exception:
return False
def verify_highlight_exists(pdf_path: str, page_num: int,
text: str, color: tuple = None) -> bool:
"""Verify a highlight annotation exists over specific text.
color: (R, G, B) as floats 0-1, e.g., (1, 1, 0) for yellow."""
try:
doc = pymupdf.open(pdf_path)
page = doc[page_num]
# Find text location
text_instances = page.search_for(text)
if not text_instances:
doc.close()
return False
# Check annotations
for annot in page.annots():
if annot.type[1] != "Highlight":
continue
annot_rect = annot.rect
# Check if annotation overlaps with text
for inst in text_instances:
if annot_rect.intersects(inst):
if color is None:
doc.close()
return True
# Check color
stroke = annot.colors.get("stroke")
if stroke and all(abs(a - e) < 0.05 for a, e in zip(stroke, color)):
doc.close()
return True
doc.close()
return False
except Exception:
return False
def verify_stamp_exists(pdf_path: str, page_num: int) -> bool:
"""Check if a stamp annotation exists on the page."""
try:
annots = get_annotations(pdf_path, page_num)
return any(a["type"] == "Stamp" for a in annots)
except Exception:
return False
def verify_text_annotation(pdf_path: str, page_num: int,
expected_content: str) -> bool:
"""Verify a sticky note with specific content exists."""
try:
annots = get_annotations(pdf_path, page_num)
return any(a["type"] == "Text" and expected_content in a["content"]
for a in annots)
except Exception:
return False
def get_links(pdf_path: str, page_num: int) -> list:
"""Get all links on a page."""
doc = pymupdf.open(pdf_path)
links = doc[page_num].get_links()
doc.close()
return links
def verify_link_exists(pdf_path: str, page_num: int,
expected_uri: str = None,
expected_page: int = None) -> bool:
"""Verify a link exists. Check URI for external links, page for internal."""
try:
links = get_links(pdf_path, page_num)
for link in links:
if expected_uri and link.get("uri", "") == expected_uri:
return True
if expected_page is not None and link.get("page", -1) == expected_page:
return True
return False
except Exception:
return False
def verify_link_count(pdf_path: str, page_num: int, expected: int) -> bool:
try:
return len(get_links(pdf_path, page_num)) == expected
except Exception:
return False
def get_form_fields(pdf_path: str, page_num: int = None) -> list:
"""Get form fields. If page_num is None, get all fields from all pages."""
doc = pymupdf.open(pdf_path)
fields = []
pages = [doc[page_num]] if page_num is not None else doc
for page in pages:
for widget in page.widgets():
fields.append({
"name": widget.field_name,
"type": widget.field_type, # 1=text, 2=checkbox, 3=radio, 4=button, 5=combobox, 6=listbox, 7=signature
"type_name": widget.field_type_string, # "Text", "CheckBox", etc.
"value": widget.field_value,
"choices": widget.choice_values, # for combo/list boxes
"rect": tuple(widget.rect),
"flags": widget.field_flags,
})
doc.close()
return fields
def verify_form_field_value(pdf_path: str, field_name: str,
expected_value: str) -> bool:
"""Verify a form field has a specific value."""
try:
fields = get_form_fields(pdf_path)
for f in fields:
if f["name"] == field_name:
return str(f["value"]) == str(expected_value)
return False
except Exception:
return False
def verify_form_field_count(pdf_path: str, expected: int,
field_type: str = None) -> bool:
"""Verify total number of form fields, optionally by type."""
try:
fields = get_form_fields(pdf_path)
if field_type:
fields = [f for f in fields if f["type_name"] == field_type]
return len(fields) == expected
except Exception:
return False
def verify_form_field_exists(pdf_path: str, field_name: str,
field_type: str = None) -> bool:
"""Verify a form field with given name (and optionally type) exists."""
try:
fields = get_form_fields(pdf_path)
for f in fields:
if f["name"] == field_name:
if field_type and f["type_name"] != field_type:
return False
return True
return False
except Exception:
return False
def verify_checkbox_state(pdf_path: str, field_name: str,
expected_checked: bool) -> bool:
"""Verify checkbox is checked or unchecked."""
try:
fields = get_form_fields(pdf_path)
for f in fields:
if f["name"] == field_name and f["type_name"] == "CheckBox":
is_checked = f["value"] not in ("Off", "", None, False)
return is_checked == expected_checked
return False
except Exception:
return False
def get_drawings(pdf_path: str, page_num: int) -> list:
"""Get vector drawings on a page. Returns list of path dicts."""
doc = pymupdf.open(pdf_path)
drawings = doc[page_num].get_drawings()
doc.close()
return drawings
def verify_has_drawings(pdf_path: str, page_num: int, min_count: int = 1) -> bool:
"""Verify page contains at least min_count vector drawings."""
try:
return len(get_drawings(pdf_path, page_num)) >= min_count
except Exception:
return False
def verify_drawing_color(pdf_path: str, page_num: int,
expected_color: tuple, check_fill: bool = True) -> bool:
"""Verify any drawing has the expected color. Color as (R, G, B) 0-1."""
try:
drawings = get_drawings(pdf_path, page_num)
for d in drawings:
if check_fill and d.get("fill"):
if all(abs(a - e) < 0.05 for a, e in zip(d["fill"], expected_color)):
return True
if d.get("color"):
if all(abs(a - e) < 0.05 for a, e in zip(d["color"], expected_color)):
return True
return False
except Exception:
return False
def render_page_to_image(pdf_path: str, page_num: int, dpi: int = 150) -> "Image":
"""Render a PDF page to PIL Image."""
doc = pymupdf.open(pdf_path)
page = doc[page_num]
mat = pymupdf.Matrix(dpi / 72, dpi / 72)
pix = page.get_pixmap(matrix=mat)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
doc.close()
return img
def compare_pages_visual(pdf1_path: str, pdf2_path: str,
page_num: int, threshold: float = 0.95,
dpi: int = 150) -> bool:
"""Visually compare a page between two PDFs using SSIM."""
try:
from skimage.metrics import structural_similarity as ssim
img1 = np.array(render_page_to_image(pdf1_path, page_num, dpi))
img2 = np.array(render_page_to_image(pdf2_path, page_num, dpi))
# Resize if different
if img1.shape != img2.shape:
h = min(img1.shape[0], img2.shape[0])
w = min(img1.shape[1], img2.shape[1])
img1 = np.array(Image.fromarray(img1).resize((w, h), Image.Resampling.LANCZOS))
img2 = np.array(Image.fromarray(img2).resize((w, h), Image.Resampling.LANCZOS))
score = ssim(img1, img2, win_size=7, channel_axis=2)
return score >= threshold
except Exception:
return False
def compare_all_pages_visual(pdf1_path: str, pdf2_path: str,
threshold: float = 0.95) -> float:
"""Compare all pages between two PDFs. Returns average SSIM score."""
try:
doc1 = pymupdf.open(pdf1_path)
doc2 = pymupdf.open(pdf2_path)
if doc1.page_count != doc2.page_count:
doc1.close()
doc2.close()
return 0.0
from skimage.metrics import structural_similarity as ssim
scores = []
for i in range(doc1.page_count):
img1 = np.array(render_page_to_image(pdf1_path, i))
img2 = np.array(render_page_to_image(pdf2_path, i))
if img1.shape != img2.shape:
h = min(img1.shape[0], img2.shape[0])
w = min(img1.shape[1], img2.shape[1])
img1 = np.array(Image.fromarray(img1).resize((w, h), Image.Resampling.LANCZOS))
img2 = np.array(Image.fromarray(img2).resize((w, h), Image.Resampling.LANCZOS))
scores.append(ssim(img1, img2, win_size=7, channel_axis=2))
doc1.close()
doc2.close()
return sum(scores) / len(scores) if scores else 0.0
except Exception:
return 0.0
def compute_reward(pdf_path: str, golden_path: str, checks: list) -> float:
"""Compute a 0.0-1.0 reward score from weighted checks.
checks: [{"name": str, "func": callable, "weight": float}, ...]
Each func returns bool or float (0.0-1.0)."""
total_weight = sum(c["weight"] for c in checks)
if total_weight == 0:
return 0.0
score = 0.0
for check in checks:
try:
result = check["func"]()
if isinstance(result, bool):
result = 1.0 if result else 0.0
score += result * check["weight"]
except Exception:
pass # check failed, contributes 0
return round(score / total_weight, 2)
# Example usage:
# reward = compute_reward(result_path, golden_path, [
# {"name": "page_count", "func": lambda: verify_page_count(result_path, 5), "weight": 0.2},
# {"name": "text_present", "func": lambda: verify_text_contains(result_path, ["Chapter 1"]), "weight": 0.3},
# {"name": "visual_match", "func": lambda: compare_all_pages_visual(result_path, golden_path), "weight": 0.5},
# ])
# print(f"REWARD: {reward}")
def extract_tables(pdf_path: str, page_num: int) -> list:
"""Extract tables from a page using PyMuPDF's table finder.
Returns list of tables, each table is a list of rows, each row is a list of cell strings."""
doc = pymupdf.open(pdf_path)
page = doc[page_num]
tables = page.find_tables()
result = []
for table in tables:
rows = []
for row in table.extract():
rows.append([cell if cell else "" for cell in row])
result.append(rows)
doc.close()
return result
def verify_table_content(pdf_path: str, page_num: int,
table_index: int, expected_data: list) -> bool:
"""Verify table content matches expected data.
expected_data: [[row1_cells], [row2_cells], ...]"""
try:
tables = extract_tables(pdf_path, page_num)
if table_index >= len(tables):
return False
actual = tables[table_index]
if len(actual) != len(expected_data):
return False
for actual_row, expected_row in zip(actual, expected_data):
if len(actual_row) != len(expected_row):
return False
for actual_cell, expected_cell in zip(actual_row, expected_row):
if actual_cell.strip() != str(expected_cell).strip():
return False
return True
except Exception:
return False
def verify_table_exists(pdf_path: str, page_num: int,
min_rows: int = 1, min_cols: int = 1) -> bool:
"""Verify at least one table exists with minimum dimensions."""
try:
tables = extract_tables(pdf_path, page_num)
for table in tables:
if len(table) >= min_rows and all(len(row) >= min_cols for row in table):
return True
return False
except Exception:
return False
PyMuPDF uses import pymupdf, not import fitz (since v1.24). Older code uses import fitz. Both work, but pymupdf is the canonical import for newer versions. Always try import pymupdf first, fall back to import fitz.
Shape.commit() is mandatory. After calling draw_rect(), draw_circle(), etc., you must call shape.finish() (to apply colors/stroke) and then shape.commit() (to write to page). Forgetting commit() means shapes never appear in the saved PDF.
Colors are float tuples (0-1), not int tuples (0-255). PyMuPDF uses (1, 0, 0) for red, NOT (255, 0, 0). (255, 0, 0) will silently clamp or produce unexpected results.
Font span color is an integer, not a tuple. get_text("dict") returns span["color"] as an integer. Convert with (color >> 16 & 0xFF, color >> 8 & 0xFF, color & 0xFF) to get (R, G, B) as 0-255.
page.search_for() returns Rect objects, not strings. The return value is a list of Rect that mark where the text is found. Use these Rects for highlighting or position verification.
PDF coordinates: origin is bottom-left in spec, but PyMuPDF uses top-left. PyMuPDF transforms coordinates so (0,0) is top-left. This is consistent within PyMuPDF but differs from raw PDF specs and reportlab (which uses bottom-left origin).
reportlab Canvas origin is bottom-left. drawString(72, 72) places text 1 inch from the bottom, NOT the top. To place text at the top, use drawString(72, height - 72). This is the opposite of PyMuPDF.
Annotation .update() is required after changes. After set_colors(), set_border(), set_opacity() etc., you must call annot.update(). Without it, changes are not rendered in the PDF appearance stream.
pikepdf vs PyMuPDF for encryption. PyMuPDF can detect encryption and authenticate, but pikepdf is better for setting encryption because it provides fine-grained Permissions control. Use pikepdf for encrypt/decrypt operations.
get_text("text") loses formatting and structure. It returns plain text with newlines. For structured extraction with fonts, sizes, and colors, use get_text("dict") which returns block/line/span hierarchy.
Table extraction requires PyMuPDF >= 1.23. page.find_tables() was added in version 1.23. On older versions, you must parse text blocks and manually reconstruct table structure.
Copy-then-modify for golden PDFs. Never create golden files from scratch. Always shutil.copy(initial, golden) then modify. PyMuPDF preserves internal PDF structure, cross-references, and resources that differ from scratch-created files.
doc.save() overwrites in-place only with incremental=True. By default, doc.save("same_file.pdf") may fail if the file is still open. Use doc.save("same_file.pdf", incremental=True, encryption=pymupdf.PDF_ENCRYPT_KEEP) or save to a temp file first.
Page rotation affects coordinate system. After set_rotation(90), the page's rect dimensions swap (width ↔ height). Text insertion coordinates must account for the new orientation. Use page.derotation_matrix if you need original coordinates.
Widget (form field) font support is limited. PyMuPDF widgets support only the 14 PDF base fonts (Helvetica, Times, Courier variants, Symbol, ZapfDingbats). Custom fonts in widgets require low-level PDF manipulation.
SSIM window size must be odd and ≤ image dimension. When comparing rendered pages, small pages at low DPI can produce images smaller than the default win_size=7. Adapt: win_size = min(7, min_dim if min_dim % 2 == 1 else min_dim - 1).
Evince does NOT support command-line zoom control. You can specify page (--page-index) and mode (--fullscreen, --presentation) but NOT zoom level. Zoom must be set interactively by the agent.
PDF text extraction may differ from visual text. Ligatures, kerning, and character encoding can cause extracted text to differ from what's displayed. Use fuzzy matching or normalize Unicode when comparing text.
doc.insert_pdf() reindexes pages. After inserting pages from another PDF, internal references (TOC, links) may break. Always rebuild TOC and links after merge operations.
pikepdf page rotation uses raw PDF values. page.Rotate = 90 sets rotation directly. Unlike PyMuPDF, pikepdf does NOT transform coordinates — the visual result depends on how the viewer interprets the rotation flag.