#!/usr/bin/env python3 import os import sys import tkinter as tk from tkinter import filedialog, messagebox, ttk import threading from PIL import Image, ImageTk, ImageFilter # --- Constants & Styling --- BG_COLOR = "#0D1117" CARD_COLOR = "#21262D" ACCENT_COLOR = "#2F81F7" TEXT_COLOR = "#C9D1D9" TEXT_DIM = "#8B949E" SUCCESS_COLOR = "#238636" class SquareIconGUI: def __init__(self, root): self.root = root self.root.title("Standard - Square Icon Studio") self.root.geometry("1000x700") self.root.configure(bg=BG_COLOR) self.source_paths = [] self.output_dir = "src-tauri/icons" self.tk_orig: ImageTk.PhotoImage | None = None self.tk_after: ImageTk.PhotoImage | None = None self.is_processing = False self.setup_styles() self.setup_ui() def setup_styles(self): style = ttk.Style() style.theme_use("clam") style.configure("TFrame", background=BG_COLOR) style.configure("Card.TFrame", background=CARD_COLOR) style.configure("TLabel", background=BG_COLOR, foreground=TEXT_COLOR, font=("Segoe UI", 10)) style.configure("Header.TLabel", background=BG_COLOR, foreground=TEXT_COLOR, font=("Segoe UI Bold", 20)) style.configure("Sub.TLabel", background=BG_COLOR, foreground=TEXT_DIM, font=("Segoe UI", 11)) style.configure("Config.TLabelframe", background=BG_COLOR, foreground=TEXT_COLOR) style.configure("Config.TLabelframe.Label", background=BG_COLOR, foreground=TEXT_COLOR, font=("Segoe UI Bold", 10)) def setup_ui(self): # Sidebar for controls sidebar = ttk.Frame(self.root, padding=20) sidebar.pack(side="left", fill="y", padx=(0, 2)) ttk.Label(sidebar, text="Square Icon", style="Header.TLabel").pack(anchor="w") ttk.Label(sidebar, text="Studio Utility", style="Sub.TLabel").pack(anchor="w", pady=(0, 40)) # Main actions self.create_action_btn(sidebar, "Select Images", "Batch or single image", self.pick_images, ACCENT_COLOR) self.create_action_btn(sidebar, "Output Folder", f"Target: {self.output_dir}", self.pick_output, CARD_COLOR) # Config section config_frame = ttk.LabelFrame(sidebar, text=" Resolution ", padding=20, style="Config.TLabelframe") config_frame.pack(fill="x", pady=30) ttk.Label(config_frame, text="Target Size (px)").pack(anchor="w") self.size_var = tk.StringVar(value="1024") self.size_entry = tk.Entry(config_frame, textvariable=self.size_var, bg="#010409", fg=TEXT_COLOR, insertbackground=TEXT_COLOR, relief="flat", font=("Segoe UI", 11)) self.size_entry.pack(fill="x", pady=(5, 0), ipady=5) # Status self.status_var = tk.StringVar(value="Ready to process...") self.status_label = ttk.Label(sidebar, textvariable=self.status_var, style="Sub.TLabel", wraplength=250) self.status_label.pack(side="bottom", fill="x", pady=20) self.process_btn = tk.Button(sidebar, text="Generate Icons", font=("Segoe UI Bold", 12), bg=SUCCESS_COLOR, fg="white", relief="flat", cursor="hand2", command=self.start_processing, padx=20, pady=12) self.process_btn.pack(side="bottom", fill="x") # Preview Area preview_container = ttk.Frame(self.root, style="TFrame", padding=30) preview_container.pack(side="right", fill="both", expand=True) title_frame = ttk.Frame(preview_container) title_frame.pack(fill="x", pady=(0, 20)) ttk.Label(title_frame, text="Before vs After", font=("Segoe UI Bold", 16)).pack(side="left") # Preview Boxes self.preview_grid = ttk.Frame(preview_container) self.preview_grid.pack(fill="both", expand=True) # Before before_frame = tk.Frame(self.preview_grid, bg="#161b22", highlightbackground="#30363d", highlightthickness=1) before_frame.place(relx=0.05, rely=0.05, relwidth=0.42, relheight=0.8) self.before_img_lbl = tk.Label(before_frame, text="Original", bg="#161b22", fg=TEXT_DIM) self.before_img_lbl.pack(fill="both", expand=True) # After after_frame = tk.Frame(self.preview_grid, bg="#161b22", highlightbackground="#30363d", highlightthickness=1) after_frame.place(relx=0.53, rely=0.05, relwidth=0.42, relheight=0.8) self.after_img_lbl = tk.Label(after_frame, text="Result", bg="#161b22", fg=TEXT_DIM) self.after_img_lbl.pack(fill="both", expand=True) def create_action_btn(self, parent, text, sub, cmd, color): f = tk.Frame(parent, bg=BG_COLOR) f.pack(fill="x", pady=(0, 20)) b = tk.Button(f, text=text, command=cmd, bg=color, fg="white", font=("Segoe UI Bold", 11), relief="flat", cursor="hand2", pady=8) b.pack(fill="x") l = ttk.Label(f, text=sub, style="Sub.TLabel") l.pack(anchor="w", pady=(2, 0)) def pick_images(self): paths = filedialog.askopenfilenames(title="Select Source Images") if paths: self.source_paths = list(paths) self.status_var.set(f"Loaded {len(self.source_paths)} file(s)") self.update_preview(self.source_paths[0]) def pick_output(self): path = filedialog.askdirectory(title="Select Output Folder") if path: self.output_dir = path self.status_var.set(f"Output: {os.path.basename(path)}") def update_preview(self, path): try: img = Image.open(path).convert("RGBA") # Squared preview w, h = img.size s = max(w, h) square = Image.new("RGBA", (s, s), (0, 0, 0, 0)) square.paste(img, ((s-w)//2, (s-h)//2)) # Constraints max_p = 350 # Thumbnails using compatible Resampling resample = getattr(Image, 'Resampling', Image).LANCZOS img.thumbnail((max_p, max_p), resample=resample) self.tk_orig = ImageTk.PhotoImage(img) self.before_img_lbl.config(image=self.tk_orig, text="") square.thumbnail((max_p, max_p), resample=resample) self.tk_after = ImageTk.PhotoImage(square) self.after_img_lbl.config(image=self.tk_after, text="") except Exception as e: self.status_var.set(f"Preview fail: {e}") def start_processing(self): if not self.source_paths: return self.is_processing = True self.process_btn.config(state="disabled", text="Working...") threading.Thread(target=self.run_logic, daemon=True).start() def run_logic(self): try: size = int(self.size_var.get()) if not os.path.exists(self.output_dir): os.makedirs(self.output_dir) for i, p in enumerate(self.source_paths): self.status_var.set(f"Saving {i+1}/{len(self.source_paths)}...") make_square_icon(p, self.output_dir, size) self.root.after(0, lambda: messagebox.showinfo("Done", f"Processed {len(self.source_paths)} icons!")) except Exception as e: self.root.after(0, lambda: messagebox.showerror("Error", str(e))) finally: self.root.after(0, self.reset) def reset(self): self.is_processing = False self.process_btn.config(state="normal", text="Generate Icons") self.status_var.set("Ready for more.") def make_square_icon(source_path, output_dir="src-tauri/icons", target_size=1024): """ Core logic: Takes any image and turns it into a perfectly centered, transparent square icon (PNG and multi-res ICO). """ if not os.path.exists(source_path): return img = Image.open(source_path).convert("RGBA") width, height = img.size size = max(width, height) square = Image.new("RGBA", (size, size), (0, 0, 0, 0)) offset = ((size - width) // 2, (size - height) // 2) square.paste(img, offset) if not os.path.exists(output_dir): os.makedirs(output_dir) # Standardize naming base = os.path.splitext(os.path.basename(source_path))[0] # 1. Detect Content Bounding Box (Auto-Zoom logic) # This ensures small icons are recognizable by removing empty transparent space bbox = img.getbbox() if bbox: # Crop to the actual icon content content = img.crop(bbox) c_w, c_h = content.size # Make content square with padding c_size = max(c_w, c_h) square = Image.new("RGBA", (c_size, c_size), (0, 0, 0, 0)) square.paste(content, ((c_size - c_w) // 2, (c_size - c_h) // 2)) # 2. Resampling Constants resample_high = getattr(Image, 'Resampling', Image).LANCZOS # 3. Save Primary (High-Res) PNG icon_png = square.resize((target_size, target_size), resample_high) png_path = os.path.join(output_dir, f"{base}_icon.png" if "app-icon" not in base else "icon.png") icon_png.save(png_path) # 4. Generate High-Quality Multi-Res for .ico (Manually Sharpened) icon_sizes = [256, 128, 64, 48, 32, 16] icon_layers = [] for s in icon_sizes: # For small sizes, apply extra sharpening to ensure legibility layer = square.resize((s, s), resample_high) if s <= 32: # Apply subtle sharpening for small icons layer = layer.filter(ImageFilter.SHARPEN) # Boost contrast slightly if very small (improves legibility on dark/light bars) if s <= 16: layer = layer.filter(ImageFilter.EDGE_ENHANCE_MORE) icon_layers.append(layer) ico_path = os.path.join(output_dir, f"{base}_icon.ico" if "app-icon" not in base else "icon.ico") icon_layers[0].save(ico_path, sizes=[(l.width, l.height) for l in icon_layers], append_images=icon_layers[1:]) if __name__ == "__main__": if len(sys.argv) > 1: # CLI Mode make_square_icon(sys.argv[1]) else: # GUI Mode root = tk.Tk() app = SquareIconGUI(root) root.mainloop()