Thumbnail Maker using Python

Thumbnail Maker using Python

Thumbnail Maker using Python & CustomTkinter – Full Project Tutorial

Thumbnail designing is one of the most common tasks in modern content creation, especially for YouTube, blogs, social media promotions, and UI previews. While professional tools like Photoshop exist, they can be overwhelming for beginners. So, today we will build a graphical tool using Python that allows users to create customized thumbnails with text placement using simple drag-and-drop!

This Python application uses a combination of CustomTkinter for a modern dark-themed UI and Pillow (PIL) to handle image processing. It allows users to:

  • Open any image
  • Check and update the image size (Width & Height)
  • Add custom text on the image
  • Change text position by dragging mouse
  • Customize font size
  • Change the text color using HEX input (#FFFFFF etc.)
  • Save the final thumbnail as PNG format

🧠 Why This Project is Useful?

Thumbnails influence clicks and audience engagement. With this tool, content creators can instantly produce thumbnails without any complex software. Also, developers learn:

  • GUI design with real-time updates
  • Mouse event handling for drag text functionality
  • Dynamic resizing and canvas scaling
  • Text rendering and color encoding
  • Managing PIL image coordinates professionally

It is an excellent skill development project for portfolio demonstrations or freelance utilities.


📌 Required Libraries

pip install customtkinter
pip install pillow

Now, let’s explore how the entire program works in detail and understand every function step-by-step.


📂 Complete Source Code

import customtkinter as ctk
from tkinter import filedialog, messagebox
from PIL import Image, ImageTk, ImageDraw, ImageFont

# Initialize CustomTkinter
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")


class CenteredThumbnailMaker:
    def __init__(self, root):
        self.root = root
        self.root.title("Centered Thumbnail Maker")
        self.img_path = None
        self.original_img = None
        self.tk_img = None

        # Text position (relative to image top-left) and dragging
        self.text_x = 50
        self.text_y = 50
        self.drag_data = {"x": 0, "y": 0}

        # Text properties
        self.font_size = ctk.IntVar(value=40)
        self.font_color = ctk.StringVar(value="#FFFFFF")

        # Canvas
        self.canvas = ctk.CTkCanvas(root, bg="#2b2b2b")
        self.canvas.pack(fill="both", expand=True)
        self.canvas.bind("", self.start_drag)
        self.canvas.bind("", self.drag_text)

        # Controls frame
        controls_frame = ctk.CTkFrame(root)
        controls_frame.pack(pady=5, fill="x")

        # Open image
        ctk.CTkButton(controls_frame, text="Open Image", command=self.open_image).grid(row=0, column=0, padx=5, pady=5)

        # Original image size label
        self.size_label = ctk.CTkLabel(controls_frame, text="Image Size: N/A")
        self.size_label.grid(row=0, column=1, padx=5)

        # Resize entries
        ctk.CTkLabel(controls_frame, text="Width").grid(row=0, column=2)
        self.width_entry = ctk.CTkEntry(controls_frame, width=60)
        self.width_entry.grid(row=0, column=3)
        self.width_entry.insert(0, "500")

        ctk.CTkLabel(controls_frame, text="Height").grid(row=0, column=4)
        self.height_entry = ctk.CTkEntry(controls_frame, width=60)
        self.height_entry.grid(row=0, column=5)
        self.height_entry.insert(0, "400")

        # Update size button
        ctk.CTkButton(controls_frame, text="Update Size", command=self.display_image).grid(row=0, column=6, padx=5)

        # Bind Enter key to update size
        self.width_entry.bind("", lambda e: self.display_image())
        self.height_entry.bind("", lambda e: self.display_image())

        # Text entry
        ctk.CTkLabel(controls_frame, text="Text").grid(row=1, column=0)
        self.text_entry = ctk.CTkEntry(controls_frame)
        self.text_entry.grid(row=1, column=1, columnspan=2)
        self.text_entry.insert(0, "Your Text Here")
        self.text_entry.bind("", lambda e: self.update_text())

        # Font size slider
        ctk.CTkLabel(controls_frame, text="Font Size").grid(row=1, column=3)
        self.size_slider = ctk.CTkSlider(controls_frame, from_=10, to=100, variable=self.font_size,
                                         command=lambda e: self.update_text())
        self.size_slider.grid(row=1, column=4, padx=5)

        # Font color
        ctk.CTkLabel(controls_frame, text="Font Color (#HEX)").grid(row=2, column=0)
        self.color_entry = ctk.CTkEntry(controls_frame, textvariable=self.font_color)
        self.color_entry.grid(row=2, column=1)
        self.color_entry.bind("", lambda e: self.update_text())

        # Save button
        ctk.CTkButton(controls_frame, text="Save Thumbnail", command=self.save_thumbnail).grid(row=2, column=2, padx=5)

        # Canvas text ID
        self.canvas_text_id = None

        # Bind window resize
        self.root.bind("", self.resize_canvas)

        # Image position on canvas
        self.img_canvas_x = 0
        self.img_canvas_y = 0

    # Open image
    def open_image(self):
        self.img_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.jpeg *.png")])
        if self.img_path:
            self.original_img = Image.open(self.img_path)

            # Show original size
            self.size_label.configure(text=f"Image Size: {self.original_img.width}x{self.original_img.height}")

            # Set width/height entries to original image size
            self.width_entry.delete(0, "end")
            self.width_entry.insert(0, str(self.original_img.width))
            self.height_entry.delete(0, "end")
            self.height_entry.insert(0, str(self.original_img.height))

            self.display_image()

    # Display image centered on canvas
    def display_image(self):
        if not self.original_img:
            return

        # Resize image based on entries
        try:
            width = int(self.width_entry.get())
            height = int(self.height_entry.get())
            img_copy = self.original_img.copy()
            img_copy = img_copy.resize((width, height))
        except:
            img_copy = self.original_img.copy()

        self.tk_img = ImageTk.PhotoImage(img_copy)

        # Clear canvas
        self.canvas.delete("all")

        # Center the image
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        self.img_canvas_x = (canvas_width - self.tk_img.width()) // 2
        self.img_canvas_y = (canvas_height - self.tk_img.height()) // 2

        self.canvas.create_image(self.img_canvas_x, self.img_canvas_y, anchor="nw", image=self.tk_img)
        self.update_text_on_canvas()

    # Update text on canvas
    def update_text_on_canvas(self):
        if self.canvas_text_id:
            self.canvas.delete(self.canvas_text_id)
        text = self.text_entry.get()
        if text and self.tk_img:
            self.canvas_text_id = self.canvas.create_text(
                self.img_canvas_x + self.text_x,
                self.img_canvas_y + self.text_y,
                text=text,
                fill=self.font_color.get(),
                font=("Arial", self.font_size.get()),
                anchor="nw"
            )

    def update_text(self, event=None):
        self.update_text_on_canvas()

    # Drag text
    def start_drag(self, event):
        self.drag_data["x"] = event.x - self.img_canvas_x - self.text_x
        self.drag_data["y"] = event.y - self.img_canvas_y - self.text_y

    def drag_text(self, event):
        self.text_x = event.x - self.img_canvas_x - self.drag_data["x"]
        self.text_y = event.y - self.img_canvas_y - self.drag_data["y"]
        self.update_text_on_canvas()

    # Resize canvas dynamically
    def resize_canvas(self, event):
        if self.original_img:
            self.display_image()

    # Save thumbnail
    def save_thumbnail(self):
        if not self.original_img:
            messagebox.showerror("Error", "No image loaded!")
            return

        try:
            width = int(self.width_entry.get())
            height = int(self.height_entry.get())
            thumbnail = self.original_img.copy()
            thumbnail = thumbnail.resize((width, height))
        except:
            thumbnail = self.original_img.copy()

        text = self.text_entry.get()
        if text:
            draw = ImageDraw.Draw(thumbnail)
            font = ImageFont.load_default()
            try:
                font = ImageFont.truetype("arial.ttf", self.font_size.get())
            except:
                pass

            # Map text position relative to image
            x_ratio = self.text_x / self.tk_img.width()
            y_ratio = self.text_y / self.tk_img.height()
            x = int(x_ratio * thumbnail.width)
            y = int(y_ratio * thumbnail.height)

            draw.text((x, y), text, font=font, fill=self.font_color.get())

        save_path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG files", "*.png")])
        if save_path:
            thumbnail.save(save_path)
            messagebox.showinfo("Success", f"Thumbnail saved at {save_path}")


# Run the app
root = ctk.CTk()
root.state("zoomed")       # Open maximized
root.resizable(True, True) # Allow resize/minimize/maximize
app = CenteredThumbnailMaker(root)
root.mainloop()

You already have the complete code above, but now let’s break down how it actually works!


🧩 Code Explanation

✨ 1️⃣ Modern GUI Initialization

We activate CustomTkinter dark mode and theme for a premium GUI feel. It makes the app look futuristic with smooth dark shades that reduce eye strain — something users absolutely love in professional software!

Then we create a centered canvas where the image will be rendered dynamically. The widget continuously listens for resizing events to adjust image position properly.

Modern GUI Initialization

🖼️ 2️⃣ Opening and Displaying Images

When the user selects an image:

  • Pillow loads the file
  • Original image size is displayed
  • Width & Height input fields automatically update
  • Image gets perfectly centered on canvas

This gives a better preview experience vs placing image at top-left position.

Opening and Displaying Images

🎯 3️⃣ Dragging Text Interactively

One of the coolest features: Real-time draggable text

How it works:

  1. On mouse button press → save initial offset
  2. While moving mouse → update text_x and text_y
  3. Re-draw the text in its new position

No complicated UI — pure intuitive user action! This is exactly how graphical editors function behind the scenes.

Dragging Text Interactively

✍️ 4️⃣ Text and Font Customization

The user can type custom text:

  • Text instantly updates on canvas
  • Font size changes using slider
  • Font color works via hexadecimal (#FF0000 etc.)

This provides full flexibility for personalized thumbnails.

Text and Font Customization
Text and Font Customization Screenshot

📐 5️⃣ Dynamic Image Resizing

Users can resize image to fit:

  • YouTube thumbnail size (1280×720)
  • Instagram post thumbnail (1080×1080)
  • Blog banner (1200×628)

Enter width and height → press Update Size → image rescales instantly.

Dynamic Image Resizing

💾 6️⃣ Exporting Final Thumbnail

A crucial challenge solved:

Canvas coordinates are not equal to actual image pixel coordinates.

So, we calculate exact position ratios:

x_ratio = text_x / displayed_width
y_ratio = text_y / displayed_height
x = x_ratio * final_image_width
y = y_ratio * final_image_height

Using PIL ImageDraw, text is printed exactly where the user placed it. Finally, thumbnail is saved as high-quality PNG!

Exporting Final Thumbnail Screenshot 1

Exporting Final Thumbnail Screenshot 2

Exporting Final Thumbnail Screenshot 3

Exporting Final Thumbnail Screenshot 4

📊 Architecture Breakdown (Deep Learning Style Explanation)

ComponentRole
CustomTkinterModern GUI rendering, event handling
CanvasView-layer for image and live graphics
Drag AlgorithmTracks displacement relative to base coordinates
PIL ImageCore pixel data and transformations
Coordinate Mapping LogicEnsures precision text placement on export

This setup mimics real-world image editing workflows.


🎨 UI/UX Design Perspective

The interface follows three key user-experience principles:

  • Discoverability: Every feature visible upfront
  • Instant Feedback: User sees updates immediately
  • Minimal User Effort: Drag instead of manually entering positions

These details make your tool feel like a mini Photoshop 😎


💡 Possible Future Enhancements

  • Add text shadow effects for YouTube-style pop-out text
  • Border / stroke styling around text
  • Drop preset thumbnail sizes for platforms
  • Font family chooser
  • Multiple text layers support
  • Live color picker tool

Each feature increases the application’s usability and value!


🎯 Final Thoughts

You have now built a fully functional Thumbnail Creator Software using Python! This project not only strengthens GUI programming skills but also demonstrates how to work with real images for real-world tasks. Feel free to improve and publish your application — users will love the simplicity!

If you have any doubts or want additional features, comment below and we will help you out!


❓ Frequently Asked Questions (FAQ)

1️⃣ What is CustomTkinter?

CustomTkinter is a modern styling extension for Tkinter that provides attractive themes like dark mode and rounded UI components. It improves UI/UX quality without needing CSS or JavaScript.

2️⃣ Why use Pillow (PIL) in this project?

Pillow helps us resize images, add text, and save final thumbnails. Tkinter alone cannot draw text directly onto an image file, so PIL is essential for real rendering.

3️⃣ Can I export thumbnails in JPG instead of PNG?

Yes! Just modify the file selection type in the save_thumbnail() function to include JPG or change the default file format to .jpg.

4️⃣ Why doesn’t the Arial font load on some systems?

Some Linux-based distributions do not include Arial by default. Python then falls back to Pillow’s built-in default font. You can install additional fonts or specify a TTF file path manually.

5️⃣ How do I add drop shadow or stroke (outline) to text?

You can draw the same text multiple times with slightly different coordinates and darker shading behind the main text. I can provide ready-made code if you want YouTube style 3D text.

6️⃣ Can I use this project commercially?

Yes! You may enhance and distribute your software freely. That’s the power of open-source development!

7️⃣ Is this tool beginner friendly?

Absolutely. Even beginners can load an image, drag text, and save thumbnails without any design skills or coding knowledge.

8️⃣ How do I convert this Python script into a Windows .exe application?

You can use PyInstaller:

pyinstaller --onefile --windowed main.py

This creates an EXE that works on systems without Python installed.

9️⃣ Does this code support transparent PNG images?

Yes. If the loaded image contains transparency, saving to PNG preserves alpha channels properly.

🔟 Can we add multiple text layers?

Currently, the app supports one draggable text. But I can upgrade the code to allow multiple selectable text objects — similar to Canva or Photoshop!


👉 What to learn next?

Keep experimenting & building!

Previous Post Next Post

Contact Form