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.
🖼️ 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.
🎯 3️⃣ Dragging Text Interactively
One of the coolest features: Real-time draggable text
How it works:
- On mouse button press → save initial offset
- While moving mouse → update text_x and text_y
- 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.
✍️ 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.
📐 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.
💾 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!
📊 Architecture Breakdown (Deep Learning Style Explanation)
| Component | Role |
|---|---|
| CustomTkinter | Modern GUI rendering, event handling |
| Canvas | View-layer for image and live graphics |
| Drag Algorithm | Tracks displacement relative to base coordinates |
| PIL Image | Core pixel data and transformations |
| Coordinate Mapping Logic | Ensures 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?
- Photo filters using Pillow
- Layer-based editing with transparency
- Export format optimizations
Keep experimenting & building!

.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)