ShortGenerator/thumbnail_editor_modern.py
klop51 6bb356948d feat: Add modern thumbnail editor with advanced UI and editing features
- Implemented a new thumbnail editor using Tkinter and MoviePy
- Added functionality to load video files and capture frames
- Created a modern user interface with a dark theme and responsive design
- Included tools for adding text and stickers to thumbnails
- Implemented export options for saving edited thumbnails
- Added default emoji stickers and functionality to load custom stickers
- Enhanced user experience with hover effects and modern button styles
2025-08-10 14:11:18 +02:00

627 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import tkinter as tk
from tkinter import filedialog, simpledialog, colorchooser, messagebox, ttk
from moviepy import VideoFileClip
from PIL import Image, ImageTk, ImageDraw, ImageFont
# Modern Thumbnail Editor with Professional UI Design
class ModernThumbnailEditor:
def __init__(self, video_path):
self.video_path = video_path
self.clip = None
self.current_frame_img = None
self.canvas_items = []
self.drag_data = {"item": None, "x": 0, "y": 0}
# Modern color scheme
self.colors = {
'bg_primary': '#1a1a1a', # Dark background
'bg_secondary': '#2d2d2d', # Card backgrounds
'bg_tertiary': '#3d3d3d', # Elevated elements
'accent_blue': '#007acc', # Primary blue
'accent_green': '#28a745', # Success green
'accent_orange': '#fd7e14', # Warning orange
'accent_purple': '#6f42c1', # Secondary purple
'accent_red': '#dc3545', # Error red
'text_primary': '#ffffff', # Primary text
'text_secondary': '#b8b8b8', # Secondary text
'text_muted': '#6c757d', # Muted text
'border': '#404040', # Border color
'hover': '#4a4a4a' # Hover state
}
# Modern fonts
self.fonts = {
'title': ('Segoe UI', 18, 'bold'),
'heading': ('Segoe UI', 14, 'bold'),
'subheading': ('Segoe UI', 12, 'bold'),
'body': ('Segoe UI', 10),
'caption': ('Segoe UI', 9),
'button': ('Segoe UI', 10, 'bold')
}
self.setup_ui()
def setup_ui(self):
self.editor = tk.Toplevel()
self.editor.title("📸 Professional Thumbnail Editor")
self.editor.geometry("1400x900")
self.editor.minsize(1200, 800)
self.editor.configure(bg=self.colors['bg_primary'])
# Load video
try:
print(f"📹 Loading video: {os.path.basename(self.video_path)}")
self.clip = VideoFileClip(self.video_path)
self.duration = int(self.clip.duration)
except Exception as e:
messagebox.showerror("Video Error", f"Failed to load video: {e}")
self.editor.destroy()
return
# Setup stickers folder
self.stickers_folder = os.path.join(os.path.dirname(__file__), "stickers")
os.makedirs(self.stickers_folder, exist_ok=True)
self.create_default_stickers()
self.create_modern_interface()
def create_modern_interface(self):
"""Create the modern thumbnail editor interface"""
# Header
header_frame = tk.Frame(self.editor, bg=self.colors['bg_secondary'], height=70)
header_frame.pack(fill="x", padx=0, pady=0)
header_frame.pack_propagate(False)
title_frame = tk.Frame(header_frame, bg=self.colors['bg_secondary'])
title_frame.pack(expand=True, fill="both", padx=30, pady=15)
title_label = tk.Label(title_frame, text="📸 Professional Thumbnail Editor",
font=self.fonts['title'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
title_label.pack(side="left")
# Video info
video_name = os.path.basename(self.video_path)
info_label = tk.Label(title_frame, text=f"Editing: {video_name}",
font=self.fonts['caption'], bg=self.colors['bg_secondary'],
fg=self.colors['text_secondary'])
info_label.pack(side="right")
# Main content area
main_container = tk.Frame(self.editor, bg=self.colors['bg_primary'])
main_container.pack(fill="both", expand=True, padx=20, pady=20)
# Left panel - Canvas area
left_panel = tk.Frame(main_container, bg=self.colors['bg_secondary'])
left_panel.pack(side="left", fill="both", expand=True, padx=(0, 10))
self.setup_canvas_area(left_panel)
# Right panel - Controls
right_panel = tk.Frame(main_container, bg=self.colors['bg_secondary'], width=350)
right_panel.pack(side="right", fill="y")
right_panel.pack_propagate(False)
self.setup_controls_panel(right_panel)
def setup_canvas_area(self, parent):
"""Setup the main canvas area with modern styling"""
# Canvas header
canvas_header = tk.Frame(parent, bg=self.colors['bg_secondary'])
canvas_header.pack(fill="x", padx=20, pady=(20, 10))
canvas_title = tk.Label(canvas_header, text="🎬 Thumbnail Preview",
font=self.fonts['heading'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
canvas_title.pack(side="left")
# Canvas container
canvas_container = tk.Frame(parent, bg=self.colors['bg_tertiary'], relief="flat", bd=2)
canvas_container.pack(fill="both", expand=True, padx=20, pady=(0, 20))
# Modern canvas with dark theme
self.canvas = tk.Canvas(canvas_container, bg='#000000', highlightthickness=0,
relief="flat", bd=0)
self.canvas.pack(fill="both", expand=True, padx=10, pady=10)
# Bind canvas events for dragging
self.canvas.bind("<Button-1>", self.on_canvas_click)
self.canvas.bind("<B1-Motion>", self.on_canvas_drag)
self.canvas.bind("<ButtonRelease-1>", self.on_canvas_release)
# Frame timeline slider
timeline_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
timeline_frame.pack(fill="x", padx=20, pady=(0, 20))
tk.Label(timeline_frame, text="⏱️ Timeline", font=self.fonts['subheading'],
bg=self.colors['bg_secondary'], fg=self.colors['text_primary']).pack(anchor="w", pady=(0, 10))
# Modern slider styling
style = ttk.Style()
style.configure("Modern.Horizontal.TScale",
background=self.colors['bg_secondary'],
troughcolor=self.colors['bg_tertiary'],
sliderlength=20,
sliderrelief="flat")
self.time_var = tk.DoubleVar(value=0)
self.time_slider = ttk.Scale(timeline_frame, from_=0, to=self.duration,
orient="horizontal", variable=self.time_var,
command=self.on_time_change, style="Modern.Horizontal.TScale")
self.time_slider.pack(fill="x", pady=(0, 5))
# Time display
time_display_frame = tk.Frame(timeline_frame, bg=self.colors['bg_secondary'])
time_display_frame.pack(fill="x")
self.time_label = tk.Label(time_display_frame, text="00:00",
font=self.fonts['body'], bg=self.colors['bg_secondary'],
fg=self.colors['text_secondary'])
self.time_label.pack(side="left")
duration_label = tk.Label(time_display_frame, text=f"/ {self.duration//60:02d}:{self.duration%60:02d}",
font=self.fonts['body'], bg=self.colors['bg_secondary'],
fg=self.colors['text_muted'])
duration_label.pack(side="right")
# Load initial frame
self.update_canvas_frame(0)
def setup_controls_panel(self, parent):
"""Setup the right panel controls with modern design"""
# Scroll container for controls
scroll_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
scroll_frame.pack(fill="both", expand=True, padx=20, pady=20)
# Title
controls_title = tk.Label(scroll_frame, text="🎨 Editing Tools",
font=self.fonts['heading'], bg=self.colors['bg_secondary'],
fg=self.colors['text_primary'])
controls_title.pack(anchor="w", pady=(0, 20))
# Text Tools Card
self.create_text_tools_card(scroll_frame)
# Stickers Card
self.create_stickers_card(scroll_frame)
# Export Card
self.create_export_card(scroll_frame)
def create_text_tools_card(self, parent):
"""Create modern text tools card"""
text_card = self.create_modern_card(parent, "✍️ Text Tools")
# Add text button
add_text_btn = self.create_modern_button(text_card, " Add Text",
self.colors['accent_blue'], self.add_text)
add_text_btn.pack(fill="x", pady=(0, 10))
# Text style options
style_frame = tk.Frame(text_card, bg=self.colors['bg_secondary'])
style_frame.pack(fill="x", pady=(0, 10))
tk.Label(style_frame, text="Text Size:", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w")
self.text_size_var = tk.IntVar(value=36)
size_frame = tk.Frame(style_frame, bg=self.colors['bg_secondary'])
size_frame.pack(fill="x", pady=(5, 0))
for size in [24, 36, 48, 64]:
btn = tk.Button(size_frame, text=str(size), font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
relief="flat", bd=0, padx=10, pady=5,
activebackground=self.colors['hover'],
command=lambda s=size: self.text_size_var.set(s))
btn.pack(side="left", padx=(0, 5))
self.add_hover_effect(btn)
# Text color
color_frame = tk.Frame(text_card, bg=self.colors['bg_secondary'])
color_frame.pack(fill="x", pady=(10, 0))
tk.Label(color_frame, text="Text Color:", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w")
self.text_color_btn = self.create_modern_button(color_frame, "🎨 Choose Color",
self.colors['accent_purple'], self.choose_text_color)
self.text_color_btn.pack(fill="x", pady=(5, 0))
self.current_text_color = "#FFFFFF"
def create_stickers_card(self, parent):
"""Create modern stickers card"""
stickers_card = self.create_modern_card(parent, "😊 Stickers & Emojis")
# Load stickers button
load_btn = self.create_modern_button(stickers_card, "📁 Load Custom Sticker",
self.colors['accent_green'], self.load_custom_sticker)
load_btn.pack(fill="x", pady=(0, 15))
# Default stickers grid
stickers_frame = tk.Frame(stickers_card, bg=self.colors['bg_secondary'])
stickers_frame.pack(fill="x")
tk.Label(stickers_frame, text="Default Stickers:", font=self.fonts['body'],
bg=self.colors['bg_secondary'], fg=self.colors['text_secondary']).pack(anchor="w", pady=(0, 10))
# Create grid for stickers
self.create_stickers_grid(stickers_frame)
def create_stickers_grid(self, parent):
"""Create a grid of default stickers"""
sticker_files = [f for f in os.listdir(self.stickers_folder) if f.endswith(('.png', '.jpg', '.jpeg'))]
grid_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
grid_frame.pack(fill="x")
cols = 3
for i, sticker_file in enumerate(sticker_files[:12]): # Limit to 12 stickers
row = i // cols
col = i % cols
try:
sticker_path = os.path.join(self.stickers_folder, sticker_file)
img = Image.open(sticker_path)
img.thumbnail((40, 40), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(img)
btn = tk.Button(grid_frame, image=photo,
bg=self.colors['bg_tertiary'], relief="flat", bd=0,
activebackground=self.colors['hover'],
command=lambda path=sticker_path: self.add_sticker(path))
btn.image = photo # Keep reference
btn.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
self.add_hover_effect(btn)
# Configure grid weights
grid_frame.grid_columnconfigure(col, weight=1)
except Exception as e:
print(f"⚠️ Error loading sticker {sticker_file}: {e}")
def create_export_card(self, parent):
"""Create modern export options card"""
export_card = self.create_modern_card(parent, "💾 Export Options")
# Clear all button
clear_btn = self.create_modern_button(export_card, "🗑️ Clear All Elements",
self.colors['accent_orange'], self.clear_all_elements)
clear_btn.pack(fill="x", pady=(0, 10))
# Save thumbnail button
save_btn = self.create_modern_button(export_card, "💾 Save Thumbnail",
self.colors['accent_green'], self.save_thumbnail)
save_btn.pack(fill="x", pady=(0, 10))
# Close editor button
close_btn = self.create_modern_button(export_card, "❌ Close Editor",
self.colors['accent_red'], self.close_editor)
close_btn.pack(fill="x")
def create_modern_card(self, parent, title):
"""Create a modern card container"""
card_frame = tk.Frame(parent, bg=self.colors['bg_tertiary'], relief="flat", bd=0)
card_frame.pack(fill="x", pady=(0, 20))
# Card header
header_frame = tk.Frame(card_frame, bg=self.colors['bg_tertiary'])
header_frame.pack(fill="x", padx=15, pady=(15, 10))
title_label = tk.Label(header_frame, text=title, font=self.fonts['subheading'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'])
title_label.pack(anchor="w")
# Card content
content_frame = tk.Frame(card_frame, bg=self.colors['bg_secondary'])
content_frame.pack(fill="both", expand=True, padx=15, pady=(0, 15))
return content_frame
def create_modern_button(self, parent, text, color, command):
"""Create a modern styled button"""
btn = tk.Button(parent, text=text, font=self.fonts['button'],
bg=color, fg=self.colors['text_primary'],
relief="flat", bd=0, padx=20, pady=12,
activebackground=self.colors['hover'],
command=command, cursor="hand2")
self.add_hover_effect(btn, color)
return btn
def add_hover_effect(self, widget, base_color=None):
"""Add hover effect to widget"""
if base_color is None:
base_color = self.colors['bg_tertiary']
def on_enter(e):
widget.configure(bg=self.colors['hover'])
def on_leave(e):
widget.configure(bg=base_color)
widget.bind("<Enter>", on_enter)
widget.bind("<Leave>", on_leave)
def capture_frame_at(self, time_sec):
"""Capture frame from video at specific time"""
try:
frame = self.clip.get_frame(max(0, min(time_sec, self.clip.duration - 0.1)))
img = Image.fromarray(frame)
# Maintain aspect ratio while fitting in canvas
img.thumbnail((720, 405), Image.Resampling.LANCZOS)
return img
except Exception as e:
print(f"⚠️ Error capturing frame: {e}")
# Create a placeholder image
img = Image.new('RGB', (720, 405), color='black')
return img
def update_canvas_frame(self, time_sec):
"""Update canvas with frame at specific time"""
try:
self.current_frame_img = self.capture_frame_at(time_sec)
self.tk_frame_img = ImageTk.PhotoImage(self.current_frame_img)
# Clear canvas and add new frame
self.canvas.delete("frame")
self.canvas.create_image(360, 202, image=self.tk_frame_img, tags="frame")
self.canvas.image = self.tk_frame_img
# Update time display
minutes = int(time_sec) // 60
seconds = int(time_sec) % 60
self.time_label.config(text=f"{minutes:02d}:{seconds:02d}")
except Exception as e:
print(f"⚠️ Error updating frame: {e}")
def on_time_change(self, val):
"""Handle timeline slider change"""
self.update_canvas_frame(float(val))
# Canvas interaction methods
def on_canvas_click(self, event):
"""Handle canvas click for dragging"""
item = self.canvas.find_closest(event.x, event.y)[0]
if item and item != self.canvas.find_withtag("frame"):
self.drag_data["item"] = item
self.drag_data["x"] = event.x
self.drag_data["y"] = event.y
def on_canvas_drag(self, event):
"""Handle canvas dragging"""
if self.drag_data["item"]:
dx = event.x - self.drag_data["x"]
dy = event.y - self.drag_data["y"]
self.canvas.move(self.drag_data["item"], dx, dy)
self.drag_data["x"] = event.x
self.drag_data["y"] = event.y
def on_canvas_release(self, event):
"""Handle canvas release"""
self.drag_data["item"] = None
# Editing functionality
def add_text(self):
"""Add text to the canvas"""
text = simpledialog.askstring("Add Text", "Enter text:")
if text:
try:
size = self.text_size_var.get()
item = self.canvas.create_text(360, 200, text=text, fill=self.current_text_color,
font=("Arial", size, "bold"), tags="draggable")
self.canvas_items.append(("text", item, text, self.current_text_color, size))
print(f"✅ Added text: {text}")
except Exception as e:
print(f"⚠️ Error adding text: {e}")
def choose_text_color(self):
"""Choose text color"""
color = colorchooser.askcolor(title="Choose text color")
if color[1]:
self.current_text_color = color[1]
# Update button color to show current selection
self.text_color_btn.config(bg=self.current_text_color)
def add_sticker(self, path):
"""Add sticker to canvas"""
try:
img = Image.open(path).convert("RGBA")
img.thumbnail((60, 60), Image.Resampling.LANCZOS)
tk_img = ImageTk.PhotoImage(img)
item = self.canvas.create_image(360, 200, image=tk_img, tags="draggable")
# Keep reference to prevent garbage collection
if not hasattr(self.canvas, 'images'):
self.canvas.images = []
self.canvas.images.append(tk_img)
self.canvas_items.append(("sticker", item, img, path))
print(f"✅ Added sticker: {os.path.basename(path)}")
except Exception as e:
print(f"⚠️ Failed to load sticker {path}: {e}")
def load_custom_sticker(self):
"""Load custom sticker file"""
file_path = filedialog.askopenfilename(
title="Select Sticker",
filetypes=[("Image files", "*.png *.jpg *.jpeg *.gif *.bmp")]
)
if file_path:
self.add_sticker(file_path)
def clear_all_elements(self):
"""Clear all added elements"""
# Clear all draggable items
self.canvas.delete("draggable")
self.canvas_items.clear()
if hasattr(self.canvas, 'images'):
self.canvas.images.clear()
print("🗑️ Cleared all elements")
def save_thumbnail(self):
"""Save the current thumbnail"""
if not self.current_frame_img:
messagebox.showerror("Error", "No frame loaded")
return
save_path = filedialog.asksaveasfilename(
title="Save Thumbnail",
defaultextension=".jpg",
filetypes=[("JPEG files", "*.jpg"), ("PNG files", "*.png")]
)
if not save_path:
return
try:
# Create a copy of the current frame
frame = self.current_frame_img.copy().convert("RGBA")
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
# Calculate scaling factors
scale_x = frame.width / canvas_width
scale_y = frame.height / canvas_height
draw = ImageDraw.Draw(frame)
# Process all canvas items
for item_type, item_id, *data in self.canvas_items:
coords = self.canvas.coords(item_id)
if not coords:
continue
if item_type == "sticker":
# Handle sticker overlay
img_data, path = data
x, y = coords[0], coords[1]
px = int(x * scale_x)
py = int(y * scale_y)
# Scale sticker size
sticker_img = img_data.copy()
new_size = (int(60 * scale_x), int(60 * scale_y))
sticker_img = sticker_img.resize(new_size, Image.Resampling.LANCZOS)
# Calculate position to center the sticker
paste_x = px - sticker_img.width // 2
paste_y = py - sticker_img.height // 2
frame.paste(sticker_img, (paste_x, paste_y), sticker_img)
elif item_type == "text":
# Handle text overlay
text_value, color, font_size = data
x, y = coords[0], coords[1]
px = int(x * scale_x)
py = int(y * scale_y)
# Scale font size
scaled_font_size = int(font_size * scale_x)
try:
font = ImageFont.truetype("arial.ttf", scaled_font_size)
except:
try:
font = ImageFont.truetype("calibri.ttf", scaled_font_size)
except:
font = ImageFont.load_default()
# Get text bounding box for centering
bbox = draw.textbbox((0, 0), text_value, font=font)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
# Draw text with outline
outline_w = max(2, scaled_font_size // 15)
for dx in range(-outline_w, outline_w + 1):
for dy in range(-outline_w, outline_w + 1):
draw.text((px - text_w//2 + dx, py - text_h//2 + dy),
text_value, font=font, fill="black")
draw.text((px - text_w//2, py - text_h//2), text_value, font=font, fill=color)
# Convert to RGB and save
if save_path.lower().endswith('.png'):
frame.save(save_path, "PNG", quality=95)
else:
background = Image.new("RGB", frame.size, (255, 255, 255))
background.paste(frame, mask=frame.split()[3] if frame.mode == 'RGBA' else None)
background.save(save_path, "JPEG", quality=95)
print(f"✅ Thumbnail saved: {save_path}")
messagebox.showinfo("Success", f"Thumbnail saved successfully!\n{save_path}")
except Exception as e:
print(f"❌ Error saving thumbnail: {e}")
messagebox.showerror("Error", f"Failed to save thumbnail:\n{str(e)}")
def close_editor(self):
"""Close the editor"""
try:
if self.clip:
self.clip.close()
except:
pass
self.editor.destroy()
def create_default_stickers(self):
"""Create default emoji stickers"""
stickers_data = {
"smile.png": "😊",
"laugh.png": "😂",
"happy-face.png": "😀",
"sad-face.png": "😢",
"confused.png": "😕",
"party.png": "🎉",
"emoji.png": "👍",
"emoji (1).png": "❤️",
"smile (1).png": "😄"
}
for filename, emoji in stickers_data.items():
filepath = os.path.join(self.stickers_folder, filename)
if not os.path.exists(filepath):
try:
# Create simple emoji images
img = Image.new('RGBA', (64, 64), (255, 255, 255, 0))
draw = ImageDraw.Draw(img)
# Try to use a font for emoji, fallback to colored rectangles
try:
font = ImageFont.truetype("seguiemj.ttf", 48)
draw.text((8, 8), emoji, font=font, fill="black")
except:
# Fallback: create colored circles/shapes
colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE']
color = colors[hash(filename) % len(colors)]
draw.ellipse([8, 8, 56, 56], fill=color)
draw.text((20, 20), emoji[:2], fill="white", font=ImageFont.load_default())
img.save(filepath, 'PNG')
print(f"✅ Created default sticker: {filename}")
except Exception as e:
print(f"⚠️ Error creating sticker {filename}: {e}")
# Legacy function to maintain compatibility
def open_thumbnail_editor(video_path):
"""Legacy function for backward compatibility"""
editor = ModernThumbnailEditor(video_path)
return editor
if __name__ == "__main__":
# Test the editor
test_video = "myvideo.mp4" # Replace with actual video path
if os.path.exists(test_video):
root = tk.Tk()
root.withdraw() # Hide main window
editor = ModernThumbnailEditor(test_video)
root.mainloop()
else:
print("Please provide a valid video file path")