- 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
627 lines
27 KiB
Python
627 lines
27 KiB
Python
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")
|