395 lines
16 KiB
Python
395 lines
16 KiB
Python
import os
|
||
import tkinter as tk
|
||
from tkinter import filedialog, simpledialog, colorchooser, messagebox
|
||
from moviepy import VideoFileClip
|
||
from PIL import Image, ImageTk, ImageDraw, ImageFont
|
||
|
||
# Enhanced Thumbnail Editor with Frame Slider + Default Emoji Pack + Text Adding
|
||
|
||
def open_thumbnail_editor(video_path):
|
||
try:
|
||
editor = tk.Toplevel()
|
||
editor.title("📸 Professional Thumbnail Editor")
|
||
editor.geometry("1200x800")
|
||
|
||
# Load video
|
||
print(f"📹 Loading video: {os.path.basename(video_path)}")
|
||
clip = VideoFileClip(video_path)
|
||
duration = int(clip.duration)
|
||
|
||
# Default emoji pack folder
|
||
stickers_folder = os.path.join(os.path.dirname(__file__), "stickers")
|
||
os.makedirs(stickers_folder, exist_ok=True)
|
||
|
||
# Create default stickers if folder is empty
|
||
create_default_stickers(stickers_folder)
|
||
|
||
# Main layout
|
||
main_frame = tk.Frame(editor)
|
||
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||
|
||
# Canvas setup (left side)
|
||
canvas_frame = tk.Frame(main_frame)
|
||
canvas_frame.pack(side="left", fill="both", expand=True)
|
||
|
||
tk.Label(canvas_frame, text="🎬 Thumbnail Preview", font=("Arial", 12, "bold")).pack()
|
||
|
||
canvas = tk.Canvas(canvas_frame, width=720, height=405, bg="black", relief="sunken", bd=2)
|
||
canvas.pack(pady=10)
|
||
|
||
# Track items for dragging
|
||
drag_data = {"item": None, "x": 0, "y": 0}
|
||
|
||
def capture_frame_at(time_sec):
|
||
try:
|
||
frame = clip.get_frame(max(0, min(time_sec, 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
|
||
|
||
# Displayed image
|
||
current_frame = capture_frame_at(duration // 2)
|
||
tk_frame_img = ImageTk.PhotoImage(current_frame)
|
||
image_item = canvas.create_image(360, 202, image=tk_frame_img)
|
||
canvas.image = tk_frame_img
|
||
|
||
# Items data
|
||
sticker_items = []
|
||
text_items = []
|
||
|
||
def update_canvas_frame(val):
|
||
nonlocal current_frame, tk_frame_img
|
||
try:
|
||
sec = float(val)
|
||
current_frame = capture_frame_at(sec)
|
||
tk_frame_img = ImageTk.PhotoImage(current_frame)
|
||
canvas.itemconfig(image_item, image=tk_frame_img)
|
||
canvas.image = tk_frame_img
|
||
except Exception as e:
|
||
print(f"⚠️ Error updating frame: {e}")
|
||
|
||
# Frame controls
|
||
controls_frame = tk.Frame(canvas_frame)
|
||
controls_frame.pack(fill="x", pady=5)
|
||
|
||
tk.Label(controls_frame, text="⏱️ Frame Time (seconds):").pack()
|
||
frame_slider = tk.Scale(controls_frame, from_=0, to=duration, orient="horizontal",
|
||
command=update_canvas_frame, length=600, resolution=0.1)
|
||
frame_slider.set(duration // 2)
|
||
frame_slider.pack(fill="x", pady=5)
|
||
|
||
# Tools panel (right side)
|
||
tools_frame = tk.Frame(main_frame, width=300, relief="groove", bd=2)
|
||
tools_frame.pack(side="right", fill="y", padx=(10, 0))
|
||
tools_frame.pack_propagate(False)
|
||
|
||
tk.Label(tools_frame, text="🛠️ Editing Tools", font=("Arial", 14, "bold")).pack(pady=10)
|
||
|
||
# Stickers section
|
||
stickers_label_frame = tk.LabelFrame(tools_frame, text="🎭 Stickers & Emojis", padx=10, pady=5)
|
||
stickers_label_frame.pack(fill="x", padx=10, pady=5)
|
||
|
||
# Create scrollable frame for stickers
|
||
stickers_canvas = tk.Canvas(stickers_label_frame, height=200)
|
||
stickers_scrollbar = tk.Scrollbar(stickers_label_frame, orient="vertical", command=stickers_canvas.yview)
|
||
stickers_scrollable_frame = tk.Frame(stickers_canvas)
|
||
|
||
stickers_scrollable_frame.bind(
|
||
"<Configure>",
|
||
lambda e: stickers_canvas.configure(scrollregion=stickers_canvas.bbox("all"))
|
||
)
|
||
|
||
stickers_canvas.create_window((0, 0), window=stickers_scrollable_frame, anchor="nw")
|
||
stickers_canvas.configure(yscrollcommand=stickers_scrollbar.set)
|
||
|
||
stickers_canvas.pack(side="left", fill="both", expand=True)
|
||
stickers_scrollbar.pack(side="right", fill="y")
|
||
|
||
def add_sticker(path):
|
||
try:
|
||
img = Image.open(path).convert("RGBA")
|
||
img.thumbnail((60, 60), Image.Resampling.LANCZOS)
|
||
tk_img = ImageTk.PhotoImage(img)
|
||
item = canvas.create_image(360, 200, image=tk_img)
|
||
|
||
# Keep reference to prevent garbage collection
|
||
if not hasattr(canvas, 'images'):
|
||
canvas.images = []
|
||
canvas.images.append(tk_img)
|
||
sticker_items.append((item, img))
|
||
print(f"✅ Added sticker: {os.path.basename(path)}")
|
||
except Exception as e:
|
||
print(f"⚠️ Failed to load sticker {path}: {e}")
|
||
|
||
# Load default stickers
|
||
sticker_count = 0
|
||
stickers_row_frame = None
|
||
|
||
for file in os.listdir(stickers_folder):
|
||
if file.lower().endswith(('.png', '.jpg', '.jpeg', '.gif')):
|
||
try:
|
||
if sticker_count % 4 == 0: # 4 stickers per row
|
||
stickers_row_frame = tk.Frame(stickers_scrollable_frame)
|
||
stickers_row_frame.pack(fill="x", pady=2)
|
||
|
||
btn_img = Image.open(os.path.join(stickers_folder, file)).convert("RGBA")
|
||
btn_img.thumbnail((40, 40), Image.Resampling.LANCZOS)
|
||
tk_btn_img = ImageTk.PhotoImage(btn_img)
|
||
|
||
b = tk.Button(stickers_row_frame, image=tk_btn_img,
|
||
command=lambda f=file: add_sticker(os.path.join(stickers_folder, f)))
|
||
b.image = tk_btn_img
|
||
b.pack(side="left", padx=2)
|
||
sticker_count += 1
|
||
except Exception as e:
|
||
print(f"⚠️ Failed to load sticker {file}: {e}")
|
||
|
||
# Add custom sticker button
|
||
tk.Button(stickers_label_frame, text="📁 Add Custom Sticker",
|
||
command=lambda: add_custom_sticker()).pack(pady=5)
|
||
|
||
def add_custom_sticker():
|
||
file_path = filedialog.askopenfilename(
|
||
title="Select Sticker Image",
|
||
filetypes=[("Image files", "*.png *.jpg *.jpeg *.gif"), ("All files", "*.*")]
|
||
)
|
||
if file_path:
|
||
add_sticker(file_path)
|
||
|
||
# Text section
|
||
text_label_frame = tk.LabelFrame(tools_frame, text="📝 Text Tools", padx=10, pady=5)
|
||
text_label_frame.pack(fill="x", padx=10, pady=5)
|
||
|
||
def add_text():
|
||
text_value = simpledialog.askstring("Add Text", "Enter text:")
|
||
if not text_value:
|
||
return
|
||
|
||
color_result = colorchooser.askcolor(title="Choose text color")
|
||
color = color_result[1] if color_result[1] else "white"
|
||
|
||
size = simpledialog.askinteger("Font size", "Enter font size:",
|
||
initialvalue=48, minvalue=8, maxvalue=200)
|
||
if not size:
|
||
size = 48
|
||
|
||
try:
|
||
item = canvas.create_text(360, 200, text=text_value, fill=color,
|
||
font=("Arial", size, "bold"), anchor="center")
|
||
text_items.append((item, text_value, color, size))
|
||
print(f"✅ Added text: '{text_value}'")
|
||
except Exception as e:
|
||
print(f"⚠️ Error adding text: {e}")
|
||
|
||
tk.Button(text_label_frame, text="➕ Add Text", command=add_text,
|
||
bg="#4CAF50", fg="white", font=("Arial", 10, "bold")).pack(pady=5, fill="x")
|
||
|
||
# Clear all button
|
||
def clear_all():
|
||
if messagebox.askyesno("Clear All", "Remove all stickers and text?"):
|
||
for item_id, _ in sticker_items + text_items:
|
||
canvas.delete(item_id)
|
||
sticker_items.clear()
|
||
text_items.clear()
|
||
print("🗑️ Cleared all items")
|
||
|
||
tk.Button(text_label_frame, text="🗑️ Clear All", command=clear_all,
|
||
bg="#F44336", fg="white").pack(pady=5, fill="x")
|
||
|
||
# Drag handling
|
||
def on_drag_start(event):
|
||
items = canvas.find_overlapping(event.x, event.y, event.x, event.y)
|
||
items = [i for i in items if i != image_item]
|
||
if not items:
|
||
return
|
||
item = items[-1] # topmost
|
||
drag_data["item"] = item
|
||
drag_data["x"] = event.x
|
||
drag_data["y"] = event.y
|
||
|
||
def on_drag_motion(event):
|
||
if drag_data["item"] is None:
|
||
return
|
||
dx = event.x - drag_data["x"]
|
||
dy = event.y - drag_data["y"]
|
||
canvas.move(drag_data["item"], dx, dy)
|
||
drag_data["x"] = event.x
|
||
drag_data["y"] = event.y
|
||
|
||
def on_drag_release(event):
|
||
drag_data["item"] = None
|
||
|
||
canvas.bind("<Button-1>", on_drag_start)
|
||
canvas.bind("<B1-Motion>", on_drag_motion)
|
||
canvas.bind("<ButtonRelease-1>", on_drag_release)
|
||
|
||
# Save section
|
||
save_frame = tk.LabelFrame(tools_frame, text="💾 Export Options", padx=10, pady=5)
|
||
save_frame.pack(fill="x", padx=10, pady=5)
|
||
|
||
def save_thumbnail():
|
||
try:
|
||
save_path = filedialog.asksaveasfilename(
|
||
defaultextension=".jpg",
|
||
filetypes=[("JPEG", "*.jpg"), ("PNG", "*.png"), ("All files", "*.*")],
|
||
title="Save Thumbnail As"
|
||
)
|
||
if not save_path:
|
||
return
|
||
|
||
print("💾 Generating high-quality thumbnail...")
|
||
|
||
# Get the current frame at full resolution
|
||
sec = float(frame_slider.get())
|
||
frame = Image.fromarray(clip.get_frame(sec)).convert("RGBA")
|
||
|
||
# Calculate scaling factors
|
||
canvas_w, canvas_h = 720, 405
|
||
scale_x = frame.width / canvas_w
|
||
scale_y = frame.height / canvas_h
|
||
|
||
# Add stickers
|
||
for item_id, sticker_img in sticker_items:
|
||
coords = canvas.coords(item_id)
|
||
if not coords:
|
||
continue
|
||
|
||
x, y = coords[0], coords[1]
|
||
# Convert canvas coordinates to frame coordinates
|
||
px = int(x * scale_x)
|
||
py = int(y * scale_y)
|
||
|
||
# Scale sticker size
|
||
target_w = int(sticker_img.width * scale_x)
|
||
target_h = int(sticker_img.height * scale_y)
|
||
|
||
if target_w > 0 and target_h > 0:
|
||
sticker_resized = sticker_img.resize((target_w, target_h), Image.Resampling.LANCZOS)
|
||
# Paste with alpha blending
|
||
frame.paste(sticker_resized, (px - target_w//2, py - target_h//2), sticker_resized)
|
||
|
||
# Add text
|
||
draw = ImageDraw.Draw(frame)
|
||
for item_id, text_value, color, font_size in text_items:
|
||
coords = canvas.coords(item_id)
|
||
if not coords:
|
||
continue
|
||
|
||
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)}")
|
||
|
||
tk.Button(save_frame, text="💾 Save Thumbnail", command=save_thumbnail,
|
||
bg="#2196F3", fg="white", font=("Arial", 12, "bold")).pack(pady=5, fill="x")
|
||
|
||
# Info label
|
||
info_text = f"📹 Video: {os.path.basename(video_path)}\n⏱️ Duration: {duration}s\n📐 Size: {clip.size[0]}x{clip.size[1]}"
|
||
tk.Label(save_frame, text=info_text, font=("Arial", 8), justify="left").pack(pady=5)
|
||
|
||
print(f"✅ Thumbnail editor loaded successfully!")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Error opening thumbnail editor: {e}")
|
||
messagebox.showerror("Error", f"Failed to open thumbnail editor:\n{str(e)}")
|
||
|
||
def create_default_stickers(stickers_folder):
|
||
"""Create some default emoji stickers if folder is empty"""
|
||
if os.listdir(stickers_folder):
|
||
return # Already has stickers
|
||
|
||
try:
|
||
from PIL import Image, ImageDraw
|
||
|
||
# Create simple emoji stickers
|
||
emojis = [
|
||
("😀", (255, 255, 0)), # Happy face
|
||
("❤️", (255, 0, 0)), # Heart
|
||
("👍", (255, 220, 177)), # Thumbs up
|
||
("🔥", (255, 100, 0)), # Fire
|
||
("⭐", (255, 215, 0)), # Star
|
||
("💯", (0, 255, 0)), # 100
|
||
]
|
||
|
||
for i, (emoji, color) in enumerate(emojis):
|
||
# Create a simple colored circle as placeholder
|
||
img = Image.new('RGBA', (80, 80), (0, 0, 0, 0))
|
||
draw = ImageDraw.Draw(img)
|
||
draw.ellipse([10, 10, 70, 70], fill=color)
|
||
|
||
# Save as PNG
|
||
img.save(os.path.join(stickers_folder, f"emoji_{i+1}.png"))
|
||
|
||
print("✅ Created default sticker pack")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ Could not create default stickers: {e}")
|
||
|
||
# Main execution
|
||
if __name__ == '__main__':
|
||
root = tk.Tk()
|
||
root.withdraw()
|
||
|
||
video_path = filedialog.askopenfilename(
|
||
title='Select a video file',
|
||
filetypes=[('Video files', '*.mp4 *.mov *.avi *.mkv'), ('All files', '*.*')]
|
||
)
|
||
|
||
if video_path:
|
||
try:
|
||
root.deiconify() # Show root window
|
||
root.title("Thumbnail Editor")
|
||
open_thumbnail_editor(video_path)
|
||
root.mainloop()
|
||
except Exception as e:
|
||
print(f"❌ Error: {e}")
|
||
messagebox.showerror("Error", f"Failed to start thumbnail editor:\n{str(e)}")
|
||
else:
|
||
print('No video selected.')
|
||
root.destroy()
|