In today’s world, PDFs are everywhere—whether it’s for official documents, study materials, or just sharing information, they’ve become an essential part of digital life. But managing multiple PDF files can get chaotic. That’s where a PDF Merger app comes in handy. Imagine having a simple tool that lets you select, reorder, and merge your PDFs effortlessly, all through an intuitive interface. Sounds useful, right?
Today, you’ll learn how to create a PDF Merger app with Python, using Tkinter for a clean graphical interface and PyPDF2 to handle PDF processing. We’ll go through each step, from file selection to drag-and-drop functionality and even merging the files into one seamless document. By the end, you’ll have a handy tool that you can customize to make your PDF organization easier and more efficient!
Table of Contents
- Getting Started
- Initializing the Main Window and PDF List
- Selecting, Adding, and Handling Drag-and-Drop PDFs
- Managing PDF File Order: Moving, Removing, and Clearing Selections
- Updating Listbox Selection
- Merging the PDF Files
- Setting Up the GUI Elements
- Example
- Full Code
Getting Started
To get started, make sure you have the necessary libraries installed. Open your terminal and run these commands:
$ pip install tk
$ pip install tkinterdnd2
$ pip install PyPDF2
Let’s begin by bringing in the libraries that will shape our code:
- tkinter: This library forms the foundation of our graphical interface. With it, we can create file selection dialogs through
filedialog
, display feedback messages withmessagebox
, and use nicely styled widgets viattk
. - To add a cool touch, we want users to be able to drag and drop PDF files into the app. That’s where TkinterDnD comes in handy.
- Finally, the star of our script: PyPDF2, which makes reading and merging PDFs possible.
With these tools, we’re all set to build an efficient and user-friendly PDF Merger app!
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from tkinterdnd2 import TkinterDnD, DND_FILES
import PyPDF2
Initializing the Main Window and PDF List
Since we have our tools ready, let’s set the stage. By that, I mean creating the main window using TkinterDnD.Tk()
to accept dragged files. Next, we’ll set its title and define its geometry to ensure it can accommodate all the GUI elements.
# Initialize main window
root = TkinterDnD.Tk()
root.title("PDF Merger - The Pycodes")
root.geometry("500x500")
With that, we have an actual interface. All that’s left are the elements we need to communicate with the code. But first, let’s focus on handling the PDFs. As we’re going to be merging them, we need a way to store the selected PDFs temporarily. The solution is to create an empty list called pdf_list
. Whenever a user selects a PDF, it will be added to this list, and when it’s time to merge, the app will combine the PDFs in the exact order they appear.
pdf_list = []
Selecting, Adding, and Handling Drag-and-Drop PDFs
Selecting PDFs
Now that we have a list to store PDFs, let’s explore how they are selected. The select_pdfs()
function handles this process. Once triggered, it uses filedialog
to allow users to select the PDF files they want to merge. When a file is selected, the add_files()
function takes over to add it to the pdf_list
.
# Select PDFs function
def select_pdfs():
files = filedialog.askopenfilenames(
title="Select PDF files to merge",
filetypes=[("PDF Files", "*.pdf")]
)
add_files(files)
Handling Drag and Drop
At this point, we’ve reached a pretty cool feature that we haven’t explored before: the ability to drag a PDF file from anywhere into the app without using filedialog
. This is handled by the handle_drop()
function, which stores the path of the dragged file. When multiple files are dragged in, root.tk.splitlist()
separates the various paths, and then those files are added to the pdf_list
thanks to the add_files()
function.
# Handle Drag-and-Drop
def handle_drop(event):
files = root.tk.splitlist(event.data)
add_files(files)
Adding Files to Our List
Speaking of add_files()
, it’s time we talked about how it works. Let’s break it down: the function starts by looping through all the file paths and checking if they’re already in the pdf_list
. If a file is not already present, it adds it to the list to avoid duplicates. After that, each newly added file is inserted into the files_listbox
widget for display.
# Add files to list
def add_files(files):
for file in files:
if file not in pdf_list:
pdf_list.append(file)
files_listbox.insert(tk.END, file)
Managing PDF File Order: Moving, Removing, and Clearing Selections
Moving Files Up and Down in the List
Our goal here is to give users control over the merge order. It would be a hassle to start over just because the order was messed up. We achieve this through the move_up()
function, which retrieves the index of the selected file using files_listbox.curselection()
. It checks if the selected file is at the top; if it isn’t, the function swaps it with the one just above it in pdf_list
. Finally, it updates the Listbox using update_listbox_selection()
to display the new order.
# Move selected file up
def move_up():
selected_index = files_listbox.curselection()
if not selected_index:
return
index = selected_index[0]
if index > 0:
pdf_list[index], pdf_list[index - 1] = pdf_list[index - 1], pdf_list[index]
update_listbox_selection(index - 1)
The move_down()
function operates with the same logic, but it checks if the selected file is at the bottom. If it isn’t, it swaps places with the one below it.
# Move selected file down
def move_down():
selected_index = files_listbox.curselection()
if not selected_index:
return
index = selected_index[0]
if index < len(pdf_list) - 1:
pdf_list[index], pdf_list[index + 1] = pdf_list[index + 1], pdf_list[index]
update_listbox_selection(index + 1)
Removing the Selected File or Clearing All Files
If you selected the wrong PDF file, you don’t have to start over; you can simply remove that file using the remove_selected()
function. This function removes the selected file from pdf_list
with pdf_list.pop()
and deletes it from the Listbox with files_listbox.delete()
.
# Remove selected file
def remove_selected():
selected_index = files_listbox.curselection()
if not selected_index:
return
index = selected_index[0]
pdf_list.pop(index)
files_listbox.delete(index)
Additionally, you can delete all the files at once without having to remove them one by one, thanks to the clear_all()
function. This function clears all files from pdf_list
using pdf_list.clear()
and deletes them from the Listbox with files_listbox.delete()
.
# Clear all files
def clear_all():
pdf_list.clear()
files_listbox.delete(0, tk.END)
Updating Listbox Selection
Next, we need to ensure that the Listbox widget is up to date and reflects all the changes immediately. To achieve this, we’ll create the update_listbox_selection()
function, which will redraw the files_listbox
with the files in pdf_list
in their exact order, while also keeping the user’s selection visible.
# Update Listbox Selection
def update_listbox_selection(new_index):
files_listbox.delete(0, tk.END)
for file in pdf_list:
files_listbox.insert(tk.END, file)
files_listbox.select_set(new_index)
Merging the PDF Files
We’ve finally reached the exciting part where the magic happens: the merge_pdfs()
function! This is where we check if we have any PDF files in our pdf_list
. If it turns out that the list is empty, we’ll simply show an error message using messagebox
to let the user know.
Next, we’ll ask the user where they’d like to save the merged PDF file. With PyPDF2.PdfWriter()
, we’ll take each PDF from our list and combine them into one neat file. As we go through each PDF, we’ll keep the progress bar updated to show how things are moving along. Once the merging process is complete and the file is saved, we’ll celebrate with a cheerful message! And of course, if anything goes wrong along the way, we’ll make sure to display an error message to keep the user informed.
# Merge PDFs
def merge_pdfs():
if not pdf_list:
messagebox.showerror("Error", "No PDF files selected!")
return
output_path = filedialog.asksaveasfilename(
title="Save Merged PDF",
defaultextension=".pdf",
filetypes=[("PDF Files", "*.pdf")]
)
if not output_path:
return # User canceled the save dialog
pdf_writer = PyPDF2.PdfWriter()
try:
progress = ttk.Progressbar(root, orient="horizontal", length=400, mode="determinate", maximum=len(pdf_list))
progress.pack(pady=10)
for idx, pdf_file in enumerate(pdf_list):
pdf_reader = PyPDF2.PdfReader(pdf_file)
for page_num in range(len(pdf_reader.pages)):
pdf_writer.add_page(pdf_reader.pages[page_num])
progress["value"] = idx + 1
root.update_idletasks()
with open(output_path, "wb") as output_pdf:
pdf_writer.write(output_pdf)
messagebox.showinfo("Success", f"Merged PDF saved as: {output_path}")
except Exception as e:
messagebox.showerror("Error", f"Failed to merge PDFs: {e}")
finally:
progress.pack_forget() # Hide the progress bar after merging
Setting Up the GUI Elements
Let’s shift our focus back to the visual side and fill in the main window we created earlier. First, we’ll add buttons: the “Select PDFs” button, which triggers the select_pdfs()
function, and the “Merge PDFs” button that calls the merge_pdfs()
function. Next, we’ll set up the files_listbox
widget to display the selected files and make sure to include a vertical scrollbar for easier navigation.
# Set up UI components
ttk.Button(root, text="Select PDFs", command=select_pdfs).pack(pady=10)
ttk.Button(root, text="Merge PDFs", command=merge_pdfs).pack(pady=5)
files_listbox = tk.Listbox(root, selectmode=tk.SINGLE, width=50, height=15)
files_listbox.pack(pady=10)
scrollbar = ttk.Scrollbar(root, orient="vertical", command=files_listbox.yview)
scrollbar.pack(side="right", fill="y")
files_listbox.config(yscrollcommand=scrollbar.set)
We’ll also create additional buttons to give users more control. These include the “Move Up” button, which activates the move_up()
function, the “Move Down” button for the move_down()
function, the “Remove Selected” button that triggers remove_selected()
, and the “Clear All” button that calls the clear_all()
function.
# Reorder and clear buttons
ttk.Button(root, text="Move Up", command=move_up).pack(pady=2)
ttk.Button(root, text="Move Down", command=move_down).pack(pady=2)
ttk.Button(root, text="Remove Selected", command=remove_selected).pack(pady=2)
ttk.Button(root, text="Clear All", command=clear_all).pack(pady=2)
After that, we’ll enable drag-and-drop functionality by using root.drop_target_register()
to allow the main window to accept files. We’ll bind the drop event to our handle_drop()
function with root.dnd_bind()
, making it easy for users to drag and drop their PDFs. Lastly, we’ll start the main event loop to keep the window running and responsive to user interactions.
# Drag and Drop Bindings
root.drop_target_register(DND_FILES)
root.dnd_bind('<<Drop>>', handle_drop)
# Start the application
root.mainloop()
Example
Now, let’s go ahead and merge the two PDF files you see below.
This is the result, as you can see:
Full Code
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from tkinterdnd2 import TkinterDnD, DND_FILES
import PyPDF2
# Initialize main window
root = TkinterDnD.Tk()
root.title("PDF Merger - The Pycodes")
root.geometry("500x500")
pdf_list = []
# Select PDFs function
def select_pdfs():
files = filedialog.askopenfilenames(
title="Select PDF files to merge",
filetypes=[("PDF Files", "*.pdf")]
)
add_files(files)
# Handle Drag-and-Drop
def handle_drop(event):
files = root.tk.splitlist(event.data)
add_files(files)
# Add files to list
def add_files(files):
for file in files:
if file not in pdf_list:
pdf_list.append(file)
files_listbox.insert(tk.END, file)
# Move selected file up
def move_up():
selected_index = files_listbox.curselection()
if not selected_index:
return
index = selected_index[0]
if index > 0:
pdf_list[index], pdf_list[index - 1] = pdf_list[index - 1], pdf_list[index]
update_listbox_selection(index - 1)
# Move selected file down
def move_down():
selected_index = files_listbox.curselection()
if not selected_index:
return
index = selected_index[0]
if index < len(pdf_list) - 1:
pdf_list[index], pdf_list[index + 1] = pdf_list[index + 1], pdf_list[index]
update_listbox_selection(index + 1)
# Remove selected file
def remove_selected():
selected_index = files_listbox.curselection()
if not selected_index:
return
index = selected_index[0]
pdf_list.pop(index)
files_listbox.delete(index)
# Clear all files
def clear_all():
pdf_list.clear()
files_listbox.delete(0, tk.END)
# Update Listbox Selection
def update_listbox_selection(new_index):
files_listbox.delete(0, tk.END)
for file in pdf_list:
files_listbox.insert(tk.END, file)
files_listbox.select_set(new_index)
# Merge PDFs
def merge_pdfs():
if not pdf_list:
messagebox.showerror("Error", "No PDF files selected!")
return
output_path = filedialog.asksaveasfilename(
title="Save Merged PDF",
defaultextension=".pdf",
filetypes=[("PDF Files", "*.pdf")]
)
if not output_path:
return # User canceled the save dialog
pdf_writer = PyPDF2.PdfWriter()
try:
progress = ttk.Progressbar(root, orient="horizontal", length=400, mode="determinate", maximum=len(pdf_list))
progress.pack(pady=10)
for idx, pdf_file in enumerate(pdf_list):
pdf_reader = PyPDF2.PdfReader(pdf_file)
for page_num in range(len(pdf_reader.pages)):
pdf_writer.add_page(pdf_reader.pages[page_num])
progress["value"] = idx + 1
root.update_idletasks()
with open(output_path, "wb") as output_pdf:
pdf_writer.write(output_pdf)
messagebox.showinfo("Success", f"Merged PDF saved as: {output_path}")
except Exception as e:
messagebox.showerror("Error", f"Failed to merge PDFs: {e}")
finally:
progress.pack_forget() # Hide the progress bar after merging
# Set up UI components
ttk.Button(root, text="Select PDFs", command=select_pdfs).pack(pady=10)
ttk.Button(root, text="Merge PDFs", command=merge_pdfs).pack(pady=5)
files_listbox = tk.Listbox(root, selectmode=tk.SINGLE, width=50, height=15)
files_listbox.pack(pady=10)
scrollbar = ttk.Scrollbar(root, orient="vertical", command=files_listbox.yview)
scrollbar.pack(side="right", fill="y")
files_listbox.config(yscrollcommand=scrollbar.set)
# Reorder and clear buttons
ttk.Button(root, text="Move Up", command=move_up).pack(pady=2)
ttk.Button(root, text="Move Down", command=move_down).pack(pady=2)
ttk.Button(root, text="Remove Selected", command=remove_selected).pack(pady=2)
ttk.Button(root, text="Clear All", command=clear_all).pack(pady=2)
# Drag and Drop Bindings
root.drop_target_register(DND_FILES)
root.dnd_bind('<<Drop>>', handle_drop)
# Start the application
root.mainloop()
Happy Coding!