Home » General Python Tutorials » How to Create a PDF Signature App in Python

How to Create a PDF Signature App in Python

In today’s digital world, signing documents electronically has become more than just a convenience; it’s almost a necessity. Whether you’re dealing with contracts, forms, or agreements, adding a digital signature can streamline workflows and reduce the hassle of printing, signing, and scanning. But wouldn’t it be even better if you could create a simple app to automate this?

That’s what we’ll explore today! In this tutorial, you’ll learn how to build a PDF signature app using Python. We’ll use Tkinter to create a user-friendly interface, and PyMuPDF to handle the PDF processing behind the scenes. By the end, you’ll have a fully functional app that lets you sign PDFs with ease, including a drag-and-drop feature for quick access. Ready to get started? Let’s dive in!

Table of Contents

Getting Started

Let’s kick things off by setting up everything we need. First, we’ll install the necessary libraries. Just open up your terminal and type in these commands:

$ pip install pypdf
$ pip install tk 
$ pip install tkinterdnd2
$ pip install pillow
$ pip install reportlab

Alright, we’ll begin by bringing in the libraries we’ll need to make our PDF signature app come to life:

  • Fitz: This one’s our PDF powerhouse! It lets us open, edit, and save PDFs, covering all the essentials we need to get our signatures in place.
  • Tkinter: Our toolkit for building the app’s interface. Tkinter will help us with selecting and saving files, plus it’ll show messages to guide users along the way.
  • Tkinterdnd2: With this, we can add a cool drag-and-drop feature, so users can drop their files right where they need them—no extra clicks required!
  • PIL: Short for Python Imaging Library, it’ll help us preview the signature image that we’re adding to our PDFs.
import fitz  # PyMuPDF
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinterdnd2 import TkinterDnD, DND_FILES
from PIL import Image, ImageTk

With our toolkit ready, it’s time to jump in!

Placing the Signature on the PDF

This is where the magic happens! Here’s how we add the signature to the PDF file:

We use the add_signature() function, which starts by opening the selected PDF with fitz.open(input_pdf). Once the PDF is opened, it loops through all the pages. If no specific pages are provided (i.e., the pages tuple is None), it signs all pages; otherwise, it only signs the specified ones.

pdf = fitz.open(input_pdf)
for page_num in range(pdf.page_count):
    if pages and (page_num + 1) not in pages:
        continue

But it doesn’t place the signature randomly—nope! It uses fitz.Rect() to get the exact coordinates input by the user, defining the precise area where the signature will go.

rect = fitz.Rect(x, y, x + width, y + height)

With the pages and signature location set, the function then inserts the signature image with page.insert_image().

page.insert_image(rect, filename=image_path)

To wrap things up, the updated PDF with the signature is saved as a new file using pdf.save(output_pdf).

pdf.save(output_pdf)

Selecting Files for the PDF Signature Process

Browsing the PDF File

Now that we know how to add a signature, the next step is selecting the PDF file we want to sign. To do this, we use the browse_pdf() function. This function opens a file dialog where the user can select a PDF file. Once the file is selected, it updates the entry box with the path to the selected PDF. Here’s how it works:

def browse_pdf():
   file_path = filedialog.askopenfilename(filetypes=[("PDF Files", "*.pdf")])
   if file_path:
       pdf_path_entry.delete(0, tk.END)
       pdf_path_entry.insert(0, file_path)

Browsing the Image File

Next, we move on to selecting the signature image. This is done through the browse_image() function, which opens a file dialog where the user can pick an image file (either PNG or JPG). After the image is selected, the function updates the entry box with the image’s file path, and also calls update_preview() to display a preview of the signature. Here’s the code for it:

def browse_image():
   file_path = filedialog.askopenfilename(filetypes=[("Image Files", "*.png;*.jpg")])
   if file_path:
       img_path_entry.delete(0, tk.END)
       img_path_entry.insert(0, file_path)
       update_preview(file_path)

Browsing the Output File

Lastly, we need to specify where to save the signed PDF. This is handled by the browse_output() function, which opens a save-as dialog for the user to choose the location and file name for the output PDF. Once selected, the entry box is updated with the chosen output path. Here’s the function:

def browse_output():
   file_path = filedialog.asksaveasfilename(defaultextension=".pdf", filetypes=[("PDF Files", "*.pdf")])
   if file_path:
       output_path_entry.delete(0, tk.END)
       output_path_entry.insert(0, file_path)

Setting Up Signature Position and Size

At this stage, we specify the exact coordinates for placing the signature. The sign_pdf() function gathers all user inputs and checks whether to sign all pages or only specific ones. Once configured, it calls add_signature() to place the signature image at the specified location. If successful, a pop-up message confirms the action; if not, an error message will appear.

def sign_pdf():
   input_pdf = pdf_path_entry.get()
   image_path = img_path_entry.get()
   output_pdf = output_path_entry.get() or (input_pdf.replace(".pdf", "_signed.pdf"))
   try:
       x = int(x_coord_entry.get())
       y = int(y_coord_entry.get())
       width = int(width_entry.get())
       height = int(height_entry.get())


       # Determine pages based on the selected option
       if page_option.get() == "all":
           pages = None  # Sign all pages
       elif page_option.get() == "range":
           start, end = map(int, page_range_entry.get().split("-"))
           pages = tuple(range(start, end + 1))
       elif page_option.get() == "specific":
           pages = tuple(map(int, specific_pages_entry.get().split(",")))
       else:
           pages = None


       add_signature(input_pdf, output_pdf, image_path, x, y, width, height, pages)
       messagebox.showinfo("Success", f"Signed PDF saved as {output_pdf}")
   except Exception as e:
       messagebox.showerror("Error", f"Failed to sign PDF: {e}")

Setup and Preview with Drag-and-Drop

Generating the Preview

If you want a sneak peek of the signature, the update_preview() function has you covered. It loads and resizes the signature image to fit in the preview frame and displays it through preview_label, giving you a quick glimpse. If anything goes wrong, messagebox will provide feedback to let you know.

def update_preview(image_path):
   try:
       img = Image.open(image_path)
       img.thumbnail((200, 200))  # Adjust preview size
       img_preview = ImageTk.PhotoImage(img)
       preview_label.config(image=img_preview)
       preview_label.image = img_preview
   except Exception as e:
       messagebox.showerror("Error", f"Failed to load preview: {e}")

Drag and Drop

To make things even more convenient, we’ve added a drag-and-drop feature with drag_and_drop(). This function allows you to drag your selected PDF file or signature image straight into the entry boxes.

def drag_and_drop(event):
   file_path = event.data.strip('{}')  # Handle spaces in paths
   if file_path.endswith(".pdf"):
       pdf_path_entry.delete(0, tk.END)
       pdf_path_entry.insert(0, file_path)
   elif file_path.endswith((".png", ".jpg")):
       img_path_entry.delete(0, tk.END)
       img_path_entry.insert(0, file_path)
       update_preview(file_path)

Building the Interface

We’ve reached the final step—creating the main window and filling it with widgets and buttons! First, we create the main window, set its title, and define its geometry. To make things more interesting, we enable drag-and-drop functionality using TkinterDnD.

# Initialize the TkinterDnD window
root = TkinterDnD.Tk()
root.title("PDF Signature Tool - The Pycodes")
root.geometry("500x700")

Next, we add an entry box for the PDF file, label it, and set up a “Browse PDF” button that triggers the browse_pdf() function, letting the user select the PDF file. We do the same for the signature image: create an entry box, label it, and add a button to browse for the image.

# PDF File Selection
tk.Label(root, text="Select PDF File").pack()
pdf_path_entry = tk.Entry(root, width=60)
pdf_path_entry.pack(pady=5)
tk.Button(root, text="Browse PDF", command=browse_pdf).pack()


# Image Selection and Preview
tk.Label(root, text="Select Signature Image").pack()
img_path_entry = tk.Entry(root, width=60)
img_path_entry.pack(pady=5)
tk.Button(root, text="Browse Image", command=browse_image).pack()

Once the image is selected, we set up the preview_label, which will display the signature image. Then, we add four labeled entry boxes to let the user input the exact coordinates where the signature will be placed.

preview_label = tk.Label(root, text="Signature Preview will appear here", relief="sunken", width=20, height=10)
preview_label.pack(pady=10)


# Coordinates and Size
coord_frame = tk.Frame(root)
coord_frame.pack(pady=5)

tk.Label(coord_frame, text="X Coordinate").grid(row=0, column=0)
x_coord_entry = tk.Entry(coord_frame, width=10)
x_coord_entry.grid(row=0, column=1, padx=5)

tk.Label(coord_frame, text="Y Coordinate").grid(row=0, column=2)
y_coord_entry = tk.Entry(coord_frame, width=10)
y_coord_entry.grid(row=0, column=3, padx=5)

tk.Label(coord_frame, text="Width").grid(row=1, column=0)
width_entry = tk.Entry(coord_frame, width=10)
width_entry.grid(row=1, column=1, padx=5)

tk.Label(coord_frame, text="Height").grid(row=1, column=2)
height_entry = tk.Entry(coord_frame, width=10)
height_entry.grid(row=1, column=3, padx=5)

For page selection, we add radio buttons that allow the user to choose which pages to sign. We also include page_range_entry and specific_pages_entry for more precise input when selecting a page range or specific pages.

# Page Selection Options
tk.Label(root, text="Select Pages to Sign").pack(pady=5)
page_option = tk.StringVar(value="all")

all_pages_rb = tk.Radiobutton(root, text="All Pages", variable=page_option, value="all")
all_pages_rb.pack()

range_pages_rb = tk.Radiobutton(root, text="Range (e.g., 1-5)", variable=page_option, value="range")
range_pages_rb.pack()

page_range_entry = tk.Entry(root, width=20, state="normal")
page_range_entry.pack(pady=5)

specific_pages_rb = tk.Radiobutton(root, text="Specific Pages (e.g., 1,3,5)", variable=page_option, value="specific")
specific_pages_rb.pack()

specific_pages_entry = tk.Entry(root, width=20, state="normal")
specific_pages_entry.pack(pady=5)

Moving on, we create an entry box for the output path, label it, and add a button that triggers the browse_output() function, enabling the user to select where to save the signed PDF file. If you’re feeling adventurous, you can leave this empty, and the app will save the file for you automatically.

# Output Path Selection
tk.Label(root, text="Output PDF Path").pack()
output_path_entry = tk.Entry(root, width=60)
output_path_entry.pack(pady=5)
tk.Button(root, text="Browse Output Path", command=browse_output).pack()

Finally, the “Sign PDF” button takes center stage! It triggers the sign_pdf() function and starts the whole process. Before we finish, we enable the drag-and-drop feature with the drag_and_drop() function, and then we kick off the main event loop with mainloop() to keep the window responsive and interactive.

# Sign Button
sign_button = tk.Button(root, text="Sign PDF", command=sign_pdf, bg="blue", fg="white")
sign_button.pack(pady=20)


# Drag-and-Drop bindings
root.drop_target_register(DND_FILES)
root.dnd_bind('<<Drop>>', drag_and_drop)


# Run the Tkinter event loop
root.mainloop()

Example

Full Code

import fitz  # PyMuPDF
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinterdnd2 import TkinterDnD, DND_FILES
from PIL import Image, ImageTk




def add_signature(input_pdf: str, output_pdf: str, image_path: str,
                 x: int, y: int, width: int, height: int, pages: tuple = None):
   pdf = fitz.open(input_pdf)
   for page_num in range(pdf.page_count):
       if pages and (page_num + 1) not in pages:
           continue
       page = pdf[page_num]
       rect = fitz.Rect(x, y, x + width, y + height)
       page.insert_image(rect, filename=image_path)
   pdf.save(output_pdf)
   pdf.close()




def browse_pdf():
   file_path = filedialog.askopenfilename(filetypes=[("PDF Files", "*.pdf")])
   if file_path:
       pdf_path_entry.delete(0, tk.END)
       pdf_path_entry.insert(0, file_path)




def browse_image():
   file_path = filedialog.askopenfilename(filetypes=[("Image Files", "*.png;*.jpg")])
   if file_path:
       img_path_entry.delete(0, tk.END)
       img_path_entry.insert(0, file_path)
       update_preview(file_path)




def browse_output():
   file_path = filedialog.asksaveasfilename(defaultextension=".pdf", filetypes=[("PDF Files", "*.pdf")])
   if file_path:
       output_path_entry.delete(0, tk.END)
       output_path_entry.insert(0, file_path)




def sign_pdf():
   input_pdf = pdf_path_entry.get()
   image_path = img_path_entry.get()
   output_pdf = output_path_entry.get() or (input_pdf.replace(".pdf", "_signed.pdf"))
   try:
       x = int(x_coord_entry.get())
       y = int(y_coord_entry.get())
       width = int(width_entry.get())
       height = int(height_entry.get())


       # Determine pages based on the selected option
       if page_option.get() == "all":
           pages = None  # Sign all pages
       elif page_option.get() == "range":
           start, end = map(int, page_range_entry.get().split("-"))
           pages = tuple(range(start, end + 1))
       elif page_option.get() == "specific":
           pages = tuple(map(int, specific_pages_entry.get().split(",")))
       else:
           pages = None


       add_signature(input_pdf, output_pdf, image_path, x, y, width, height, pages)
       messagebox.showinfo("Success", f"Signed PDF saved as {output_pdf}")
   except Exception as e:
       messagebox.showerror("Error", f"Failed to sign PDF: {e}")




def update_preview(image_path):
   try:
       img = Image.open(image_path)
       img.thumbnail((200, 200))  # Adjust preview size
       img_preview = ImageTk.PhotoImage(img)
       preview_label.config(image=img_preview)
       preview_label.image = img_preview
   except Exception as e:
       messagebox.showerror("Error", f"Failed to load preview: {e}")




def drag_and_drop(event):
   file_path = event.data.strip('{}')  # Handle spaces in paths
   if file_path.endswith(".pdf"):
       pdf_path_entry.delete(0, tk.END)
       pdf_path_entry.insert(0, file_path)
   elif file_path.endswith((".png", ".jpg")):
       img_path_entry.delete(0, tk.END)
       img_path_entry.insert(0, file_path)
       update_preview(file_path)




# Initialize the TkinterDnD window
root = TkinterDnD.Tk()
root.title("PDF Signature Tool - The Pycodes")
root.geometry("500x700")


# Instructions Label
tk.Label(root, text="Drag and Drop PDF/Image or use Browse Buttons", font=("Arial", 12, "bold")).pack(pady=10)


# PDF File Selection
tk.Label(root, text="Select PDF File").pack()
pdf_path_entry = tk.Entry(root, width=60)
pdf_path_entry.pack(pady=5)
tk.Button(root, text="Browse PDF", command=browse_pdf).pack()


# Image Selection and Preview
tk.Label(root, text="Select Signature Image").pack()
img_path_entry = tk.Entry(root, width=60)
img_path_entry.pack(pady=5)
tk.Button(root, text="Browse Image", command=browse_image).pack()


preview_label = tk.Label(root, text="Signature Preview will appear here", relief="sunken", width=20, height=10)
preview_label.pack(pady=10)


# Coordinates and Size
coord_frame = tk.Frame(root)
coord_frame.pack(pady=5)


tk.Label(coord_frame, text="X Coordinate").grid(row=0, column=0)
x_coord_entry = tk.Entry(coord_frame, width=10)
x_coord_entry.grid(row=0, column=1, padx=5)


tk.Label(coord_frame, text="Y Coordinate").grid(row=0, column=2)
y_coord_entry = tk.Entry(coord_frame, width=10)
y_coord_entry.grid(row=0, column=3, padx=5)


tk.Label(coord_frame, text="Width").grid(row=1, column=0)
width_entry = tk.Entry(coord_frame, width=10)
width_entry.grid(row=1, column=1, padx=5)


tk.Label(coord_frame, text="Height").grid(row=1, column=2)
height_entry = tk.Entry(coord_frame, width=10)
height_entry.grid(row=1, column=3, padx=5)


# Page Selection Options
tk.Label(root, text="Select Pages to Sign").pack(pady=5)
page_option = tk.StringVar(value="all")


all_pages_rb = tk.Radiobutton(root, text="All Pages", variable=page_option, value="all")
all_pages_rb.pack()


range_pages_rb = tk.Radiobutton(root, text="Range (e.g., 1-5)", variable=page_option, value="range")
range_pages_rb.pack()


page_range_entry = tk.Entry(root, width=20, state="normal")
page_range_entry.pack(pady=5)


specific_pages_rb = tk.Radiobutton(root, text="Specific Pages (e.g., 1,3,5)", variable=page_option, value="specific")
specific_pages_rb.pack()


specific_pages_entry = tk.Entry(root, width=20, state="normal")
specific_pages_entry.pack(pady=5)


# Output Path Selection
tk.Label(root, text="Output PDF Path").pack()
output_path_entry = tk.Entry(root, width=60)
output_path_entry.pack(pady=5)
tk.Button(root, text="Browse Output Path", command=browse_output).pack()


# Sign Button
sign_button = tk.Button(root, text="Sign PDF", command=sign_pdf, bg="blue", fg="white")
sign_button.pack(pady=20)


# Drag-and-Drop bindings
root.drop_target_register(DND_FILES)
root.dnd_bind('<<Drop>>', drag_and_drop)


# Run the Tkinter event loop
root.mainloop()

Happy Coding!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top