In this tutorial, we’ll build PDFSnap, a Windows-friendly GUI app that converts PDFs to PNG or JPG images. We’ll use Tkinter, ttkbootstrap, and Pillow, along with Ghostscript for PDF rendering.
This tutorial is beginner-friendly and breaks everything into small, digestible sections.
- Setup & Prerequisites
Before starting, make sure you have Python installed (preferably 3.10+), and install the following libraries:
pip install ttkbootstrap pillow
Ghostscript is required to convert PDFs. Download and install it: Ghostscript for Windows
- Import Required Modules
We start by importing all the modules we need:
import tkinter as tk
from tkinter import filedialog
import ttkbootstrap as tb
from ttkbootstrap.widgets.scrolled import ScrolledText
import threading
import os
import sys
from PIL import Image
import subprocess
import shutil
import tempfile
Explanation:
tkinter → GUI framework
ttkbootstrap → Adds modern dark/light themes
PIL.Image → Handles images
subprocess → Runs Ghostscript commands
threading → Keeps the GUI responsive
- Helper Function for Resource Path
When packaging Python apps (e.g., with PyInstaller), paths to resources can change. We add a utility function:
def resource_path(file_name):
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, file_name)
This ensures your icon or other files are found whether you run the .py or .exe.
- Create the Main Window
Let’s create the main window with ttkbootstrap:
app = tb.Window(
title="PDFSnap – PDF to PNG/JPG Converter",
themename="darkly",
size=(1100, 650)
)
# Make grid responsive
app.grid_columnconfigure(0, weight=1)
app.grid_columnconfigure(1, weight=2)
app.grid_rowconfigure(0, weight=1)
try:
app.iconbitmap(resource_path("logo.ico"))
except Exception:
pass
darkly → Dark theme
grid_columnconfigure → Makes layout flexible
- Create PDF Input Panel
We allow users to select either a single PDF or a folder:
left_panel = tb.Frame(app)
left_panel.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
pdf_card = tb.Labelframe(left_panel, text="PDF Input", padding=15)
pdf_card.grid(row=0, column=0, sticky="ew", pady=5)
pdf_path = tk.StringVar()
folder_path = tk.StringVar()
tb.Entry(pdf_card, textvariable=pdf_path).grid(row=0, column=0, sticky="ew", padx=5)
tb.Button(pdf_card, text="Browse PDF", bootstyle="info",
command=lambda: pdf_path.set(filedialog.askopenfilename(filetypes=[("PDF Files", "*.pdf")]))).grid(row=0, column=1, padx=5)
tb.Label(pdf_card, text="OR select a folder:").grid(row=1, column=0, sticky="w", pady=(10,0))
tb.Entry(pdf_card, textvariable=folder_path).grid(row=2, column=0, sticky="ew", padx=5, pady=(0,5))
tb.Button(pdf_card, text="Browse Folder", bootstyle="info",
command=lambda: folder_path.set(filedialog.askdirectory())).grid(row=2, column=1, padx=5)
Explanation:
StringVar() → Dynamic variables for inputs
Users can choose PDF file or folder
Buttons open file/folder dialogs
- Output Settings Panel
Let’s allow the user to choose output folder, format, and filename options:
output_card = tb.Labelframe(left_panel, text="Output Settings", padding=15)
output_card.grid(row=1, column=0, sticky="ew", pady=5)
output_dir = tk.StringVar()
output_format = tb.Combobox(output_card, values=["PNG", "JPG"], state="readonly", width=10)
output_format.set("PNG")
file_prefix = tk.StringVar(value="page")
preserve_page_nums = tk.BooleanVar(value=True)
subfolder_per_pdf = tk.BooleanVar(value=True)
# Widgets
tb.Entry(output_card, textvariable=output_dir).grid(row=0, column=1, sticky="ew", pady=(0,10))
tb.Button(output_card, text="Browse", bootstyle="info", command=lambda: output_dir.set(filedialog.askdirectory())).grid(row=0, column=2, padx=5)
tb.Label(output_card, text="Convert To").grid(row=1, column=0, sticky="w")
output_format.grid(row=1, column=1, sticky="w")
tb.Label(output_card, text="Filename Prefix").grid(row=2, column=0, sticky="w")
tb.Entry(output_card, textvariable=file_prefix, width=20).grid(row=2, column=1, sticky="w")
tb.Checkbutton(output_card, text="Preserve Original Page Numbers", variable=preserve_page_nums, bootstyle="success").grid(row=3, column=0, columnspan=2, sticky="w")
tb.Checkbutton(output_card, text="Create Subfolder per PDF", variable=subfolder_per_pdf, bootstyle="warning").grid(row=4, column=0, columnspan=2, sticky="w")
- Live Output Log
It’s important to show progress:
right_panel = tb.Frame(app)
right_panel.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)
log_card = tb.Labelframe(right_panel, text="Live Output", padding=15)
log_card.grid(row=0, column=0, sticky="nsew")
log_card.grid_rowconfigure(0, weight=1)
log_card.grid_columnconfigure(0, weight=1)
log = ScrolledText(log_card)
log.grid(row=0, column=0, sticky="nsew")
log.text.config(state="disabled")
ScrolledText → scrollable live log
We will append messages during PDF conversion
- Ghostscript PDF Conversion Function
We need Ghostscript to convert PDF pages to images:
def find_ghostscript():
for exe in ("gswin64c.exe", "gswin32c.exe"):
path = shutil.which(exe)
if path and os.path.isfile(path):
return path
return None
def pdf_to_images_gs(pdf_path, output_dir, fmt="png", dpi=300):
gs = find_ghostscript()
if not gs:
raise RuntimeError("Ghostscript not found. Please install it.")
device = "png16m" if fmt.lower() == "png" else "jpeg"
output_pattern = os.path.join(output_dir, "page_%03d." + fmt.lower())
cmd = [gs, "-dSAFER", "-dBATCH", "-dNOPAUSE", "-sDEVICE=" + device, f"-r{dpi}", "-sOutputFile=" + output_pattern, pdf_path]
subprocess.run(cmd, capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW)
return sorted([os.path.join(output_dir, f) for f in os.listdir(output_dir) if f.lower().endswith(fmt.lower())])
find_ghostscript() → detects GS executable
Ghostscript command converts PDFs to PNG/JPG
Outputs a list of generated image paths
- Convert Single PDF
Now we handle one PDF at a time:
def convert_single_pdf(pdf_file, output_dir_path):
fmt = output_format.get()
base_name = os.path.splitext(os.path.basename(pdf_file))[0]
save_dir = os.path.join(output_dir_path, base_name) if subfolder_per_pdf.get() else output_dir_path
os.makedirs(save_dir, exist_ok=True)
with tempfile.TemporaryDirectory() as temp_dir:
images = pdf_to_images_gs(pdf_file, temp_dir, fmt=fmt.lower())
for idx, img_path in enumerate(images, start=1):
img = Image.open(img_path)
prefix = file_prefix.get() if pdf_path.get() and not folder_path.get() else base_name
fname = f"{prefix}_Page{idx}.{fmt.lower()}" if preserve_page_nums.get() else f"{prefix}_{idx:03d}.{fmt.lower()}"
out_path = os.path.join(save_dir, fname)
img.save(out_path)
log.text.config(state="normal")
log.text.insert("end", f"Saved: {fname}\n")
log.text.see("end")
log.text.config(state="disabled")
- Batch Conversion & Threading
We allow multiple PDFs and keep the GUI responsive:
def convert_batch():
output_folder = output_dir.get()
if not output_folder:
tb.show_error("Missing Output Folder", "Please select an output folder.")
return
pdf_files = [pdf_path.get()] if pdf_path.get() else [os.path.join(folder_path.get(), f) for f in os.listdir(folder_path.get()) if f.lower().endswith(".pdf")]
for pdf_file in pdf_files:
convert_single_pdf(pdf_file, output_folder)
btn_start = tb.Button(app, text="Start Conversion", bootstyle="success", command=lambda: threading.Thread(target=convert_batch, daemon=True).start())
btn_start.pack()
Threading prevents GUI freezing
Can handle single PDF or folder of PDFs
✅ 11. Final Touches
Add Stop and About buttons:
btn_stop = tb.Button(app, text="Stop", bootstyle="danger", command=lambda: stop_event.set())
btn_about = tb.Button(app, text="About", bootstyle="info", command=lambda: tb.show_info("About PDFSnap", "PDFSnap converts PDFs to images."))
btn_stop.pack()
btn_about.pack()
stop_event → Allows user to cancel conversion
- Run the Application
Finally, start the main loop:
app.mainloop()
Now your PDFSnap application is fully functional!
✅ Features Recap
Converts PDFs to PNG/JPG
Single PDF and batch folder mode
Live log and progress bar
Option to preserve page numbers
Automatic subfolders per PDF

Top comments (0)