feat: Implement professional thumbnail editor with video frame capture, emoji stickers, and text editing features

This commit is contained in:
klop51 2025-08-09 17:44:56 +02:00
parent bd55be0448
commit 8882aa1265
11 changed files with 1382 additions and 0 deletions

File diff suppressed because it is too large Load Diff

BIN
stickers/confused.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
stickers/emoji (1).png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
stickers/emoji.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
stickers/happy-face.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
stickers/laugh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
stickers/party.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
stickers/sad-face.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
stickers/smile (1).png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
stickers/smile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

394
thumbnail_editor.py Normal file
View File

@ -0,0 +1,394 @@
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()