|
| 1 | +import tkinter as tk |
| 2 | +from tkinter import messagebox, filedialog |
| 3 | +import ttkbootstrap as tb |
| 4 | +from ttkbootstrap.widgets.scrolled import ScrolledFrame |
| 5 | +import threading |
| 6 | +import os |
| 7 | +import sys |
| 8 | +from PIL import Image, ImageDraw, ImageTk |
| 9 | +import random |
| 10 | + |
| 11 | +# Optional for PDF export |
| 12 | +from reportlab.lib.pagesizes import A4 |
| 13 | +from reportlab.pdfgen import canvas |
| 14 | +from reportlab.lib.utils import ImageReader |
| 15 | + |
| 16 | +# Optional for Stable Diffusion API |
| 17 | +import requests |
| 18 | +import base64 |
| 19 | +from io import BytesIO |
| 20 | + |
| 21 | +# =================== APP INFO =================== |
| 22 | +APP_NAME = "AI Coloring Book Maker" |
| 23 | +VERSION = "1.0" |
| 24 | + |
| 25 | +# =================== UTIL =================== |
| 26 | +def resource_path(file_name): |
| 27 | + base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__))) |
| 28 | + return os.path.join(base_path, file_name) |
| 29 | + |
| 30 | +def show_error(title, msg): |
| 31 | + messagebox.showerror(title, msg) |
| 32 | + |
| 33 | +def show_info(title, msg): |
| 34 | + messagebox.showinfo(title, msg) |
| 35 | + |
| 36 | +# =================== APP =================== |
| 37 | +app = tb.Window(f"{APP_NAME} v{VERSION}", themename="superhero", size=(1300, 720)) |
| 38 | +app.grid_columnconfigure(0, weight=1) |
| 39 | +app.grid_columnconfigure(1, weight=2) |
| 40 | +app.grid_rowconfigure(1, weight=1) |
| 41 | + |
| 42 | +try: |
| 43 | + app.iconbitmap(resource_path("icon.ico")) |
| 44 | +except Exception: |
| 45 | + pass |
| 46 | + |
| 47 | +generated_images = [] |
| 48 | +selected_images = set() |
| 49 | +thumbnail_labels = [] |
| 50 | + |
| 51 | +# ================= LEFT PANEL ================= |
| 52 | +left_panel = tb.Labelframe(app, text="AI Prompt Settings", padding=15) |
| 53 | +left_panel.grid(row=0, column=0, rowspan=2, sticky="nsew", padx=10, pady=10) |
| 54 | + |
| 55 | +tb.Label(left_panel, text="Prompt / Theme").pack(anchor="w") |
| 56 | +prompt_entry = tb.Entry(left_panel) |
| 57 | +prompt_entry.pack(fill="x", pady=5) |
| 58 | + |
| 59 | +tb.Label(left_panel, text="Art Style").pack(anchor="w") |
| 60 | +style_combo = tb.Combobox( |
| 61 | + left_panel, |
| 62 | + values=[ |
| 63 | + "Cartoon Coloring Page", |
| 64 | + "Cute Kawaii Line Art", |
| 65 | + "Kids Coloring Book", |
| 66 | + "Mandala Line Art", |
| 67 | + "Simple Thick Outline" |
| 68 | + ], |
| 69 | + state="readonly" |
| 70 | +) |
| 71 | +style_combo.set("Cartoon Coloring Page") |
| 72 | +style_combo.pack(fill="x", pady=5) |
| 73 | + |
| 74 | +tb.Label(left_panel, text="Image Size").pack(anchor="w") |
| 75 | +size_combo = tb.Combobox( |
| 76 | + left_panel, |
| 77 | + values=["256x256", "512x512", "768x768", "1024x1024"], |
| 78 | + state="readonly" |
| 79 | +) |
| 80 | +size_combo.set("512x512") |
| 81 | +size_combo.pack(fill="x", pady=5) |
| 82 | + |
| 83 | +tb.Label(left_panel, text="Batch Count (1–10)").pack(anchor="w") |
| 84 | +batch_spin = tb.Spinbox(left_panel, from_=1, to=10, width=5) |
| 85 | +batch_spin.set(4) |
| 86 | +batch_spin.pack(anchor="w", pady=5) |
| 87 | + |
| 88 | +generate_btn = tb.Button( |
| 89 | + left_panel, |
| 90 | + text="Generate Images", |
| 91 | + bootstyle="success", |
| 92 | + width=25 |
| 93 | +) |
| 94 | +generate_btn.pack(pady=15) |
| 95 | + |
| 96 | +save_btn = tb.Button( |
| 97 | + left_panel, |
| 98 | + text="Save Selected Images", |
| 99 | + bootstyle="primary", |
| 100 | + width=25 |
| 101 | +) |
| 102 | +save_btn.pack(pady=5) |
| 103 | + |
| 104 | +export_pdf_btn = tb.Button( |
| 105 | + left_panel, |
| 106 | + text="Export Coloring Book (PDF)", |
| 107 | + bootstyle="warning", |
| 108 | + width=25 |
| 109 | +) |
| 110 | +export_pdf_btn.pack(pady=5) |
| 111 | + |
| 112 | +# ================= RIGHT PANEL ================= |
| 113 | +preview_panel = tb.Labelframe(app, text="Thumbnail Preview", padding=10) |
| 114 | +preview_panel.grid(row=0, column=1, rowspan=2, sticky="nsew", padx=10, pady=10) |
| 115 | +preview_panel.grid_rowconfigure(0, weight=1) |
| 116 | +preview_panel.grid_columnconfigure(0, weight=1) |
| 117 | + |
| 118 | +scroll = ScrolledFrame(preview_panel, autohide=True) |
| 119 | +scroll.pack(fill="both", expand=True) |
| 120 | + |
| 121 | +# ================= AI PLACEHOLDER ================= |
| 122 | +def fake_ai_generate(size): |
| 123 | + img = Image.new("RGB", size, "white") |
| 124 | + d = ImageDraw.Draw(img) |
| 125 | + for _ in range(15): |
| 126 | + x1 = random.randint(0, size[0]) |
| 127 | + y1 = random.randint(0, size[1]) |
| 128 | + x2 = random.randint(0, size[0]) |
| 129 | + y2 = random.randint(0, size[1]) |
| 130 | + d.line((x1, y1, x2, y2), fill="black", width=3) |
| 131 | + return img |
| 132 | + |
| 133 | +# Optional: Stable Diffusion generator |
| 134 | +def stable_diffusion_generate(prompt, size): |
| 135 | + # Replace URL with your SD WebUI API endpoint |
| 136 | + try: |
| 137 | + payload = { |
| 138 | + "prompt": f"{prompt}, black and white coloring page, line art, no shading", |
| 139 | + "negative_prompt": "color, gray, shadows, text", |
| 140 | + "steps": 25, |
| 141 | + "width": size[0], |
| 142 | + "height": size[1], |
| 143 | + "cfg_scale": 7, |
| 144 | + "sampler_name": "Euler a" |
| 145 | + } |
| 146 | + r = requests.post("http://127.0.0.1:7860/sdapi/v1/txt2img", json=payload) |
| 147 | + r.raise_for_status() |
| 148 | + img_data = base64.b64decode(r.json()["images"][0]) |
| 149 | + return Image.open(BytesIO(img_data)).convert("RGB") |
| 150 | + except Exception as e: |
| 151 | + show_error("Stable Diffusion Error", str(e)) |
| 152 | + return fake_ai_generate(size) |
| 153 | + |
| 154 | +# ================= GENERATION ================= |
| 155 | +def get_column_count(): |
| 156 | + width = scroll.winfo_width() |
| 157 | + return max(1, width // 210) |
| 158 | + |
| 159 | +def refresh_thumbnail_grid(event=None): |
| 160 | + cols = get_column_count() |
| 161 | + for idx, lbl in enumerate(thumbnail_labels): |
| 162 | + lbl.grid(row=idx // cols, column=idx % cols, padx=5, pady=5) |
| 163 | + |
| 164 | +def generate_images(): |
| 165 | + prompt = prompt_entry.get().strip() |
| 166 | + if not prompt: |
| 167 | + show_error("Input Error", "Please enter a prompt or theme.") |
| 168 | + return |
| 169 | + |
| 170 | + for widget in scroll.winfo_children(): |
| 171 | + widget.destroy() |
| 172 | + |
| 173 | + generated_images.clear() |
| 174 | + selected_images.clear() |
| 175 | + thumbnail_labels.clear() |
| 176 | + |
| 177 | + size = tuple(map(int, size_combo.get().split("x"))) |
| 178 | + batch = int(batch_spin.get()) |
| 179 | + |
| 180 | + for i in range(batch): |
| 181 | + # Replace fake_ai_generate with stable_diffusion_generate if SD is available |
| 182 | + img = fake_ai_generate(size) |
| 183 | + # img = stable_diffusion_generate(prompt, size) |
| 184 | + |
| 185 | + generated_images.append(img) |
| 186 | + |
| 187 | + thumb = img.copy() |
| 188 | + thumb.thumbnail((200, 200)) |
| 189 | + tk_img = ImageTk.PhotoImage(thumb) |
| 190 | + |
| 191 | + lbl = tk.Label(scroll, image=tk_img, borderwidth=2, relief="ridge") |
| 192 | + lbl.image = tk_img |
| 193 | + thumbnail_labels.append(lbl) |
| 194 | + |
| 195 | + def toggle_select(event, index=i, label=lbl): |
| 196 | + if index in selected_images: |
| 197 | + selected_images.remove(index) |
| 198 | + label.config(relief="ridge", borderwidth=2) |
| 199 | + else: |
| 200 | + selected_images.add(index) |
| 201 | + label.config(relief="solid", borderwidth=4) |
| 202 | + |
| 203 | + lbl.bind("<Button-1>", toggle_select) |
| 204 | + |
| 205 | + refresh_thumbnail_grid() |
| 206 | + |
| 207 | +scroll.bind("<Configure>", refresh_thumbnail_grid) |
| 208 | + |
| 209 | +# ================= SAVE ================= |
| 210 | +def save_selected(): |
| 211 | + if not selected_images: |
| 212 | + show_error("Save Error", "No images selected.") |
| 213 | + return |
| 214 | + |
| 215 | + folder = filedialog.askdirectory(title="Select Save Folder") |
| 216 | + if not folder: |
| 217 | + return |
| 218 | + |
| 219 | + for i in selected_images: |
| 220 | + path = os.path.join(folder, f"coloring_page_{i+1}.png") |
| 221 | + img_300 = generated_images[i].resize((2480, 3508), Image.LANCZOS) |
| 222 | + img_300.save(path, dpi=(300, 300)) |
| 223 | + |
| 224 | + show_info("Saved", f"{len(selected_images)} images saved successfully.") |
| 225 | + |
| 226 | +# ================= PDF EXPORT ================= |
| 227 | +def export_pdf(): |
| 228 | + if not selected_images: |
| 229 | + show_error("Export Error", "No images selected.") |
| 230 | + return |
| 231 | + |
| 232 | + path = filedialog.asksaveasfilename( |
| 233 | + defaultextension=".pdf", |
| 234 | + filetypes=[("PDF Files", "*.pdf")] |
| 235 | + ) |
| 236 | + if not path: |
| 237 | + return |
| 238 | + |
| 239 | + c = canvas.Canvas(path, pagesize=A4) |
| 240 | + page_width, page_height = A4 |
| 241 | + |
| 242 | + for idx in selected_images: |
| 243 | + img = generated_images[idx] |
| 244 | + img_300 = img.resize((2480, 3508), Image.LANCZOS) |
| 245 | + img_reader = ImageReader(img_300) |
| 246 | + c.drawImage( |
| 247 | + img_reader, |
| 248 | + 0, |
| 249 | + 0, |
| 250 | + width=page_width, |
| 251 | + height=page_height, |
| 252 | + preserveAspectRatio=True, |
| 253 | + anchor='c' |
| 254 | + ) |
| 255 | + c.showPage() |
| 256 | + |
| 257 | + c.save() |
| 258 | + show_info("PDF Exported", "Coloring book PDF created successfully.") |
| 259 | + |
| 260 | +generate_btn.config(command=lambda: threading.Thread(target=generate_images, daemon=True).start()) |
| 261 | +save_btn.config(command=save_selected) |
| 262 | +export_pdf_btn.config(command=export_pdf) |
| 263 | + |
| 264 | +# ================= ABOUT ================= |
| 265 | +def show_about(): |
| 266 | + win = tb.Toplevel(app) |
| 267 | + win.title("About") |
| 268 | + win.geometry("500x400") |
| 269 | + win.resizable(False, False) |
| 270 | + frame = tb.Frame(win, padding=15) |
| 271 | + frame.pack(fill="both", expand=True) |
| 272 | + |
| 273 | + tb.Label(frame, text=APP_NAME, font=("Segoe UI", 14, "bold")).pack(pady=5) |
| 274 | + tb.Label(frame, text=f"Version {VERSION}").pack() |
| 275 | + |
| 276 | + tb.Label( |
| 277 | + frame, |
| 278 | + text=( |
| 279 | + "AI Coloring Book Maker is a standalone desktop application\n" |
| 280 | + "that generates printable black-and-white coloring pages\n" |
| 281 | + "using AI-powered image generation.\n\n" |
| 282 | + "Key Features:\n" |
| 283 | + "- Custom prompts\n" |
| 284 | + "- Batch image generation\n" |
| 285 | + "- Thumbnail selection\n" |
| 286 | + "- Print-ready line art\n" |
| 287 | + "- 300 DPI PDF export\n" |
| 288 | + "- Fully offline executable\n" |
| 289 | + ), |
| 290 | + justify="left", |
| 291 | + wraplength=460 |
| 292 | + ).pack(pady=10) |
| 293 | + |
| 294 | + tb.Button(frame, text="Close", bootstyle="danger", command=win.destroy).pack(pady=10) |
| 295 | + |
| 296 | +menu = tk.Menu(app) |
| 297 | +app.config(menu=menu) |
| 298 | +help_menu = tk.Menu(menu, tearoff=0) |
| 299 | +menu.add_cascade(label="Help", menu=help_menu) |
| 300 | +help_menu.add_command(label="About", command=show_about) |
| 301 | + |
| 302 | +app.mainloop() |
0 commit comments