|
| 1 | +import ttkbootstrap as tb |
| 2 | +from ttkbootstrap.constants import * |
| 3 | +from tkinter import filedialog, messagebox |
| 4 | +from tkinter import ttk # For Combobox |
| 5 | +from PIL import Image, ImageDraw, ImageFilter, ImageTk |
| 6 | +import numpy as np |
| 7 | +import random |
| 8 | +import time |
| 9 | + |
| 10 | +# ========================= |
| 11 | +# UTILITY FUNCTIONS |
| 12 | +# ========================= |
| 13 | +def add_noise(img, intensity=10): |
| 14 | + """Add random noise to an image.""" |
| 15 | + arr = np.array(img) |
| 16 | + noise = np.random.randint(-intensity, intensity + 1, arr.shape, "int16") |
| 17 | + noisy = np.clip(arr + noise, 0, 255).astype("uint8") |
| 18 | + return Image.fromarray(noisy) |
| 19 | + |
| 20 | +def generate_gradient(size): |
| 21 | + """Generate a simple vertical gradient with optional noise.""" |
| 22 | + w, h = size |
| 23 | + top_color = ( |
| 24 | + random.randint(50, 255), |
| 25 | + random.randint(50, 255), |
| 26 | + random.randint(50, 255) |
| 27 | + ) |
| 28 | + bottom_color = ( |
| 29 | + random.randint(50, 255), |
| 30 | + random.randint(50, 255), |
| 31 | + random.randint(50, 255) |
| 32 | + ) |
| 33 | + img = Image.new("RGB", size) |
| 34 | + draw = ImageDraw.Draw(img) |
| 35 | + for y in range(h): |
| 36 | + r = int(top_color[0] + (bottom_color[0] - top_color[0]) * y / h) |
| 37 | + g = int(top_color[1] + (bottom_color[1] - top_color[1]) * y / h) |
| 38 | + b = int(top_color[2] + (bottom_color[2] - top_color[2]) * y / h) |
| 39 | + draw.line((0, y, w, y), fill=(r, g, b)) |
| 40 | + return add_noise(img, intensity=5) |
| 41 | + |
| 42 | +def generate_texture(size): |
| 43 | + """Generate a random texture overlay.""" |
| 44 | + img = Image.new("RGBA", size) |
| 45 | + draw = ImageDraw.Draw(img) |
| 46 | + for _ in range(size[0] * size[1] // 50): |
| 47 | + x = random.randint(0, size[0] - 1) |
| 48 | + y = random.randint(0, size[1] - 1) |
| 49 | + color = ( |
| 50 | + random.randint(100, 255), |
| 51 | + random.randint(100, 255), |
| 52 | + random.randint(100, 255), |
| 53 | + random.randint(20, 50) |
| 54 | + ) |
| 55 | + draw.point((x, y), fill=color) |
| 56 | + return img |
| 57 | + |
| 58 | +def generate_background(size): |
| 59 | + """Combine gradient and texture to produce the final background.""" |
| 60 | + base = generate_gradient(size) |
| 61 | + texture = generate_texture(size) |
| 62 | + base.paste(texture, (0, 0), texture) |
| 63 | + return base |
| 64 | + |
| 65 | +# ========================= |
| 66 | +# MAIN APP CLASS |
| 67 | +# ========================= |
| 68 | +class BackgroundGenerator: |
| 69 | + def __init__(self, root): |
| 70 | + self.root = root |
| 71 | + root.title("Background Generator Tool") |
| 72 | + root.geometry("1200x700") |
| 73 | + root.resizable(True, True) |
| 74 | + |
| 75 | + self.resolutions = { |
| 76 | + "HD (1280x720)": (1280, 720), |
| 77 | + "Full HD (1920x1080)": (1920, 1080), |
| 78 | + "2K (2560x1440)": (2560, 1440), |
| 79 | + "4K (3840x2160)": (3840, 2160), |
| 80 | + "Ultra HD (6000x3375)": (6000, 3375) |
| 81 | + } |
| 82 | + |
| 83 | + self.state = { |
| 84 | + "size": (1920, 1080), |
| 85 | + "bg": generate_background((1920, 1080)) |
| 86 | + } |
| 87 | + |
| 88 | + # LEFT PANEL |
| 89 | + left = tb.Frame(root, width=300, bootstyle="secondary") |
| 90 | + left.pack(side="left", fill="y", padx=10, pady=10) |
| 91 | + |
| 92 | + # Orientation Selector |
| 93 | + tb.Label(left, text="Orientation:", font=("Segoe UI", 10, "bold")).pack(anchor="w", pady=(5, 2)) |
| 94 | + self.orient_combo = ttk.Combobox(left, values=["Landscape", "Portrait"], state="readonly") |
| 95 | + self.orient_combo.current(0) # Default to Landscape |
| 96 | + self.orient_combo.pack(fill=X, pady=(0, 10)) |
| 97 | + |
| 98 | + # Resolution Selector |
| 99 | + tb.Label(left, text="Export Resolution:", font=("Segoe UI", 10, "bold")).pack(anchor="w", pady=(5, 2)) |
| 100 | + self.res_var = tb.StringVar(value="Full HD (1920x1080)") |
| 101 | + tb.OptionMenu(left, self.res_var, *self.resolutions.keys(), bootstyle="secondary").pack(fill=X, pady=(0, 10)) |
| 102 | + |
| 103 | + # Buttons |
| 104 | + tb.Button(left, text="🔄 Generate Background", bootstyle=INFO, command=self.generate_bg).pack(fill=X, pady=5) |
| 105 | + tb.Button(left, text="💾 Export", bootstyle=SUCCESS, command=self.save_bg).pack(fill=X, pady=5) |
| 106 | + |
| 107 | + # RIGHT PANEL - Canvas Preview |
| 108 | + self.canvas_frame = tb.Frame(root) |
| 109 | + self.canvas_frame.pack(side="right", fill="both", expand=True, padx=10, pady=10) |
| 110 | + self.canvas = tb.Canvas(self.canvas_frame, bg="black", highlightthickness=0) |
| 111 | + self.canvas.pack(expand=True, fill="both") |
| 112 | + self.canvas_frame.bind("<Configure>", self.on_resize) |
| 113 | + |
| 114 | + self.update_preview() |
| 115 | + |
| 116 | + # ========================= |
| 117 | + # EVENT METHODS |
| 118 | + # ========================= |
| 119 | + def on_resize(self, event): |
| 120 | + self.update_preview() |
| 121 | + |
| 122 | + # ========================= |
| 123 | + # STATE HANDLERS |
| 124 | + # ========================= |
| 125 | + def generate_bg(self): |
| 126 | + w, h = self.resolutions.get(self.res_var.get(), (1920, 1080)) |
| 127 | + orientation = self.orient_combo.get() |
| 128 | + if orientation == "Portrait": |
| 129 | + w, h = h, w |
| 130 | + self.state["size"] = (w, h) |
| 131 | + self.state["bg"] = generate_background((w, h)) |
| 132 | + self.update_preview() |
| 133 | + |
| 134 | + def update_preview(self): |
| 135 | + img = self.state["bg"].copy() |
| 136 | + canvas_w = self.canvas.winfo_width() |
| 137 | + canvas_h = self.canvas.winfo_height() |
| 138 | + if canvas_w > 0 and canvas_h > 0: |
| 139 | + img_w, img_h = img.size |
| 140 | + ratio = min(canvas_w / img_w, canvas_h / img_h) |
| 141 | + new_w = max(1, int(img_w * ratio)) |
| 142 | + new_h = max(1, int(img_h * ratio)) |
| 143 | + preview = img.resize((new_w, new_h)) |
| 144 | + self.tk_img = ImageTk.PhotoImage(preview) |
| 145 | + self.canvas.delete("all") |
| 146 | + self.canvas.create_image(canvas_w // 2, canvas_h // 2, image=self.tk_img) |
| 147 | + |
| 148 | + def save_bg(self): |
| 149 | + # Get current background info |
| 150 | + w, h = self.state["size"] |
| 151 | + orientation = self.orient_combo.get() |
| 152 | + |
| 153 | + # Generate automatic filename |
| 154 | + timestamp = int(time.time()) # simple timestamp |
| 155 | + filename = f"background_{w}x{h}_{orientation}_{timestamp}.png" |
| 156 | + |
| 157 | + # Ask user where to save, default filename pre-filled |
| 158 | + f = filedialog.asksaveasfilename( |
| 159 | + initialfile=filename, |
| 160 | + defaultextension=".png", |
| 161 | + filetypes=[("PNG Image", "*.png"), ("JPEG Image", "*.jpg")] |
| 162 | + ) |
| 163 | + |
| 164 | + if f: |
| 165 | + ext = f.split('.')[-1].lower() |
| 166 | + if ext in ["jpg", "jpeg"]: |
| 167 | + self.state["bg"].save(f, quality=85) |
| 168 | + else: |
| 169 | + self.state["bg"].save(f, optimize=True) |
| 170 | + messagebox.showinfo("Success", f"Background saved as {f}!") |
| 171 | + |
| 172 | +# ========================= |
| 173 | +# RUN APP |
| 174 | +# ========================= |
| 175 | +if __name__ == "__main__": |
| 176 | + root = tb.Window(themename="darkly") |
| 177 | + app = BackgroundGenerator(root) |
| 178 | + root.mainloop() |
0 commit comments