ShortGenerator/thumbnail_editor.py

395 lines
16 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
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()