ShortGenerator/thumbnail_editor.py
klop51 caf64b9815 Refactor thumbnail editor layout for improved aesthetics
- Reduced top padding in canvas header for a more compact look.
- Adjusted canvas container to minimize empty space by changing expand behavior.
- Centered the canvas within its container for better positioning.
- Decreased bottom padding in timeline frame and label for a cleaner interface.
2025-08-10 16:56:58 +02:00

809 lines
36 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("1200x800") # Reduced window size to match smaller canvas
self.editor.minsize(1000, 700) # Reduced minimum size
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 (reduced top padding)
canvas_header = tk.Frame(parent, bg=self.colors['bg_secondary'])
canvas_header.pack(fill="x", padx=20, pady=(10, 5)) # Reduced from (20, 10) to (10, 5)
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 (reduced expand behavior to minimize empty space)
canvas_container = tk.Frame(parent, bg=self.colors['bg_tertiary'], relief="flat", bd=2)
canvas_container.pack(fill="x", padx=20, pady=(0, 5)) # Further reduced bottom padding from 10 to 5
# Calculate proper canvas size based on video dimensions
video_width, video_height = self.clip.size
aspect_ratio = video_width / video_height
# Set canvas size to maintain video aspect ratio (reduced to match video player)
max_width, max_height = 480, 360 # Smaller size for better interface fit
if aspect_ratio > max_width / max_height:
# Video is wider
canvas_width = max_width
canvas_height = int(max_width / aspect_ratio)
else:
# Video is taller
canvas_height = max_height
canvas_width = int(max_height * aspect_ratio)
# Modern canvas with proper video proportions (centered)
self.canvas = tk.Canvas(canvas_container, bg='#000000', highlightthickness=0,
relief="flat", bd=0, width=canvas_width, height=canvas_height)
self.canvas.pack(anchor="center", padx=5, pady=5) # Added anchor="center" for better positioning
# Store canvas dimensions for boundary checking
self.canvas_width = canvas_width
self.canvas_height = canvas_height
# Store center coordinates for consistent frame positioning
self.canvas_center_x = canvas_width // 2
self.canvas_center_y = canvas_height // 2
# Add visual border to show canvas boundaries
self.canvas.create_rectangle(2, 2, canvas_width-2, canvas_height-2,
outline='#333333', width=1, tags="border")
# 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 (reduced spacing)
timeline_frame = tk.Frame(parent, bg=self.colors['bg_secondary'])
timeline_frame.pack(fill="x", padx=20, pady=(0, 5)) # Further reduced bottom padding from 10 to 5
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, 5)) # Reduced from 10 to 5
# 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 and scrollbar"""
# Create main container
main_container = tk.Frame(parent, bg=self.colors['bg_secondary'])
main_container.pack(fill="both", expand=True, padx=10, pady=10)
# Create canvas for scrolling
self.controls_canvas = tk.Canvas(main_container, bg=self.colors['bg_secondary'],
highlightthickness=0, bd=0)
# Create scrollbar with modern styling
scrollbar = tk.Scrollbar(main_container, orient="vertical",
command=self.controls_canvas.yview,
bg=self.colors['bg_tertiary'],
activebackground=self.colors['accent_blue'],
troughcolor=self.colors['bg_primary'],
width=12, relief="flat", bd=0)
# Create scrollable frame
self.scrollable_frame = tk.Frame(self.controls_canvas, bg=self.colors['bg_secondary'])
# Create window in canvas first
canvas_window = self.controls_canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
# Configure scrolling with better update handling
def configure_scroll_region(event):
self.controls_canvas.configure(scrollregion=self.controls_canvas.bbox("all"))
self.scrollable_frame.bind("<Configure>", configure_scroll_region)
# Update canvas width when it's resized
def on_canvas_configure(event):
self.controls_canvas.itemconfig(canvas_window, width=event.width)
self.controls_canvas.bind("<Configure>", on_canvas_configure)
self.controls_canvas.configure(yscrollcommand=scrollbar.set)
# Pack scrollbar and canvas
scrollbar.pack(side="right", fill="y")
self.controls_canvas.pack(side="left", fill="both", expand=True)
# Bind mouse wheel to canvas
self.controls_canvas.bind("<MouseWheel>", self._on_mousewheel)
self.scrollable_frame.bind("<MouseWheel>", self._on_mousewheel)
# Also bind to main container for better coverage
main_container.bind("<MouseWheel>", self._on_mousewheel)
# Add padding container inside scrollable frame
scroll_frame = tk.Frame(self.scrollable_frame, bg=self.colors['bg_secondary'])
scroll_frame.pack(fill="both", expand=True, padx=10, pady=10)
# 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)
# Bind mousewheel to all widgets in the scroll frame
self.bind_mousewheel_to_widget(scroll_frame)
# Ensure canvas starts at top
self.controls_canvas.yview_moveto(0)
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")
# Replace limited buttons with spinbox for any size
size_control_frame = tk.Frame(style_frame, bg=self.colors['bg_secondary'])
size_control_frame.pack(fill="x", pady=(5, 0))
self.text_size_var = tk.IntVar(value=36)
size_spinbox = tk.Spinbox(size_control_frame, from_=8, to=200, increment=1,
textvariable=self.text_size_var, width=10,
font=self.fonts['body'], bg=self.colors['bg_tertiary'],
fg=self.colors['text_primary'], insertbackground=self.colors['text_primary'],
buttonbackground=self.colors['accent_blue'],
relief="flat", bd=1, highlightthickness=1,
highlightcolor=self.colors['accent_blue'],
command=self.update_selected_text_size)
size_spinbox.pack(side="left", padx=(0, 10))
# Bind spinbox value changes
self.text_size_var.trace('w', lambda *args: self.update_selected_text_size())
# Quick size buttons for common sizes
quick_sizes_frame = tk.Frame(size_control_frame, bg=self.colors['bg_secondary'])
quick_sizes_frame.pack(side="left")
tk.Label(quick_sizes_frame, text="Quick:", font=self.fonts['caption'],
bg=self.colors['bg_secondary'], fg=self.colors['text_muted']).pack(side="left", padx=(0, 5))
for size in [24, 36, 48, 64, 80]:
btn = tk.Button(quick_sizes_frame, text=str(size), font=self.fonts['caption'],
bg=self.colors['bg_tertiary'], fg=self.colors['text_primary'],
relief="flat", bd=0, padx=8, pady=3,
activebackground=self.colors['hover'],
command=lambda s=size: self.text_size_var.set(s))
btn.pack(side="left", padx=(0, 3))
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 _on_mousewheel(self, event):
"""Handle mouse wheel scrolling with cross-platform support"""
# Check if there's content to scroll
if self.controls_canvas.winfo_exists():
# Windows
if event.delta:
self.controls_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
# Linux
elif event.num == 4:
self.controls_canvas.yview_scroll(-1, "units")
elif event.num == 5:
self.controls_canvas.yview_scroll(1, "units")
def bind_mousewheel_to_widget(self, widget):
"""Recursively bind mousewheel to widget and all its children"""
widget.bind("<MouseWheel>", self._on_mousewheel)
for child in widget.winfo_children():
self.bind_mousewheel_to_widget(child)
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, sized for canvas"""
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 the smaller canvas
img.thumbnail((self.canvas_width, self.canvas_height), Image.Resampling.LANCZOS)
return img
except Exception as e:
print(f"⚠️ Error capturing frame: {e}")
# Create a placeholder image sized for canvas
img = Image.new('RGB', (self.canvas_width, self.canvas_height), color='black')
return img
def update_canvas_frame(self, time_sec):
"""Update canvas with frame at specific time, centered within bounds"""
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, centered within canvas bounds
self.canvas.delete("frame")
# Use stored center coordinates for consistent positioning
self.canvas.create_image(self.canvas_center_x, self.canvas_center_y,
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 (excluding video frame)"""
item = self.canvas.find_closest(event.x, event.y)[0]
# Prevent dragging the video frame or border
if item and item not in self.canvas.find_withtag("frame") and item not in self.canvas.find_withtag("border"):
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 with boundary constraints"""
if self.drag_data["item"]:
dx = event.x - self.drag_data["x"]
dy = event.y - self.drag_data["y"]
# Get current item position and bounds
item = self.drag_data["item"]
bbox = self.canvas.bbox(item)
if bbox:
x1, y1, x2, y2 = bbox
# Calculate new position
new_x1 = x1 + dx
new_y1 = y1 + dy
new_x2 = x2 + dx
new_y2 = y2 + dy
# Check boundaries and constrain movement
if new_x1 < 0:
dx = -x1
elif new_x2 > self.canvas_width:
dx = self.canvas_width - x2
if new_y1 < 0:
dy = -y1
elif new_y2 > self.canvas_height:
dy = self.canvas_height - y2
# Move item with constraints
self.canvas.move(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 within boundaries"""
text = simpledialog.askstring("Add Text", "Enter text:")
if text:
try:
size = self.text_size_var.get()
# Center text but ensure it's within canvas bounds
x = min(self.canvas_width // 2, self.canvas_width - 50)
y = min(self.canvas_height // 2, self.canvas_height - 30)
item = self.canvas.create_text(x, y, 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 update_selected_text_size(self):
"""Update the size of selected text items"""
try:
new_size = self.text_size_var.get()
# Update all selected text items or the last added text
for i, (item_type, item_id, text, color, size) in enumerate(self.canvas_items):
if item_type == "text":
# Update the font size
current_font = self.canvas.itemcget(item_id, "font")
if isinstance(current_font, str):
font_parts = current_font.split()
if len(font_parts) >= 2:
font_family = font_parts[0]
font_style = font_parts[2] if len(font_parts) > 2 else "bold"
new_font = (font_family, new_size, font_style)
else:
new_font = ("Arial", new_size, "bold")
else:
new_font = ("Arial", new_size, "bold")
self.canvas.itemconfig(item_id, font=new_font)
# Update the stored size in canvas_items
self.canvas_items[i] = (item_type, item_id, text, color, new_size)
except Exception as e:
print(f"⚠️ Error updating text size: {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 within boundaries"""
try:
img = Image.open(path).convert("RGBA")
img.thumbnail((60, 60), Image.Resampling.LANCZOS)
tk_img = ImageTk.PhotoImage(img)
# Place sticker within canvas bounds
x = min(self.canvas_width // 2, self.canvas_width - 30)
y = min(self.canvas_height // 2, self.canvas_height - 30)
item = self.canvas.create_image(x, y, 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")