2025, Nov 20 15:00

How to place a clickable image in the top-left corner of a PDF reliably with PyMuPDF (and why ReportLab/PyPDF2 drift)

Learn why ReportLab/PyPDF2 overlays misalign and how to place a clickable image in the PDF top-left reliably using PyMuPDF or stamping. Code examples inside.

Placing a clickable image exactly in the top-left corner of a PDF sounds simple, yet in practice it often drifts, especially when an overlay is drawn with a fixed canvas size and then merged onto pages that don’t share that geometry. Below is a focused guide that shows where things go astray and a reliable path to make it work.

Problem overview

The goal is to add an image icon to the top-left of the first page in a PDF and make it link to a URL. A straightforward attempt uses ReportLab to draw an overlay and PyPDF2 to merge it onto the original.

Problematic example

This snippet illustrates the approach where the overlay canvas is created with a fixed letter size and merged onto the first page. Despite computing the position for the top-left corner with padding, the image ends up in the middle area.

from PyPDF2 import PdfReader, PdfWriter
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
import io
# Inputs
src_pdf = "original.pdf"
badge_path = "icon.jpeg"
out_path = "output.pdf"
# Build an overlay canvas
buf = io.BytesIO()
painter = canvas.Canvas(buf, pagesize=letter)
# Intended top-left placement with padding
pos_x, pos_y = 40, 792 - 40 - inch
painter.drawImage(badge_path, pos_x, pos_y, width=inch, height=inch)
painter.linkURL("https://t.me/newsmalayalampdf", (pos_x, pos_y, pos_x + inch, pos_y + inch), relative=0)
painter.save()
buf.seek(0)
# Merge overlay with the original PDF
mask_pdf = PdfReader(buf)
reader_pdf = PdfReader(src_pdf)
writer_pdf = PdfWriter()
page0 = reader_pdf.pages[0]
page0.merge_page(mask_pdf.pages[0])
writer_pdf.add_page(page0)
for pg in reader_pdf.pages[1:]:
    writer_pdf.add_page(pg)
with open(out_path, "wb") as handle:
    writer_pdf.write(handle)

What’s going on

When the overlay is drawn onto a fixed letter-sized canvas and then merged, placement becomes fragile if the target page isn’t using that same geometry. A PDF page can report multiple “sizes”, and hardcoding values works inconsistently across files. Recalculating the Y coordinate against a fixed constant doesn’t address the mismatch. On top of that, it’s worth noting that images in ReportLab rely on Pillow, and PyPDF2 itself is no longer maintained, with the project having moved to pypdf.

A clean, reliable approach with stamping or PyMuPDF

There are two practical approaches that avoid the overlay pitfall. One is to use a single shell command to stamp a prepared PDF containing your image and link onto every page. Another is to script the placement using PyMuPDF and define the clickable rectangle directly on each page.

If you prefer a one-liner from the shell, stamping can be done like this:

cpdf -stamp-on logouri.pdf -pos-left "40 580" in.pdf -o output.pdf

While this works well as a no-libs technique, it introduces handling aspects that can be less convenient for others to reproduce. A similarly simple programmatic route uses PyMuPDF and keeps the call site small and readable.

Solution: place and link with PyMuPDF

The script below takes an input PDF, draws the image at a defined offset, and attaches a URI link to the same rectangle. The result can be applied to all pages or just the first page.

import pymupdf
import sys
import os
if len(sys.argv) < 2:
    print("Usage: python logouri.py input.pdf")
    sys.exit(1)
in_file = sys.argv[1]
out_file = os.path.splitext(in_file)[0] + "-withlogo.pdf"
img_file = "logo.jpeg"
jump_url = "https://t.me/newsmalayalampdf"
# Offsets and display size (points)
x_pad = 36
y_pad = 36
square = 72
doc = pymupdf.open(in_file)
for pg in doc:
    box = pymupdf.Rect(x_pad, y_pad, x_pad + square, y_pad + square)
    pg.insert_image(box, filename=img_file)
    pg.insert_link({"kind": pymupdf.LINK_URI, "from": box, "uri": jump_url})
doc.save(out_file)

If you only need the first page, replace the per-page loop with a direct reference to page zero.

doc = pymupdf.open(in_file)
pg = doc[0]
box = pymupdf.Rect(x_pad, y_pad, x_pad + square, y_pad + square)
pg.insert_image(box, filename=img_file)
pg.insert_link({"kind": pymupdf.LINK_URI, "from": box, "uri": jump_url})
doc.save(out_file)

Why this matters

Programmatically composing PDFs is sensitive to page geometry. Assuming a single letter-sized canvas and then merging that onto arbitrary documents is brittle and often misaligns overlays. Using stamping tools or APIs that operate directly on each page’s coordinate space leads to predictable placement and simpler code. It is also helpful to remember ancillary details such as the image pipeline dependency in ReportLab and the current status of libraries you rely on, for example the move from PyPDF2 to pypdf.

Takeaways

If an overlay drawn on a fixed canvas lands in the wrong spot, step back from hardcoded dimensions. Either stamp a prepared asset with a single command or place the image and link directly on the page with PyMuPDF using a rectangle. When accuracy matters, read the page’s actual geometry and avoid assumptions about size. This keeps the output consistent and the integration maintainable.