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("", self.on_canvas_click) self.canvas.bind("", self.on_canvas_drag) self.canvas.bind("", 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_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("", 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("", self._on_mousewheel) self.scrollable_frame.bind("", self._on_mousewheel) # Also bind to main container for better coverage main_container.bind("", 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("", 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("", on_enter) widget.bind("", 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")