DEV Community

Mate Technologies
Mate Technologies

Posted on

Step-by-Step Guide: Build a PDF to PNG/JPG Converter with Python and Tkinter

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.

  1. 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

  1. 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
Enter fullscreen mode Exit fullscreen mode

Explanation:

tkinter → GUI framework

ttkbootstrap → Adds modern dark/light themes

PIL.Image → Handles images

subprocess → Runs Ghostscript commands

threading → Keeps the GUI responsive

  1. 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)
Enter fullscreen mode Exit fullscreen mode

This ensures your icon or other files are found whether you run the .py or .exe.

  1. 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
Enter fullscreen mode Exit fullscreen mode

darkly → Dark theme

grid_columnconfigure → Makes layout flexible

  1. 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)
Enter fullscreen mode Exit fullscreen mode

Explanation:

StringVar() → Dynamic variables for inputs

Users can choose PDF file or folder

Buttons open file/folder dialogs

  1. 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")
Enter fullscreen mode Exit fullscreen mode
  1. 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")
Enter fullscreen mode Exit fullscreen mode

ScrolledText → scrollable live log

We will append messages during PDF conversion

  1. 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())])
Enter fullscreen mode Exit fullscreen mode

find_ghostscript() → detects GS executable

Ghostscript command converts PDFs to PNG/JPG

Outputs a list of generated image paths

  1. 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")
Enter fullscreen mode Exit fullscreen mode
  1. 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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

stop_event → Allows user to cancel conversion

  1. Run the Application

Finally, start the main loop:

app.mainloop()
Enter fullscreen mode Exit fullscreen mode

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

PDFSnap

Top comments (0)