Created
July 26, 2026 03:52
-
-
Save cookieukw/f91191577455b5074ff5369e7aa2a99a to your computer and use it in GitHub Desktop.
A simple utility to overlay an image (watermark) onto another image.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| import os | |
| import tkinter as tk | |
| from tkinter import ttk, filedialog, messagebox | |
| from PIL import Image, ImageTk | |
| PREVIEW_MAX_W = 1100 | |
| PREVIEW_MAX_H = 760 | |
| def clamp(value, low, high): | |
| return max(low, min(high, value)) | |
| def hex_from_rgb(rgb): | |
| return "#{:02x}{:02x}{:02x}".format(*rgb) | |
| def apply_opacity(img, opacity): | |
| opacity = max(0.0, min(1.0, float(opacity))) | |
| if opacity >= 1.0: | |
| return img | |
| img = img.convert("RGBA") | |
| alpha = img.getchannel("A") | |
| alpha = alpha.point(lambda p: int(p * opacity)) | |
| img.putalpha(alpha) | |
| return img | |
| def remove_background_by_color(img, target_rgb, tolerance): | |
| """ | |
| Faz color-to-alpha simples: pixels próximos de target_rgb viram transparentes. | |
| tolerance: distância euclidiana em RGB. | |
| """ | |
| img = img.convert("RGBA") | |
| px = img.load() | |
| tr, tg, tb = target_rgb | |
| tol2 = int(tolerance) * int(tolerance) | |
| for y in range(img.height): | |
| for x in range(img.width): | |
| r, g, b, a = px[x, y] | |
| dr = r - tr | |
| dg = g - tg | |
| db = b - tb | |
| if (dr * dr + dg * dg + db * db) <= tol2: | |
| px[x, y] = (r, g, b, 0) | |
| return img | |
| class WatermarkApp: | |
| def __init__(self, root): | |
| self.root = root | |
| self.root.title("Watermark GUI") | |
| self.root.geometry("1380x900") | |
| self.root.minsize(1180, 780) | |
| self.base_path = None | |
| self.logo_path = None | |
| self.base_original = None | |
| self.logo_original = None | |
| self.preview_base = None | |
| self.preview_ratio = 1.0 | |
| self.logo_x_preview = 20 | |
| self.logo_y_preview = 20 | |
| self.preview_logo_cache = None | |
| self.dragging = False | |
| self.drag_offset_x = 0 | |
| self.drag_offset_y = 0 | |
| self.pick_color_mode = False | |
| self.bg_color = None | |
| self._syncing_fields = False | |
| self._build_style() | |
| self._build_ui() | |
| self.root.bind_all("<Left>", lambda e: self.nudge(-1, 0)) | |
| self.root.bind_all("<Right>", lambda e: self.nudge(1, 0)) | |
| self.root.bind_all("<Up>", lambda e: self.nudge(0, -1)) | |
| self.root.bind_all("<Down>", lambda e: self.nudge(0, 1)) | |
| self.root.bind_all("<Shift-Left>", lambda e: self.nudge(-10, 0)) | |
| self.root.bind_all("<Shift-Right>", lambda e: self.nudge(10, 0)) | |
| self.root.bind_all("<Shift-Up>", lambda e: self.nudge(0, -10)) | |
| self.root.bind_all("<Shift-Down>", lambda e: self.nudge(0, 10)) | |
| def _build_style(self): | |
| try: | |
| style = ttk.Style() | |
| style.theme_use("clam") | |
| except Exception: | |
| pass | |
| def _build_ui(self): | |
| outer = ttk.Frame(self.root, padding=10) | |
| outer.pack(fill="both", expand=True) | |
| # Container esquerdo com largura fixa para abrigar o Canvas e a Scrollbar | |
| left_container = ttk.Frame(outer, width=360) | |
| left_container.pack(side="left", fill="y", padx=(0, 12)) | |
| left_container.pack_propagate(False) | |
| # Configuração do Canvas e Scrollbar | |
| self.left_canvas = tk.Canvas(left_container, highlightthickness=0) | |
| left_scrollbar = ttk.Scrollbar(left_container, orient="vertical", command=self.left_canvas.yview) | |
| left = ttk.Frame(self.left_canvas) | |
| left.bind( | |
| "<Configure>", | |
| lambda e: self.left_canvas.configure(scrollregion=self.left_canvas.bbox("all")) | |
| ) | |
| # Largura interna ajustada para acomodar a barra de rolagem (360 total - ~20 da barra) | |
| self.left_canvas.create_window((0, 0), window=left, anchor="nw", width=335) | |
| self.left_canvas.configure(yscrollcommand=left_scrollbar.set) | |
| self.left_canvas.pack(side="left", fill="both", expand=True) | |
| left_scrollbar.pack(side="right", fill="y") | |
| # Bindings de rolagem do mouse (Suporte cross-platform, incluindo X11/Wayland) | |
| def _on_mousewheel(event): | |
| if event.delta: | |
| self.left_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") | |
| def _on_linux_scroll_up(event): | |
| self.left_canvas.yview_scroll(-1, "units") | |
| def _on_linux_scroll_down(event): | |
| self.left_canvas.yview_scroll(1, "units") | |
| self.left_canvas.bind_all("<MouseWheel>", _on_mousewheel) | |
| self.left_canvas.bind_all("<Button-4>", _on_linux_scroll_up) | |
| self.left_canvas.bind_all("<Button-5>", _on_linux_scroll_down) | |
| right = ttk.Frame(outer) | |
| right.pack(side="right", fill="both", expand=True) | |
| # Arquivos | |
| box_files = ttk.LabelFrame(left, text="Arquivos", padding=10) | |
| box_files.pack(fill="x", pady=(0, 10)) | |
| ttk.Button(box_files, text="Abrir imagem", command=self.open_base).pack(fill="x") | |
| ttk.Button(box_files, text="Abrir logo", command=self.open_logo).pack(fill="x", pady=(6, 0)) | |
| self.base_label = ttk.Label(box_files, text="Imagem: -", wraplength=285, justify="left") | |
| self.base_label.pack(fill="x", pady=(8, 0)) | |
| self.logo_label = ttk.Label(box_files, text="Logo: -", wraplength=285, justify="left") | |
| self.logo_label.pack(fill="x", pady=(4, 0)) | |
| # Ajustes | |
| box_adjust = ttk.LabelFrame(left, text="Ajustes", padding=10) | |
| box_adjust.pack(fill="x", pady=(0, 10)) | |
| ttk.Label(box_adjust, text="Escala da logo").pack(anchor="w") | |
| self.scale_var = tk.DoubleVar(value=1.0) | |
| self.scale_value = ttk.Label(box_adjust, text="1.00x") | |
| self.scale_value.pack(anchor="e") | |
| self.scale_slider = ttk.Scale( | |
| box_adjust, from_=0.05, to=4.0, orient="horizontal", | |
| variable=self.scale_var, command=self._on_scale_change | |
| ) | |
| self.scale_slider.pack(fill="x") | |
| ttk.Label(box_adjust, text="Opacidade").pack(anchor="w", pady=(10, 0)) | |
| self.opacity_var = tk.DoubleVar(value=1.0) | |
| self.opacity_value = ttk.Label(box_adjust, text="100%") | |
| self.opacity_value.pack(anchor="e") | |
| self.opacity_slider = ttk.Scale( | |
| box_adjust, from_=0.05, to=1.0, orient="horizontal", | |
| variable=self.opacity_var, command=self._on_opacity_change | |
| ) | |
| self.opacity_slider.pack(fill="x") | |
| ttk.Label(box_adjust, text="Margem").pack(anchor="w", pady=(10, 0)) | |
| self.margin_var = tk.IntVar(value=20) | |
| self.margin_value = ttk.Label(box_adjust, text="20 px") | |
| self.margin_value.pack(anchor="e") | |
| self.margin_slider = ttk.Scale( | |
| box_adjust, from_=0, to=200, orient="horizontal", | |
| variable=self.margin_var, command=self._on_margin_change | |
| ) | |
| self.margin_slider.pack(fill="x") | |
| # Posição | |
| box_pos = ttk.LabelFrame(left, text="Posição", padding=10) | |
| box_pos.pack(fill="x", pady=(0, 10)) | |
| grid = ttk.Frame(box_pos) | |
| grid.pack(fill="x") | |
| ttk.Button(grid, text="↖", width=4, command=lambda: self.snap("left", "top")).grid(row=0, column=0, padx=2, pady=2) | |
| ttk.Button(grid, text="↑", width=4, command=lambda: self.snap("center", "top")).grid(row=0, column=1, padx=2, pady=2) | |
| ttk.Button(grid, text="↗", width=4, command=lambda: self.snap("right", "top")).grid(row=0, column=2, padx=2, pady=2) | |
| ttk.Button(grid, text="←", width=4, command=lambda: self.snap("left", "center")).grid(row=1, column=0, padx=2, pady=2) | |
| ttk.Button(grid, text="●", width=4, command=lambda: self.snap("center", "center")).grid(row=1, column=1, padx=2, pady=2) | |
| ttk.Button(grid, text="→", width=4, command=lambda: self.snap("right", "center")).grid(row=1, column=2, padx=2, pady=2) | |
| ttk.Button(grid, text="↙", width=4, command=lambda: self.snap("left", "bottom")).grid(row=2, column=0, padx=2, pady=2) | |
| ttk.Button(grid, text="↓", width=4, command=lambda: self.snap("center", "bottom")).grid(row=2, column=1, padx=2, pady=2) | |
| ttk.Button(grid, text="↘", width=4, command=lambda: self.snap("right", "bottom")).grid(row=2, column=2, padx=2, pady=2) | |
| form = ttk.Frame(box_pos) | |
| form.pack(fill="x", pady=(10, 0)) | |
| ttk.Label(form, text="X").grid(row=0, column=0, sticky="w") | |
| ttk.Label(form, text="Y").grid(row=0, column=2, sticky="w", padx=(10, 0)) | |
| self.x_var = tk.IntVar(value=20) | |
| self.y_var = tk.IntVar(value=20) | |
| self.x_spin = tk.Spinbox(form, from_=0, to=99999, textvariable=self.x_var, width=10, command=self._on_xy_changed) | |
| self.y_spin = tk.Spinbox(form, from_=0, to=99999, textvariable=self.y_var, width=10, command=self._on_xy_changed) | |
| self.x_spin.grid(row=1, column=0, sticky="we") | |
| ttk.Button(form, text="←", width=3, command=lambda: self.nudge(-1, 0)).grid(row=1, column=1, padx=3) | |
| self.y_spin.grid(row=1, column=2, sticky="we") | |
| ttk.Button(form, text="↑", width=3, command=lambda: self.nudge(0, -1)).grid(row=1, column=3, padx=3) | |
| ttk.Button(form, text="→", width=3, command=lambda: self.nudge(1, 0)).grid(row=2, column=1, padx=3, pady=(3, 0)) | |
| ttk.Button(form, text="↓", width=3, command=lambda: self.nudge(0, 1)).grid(row=2, column=3, padx=3, pady=(3, 0)) | |
| ttk.Button(box_pos, text="Centralizar logo", command=lambda: self.snap("center", "center")).pack(fill="x", pady=(10, 0)) | |
| ttk.Label( | |
| box_pos, | |
| text="Dica: arraste a logo na prévia ou use as setas do teclado.\nShift + setas move 10 px.", | |
| wraplength=285, | |
| justify="left" | |
| ).pack(fill="x", pady=(8, 0)) | |
| # Fundo | |
| box_bg = ttk.LabelFrame(left, text="Remover fundo da logo", padding=10) | |
| box_bg.pack(fill="x", pady=(0, 10)) | |
| self.remove_bg_var = tk.BooleanVar(value=False) | |
| ttk.Checkbutton( | |
| box_bg, | |
| text="Ativar remoção de fundo", | |
| variable=self.remove_bg_var, | |
| command=self.update_preview, | |
| ).pack(anchor="w") | |
| self.auto_bg_var = tk.BooleanVar(value=True) | |
| ttk.Checkbutton( | |
| box_bg, | |
| text="Usar canto superior esquerdo como cor", | |
| variable=self.auto_bg_var, | |
| command=self._on_bg_mode_change, | |
| ).pack(anchor="w", pady=(4, 0)) | |
| ttk.Label(box_bg, text="Tolerância").pack(anchor="w", pady=(8, 0)) | |
| self.tolerance_var = tk.IntVar(value=24) | |
| self.tolerance_value = ttk.Label(box_bg, text="24") | |
| self.tolerance_value.pack(anchor="e") | |
| self.tolerance_slider = ttk.Scale( | |
| box_bg, from_=0, to=120, orient="horizontal", | |
| variable=self.tolerance_var, command=self._on_tolerance_change | |
| ) | |
| self.tolerance_slider.pack(fill="x") | |
| ttk.Button(box_bg, text="Pegar cor na prévia", command=self.enable_pick_color).pack(fill="x", pady=(8, 0)) | |
| self.swatch_frame = tk.Frame(box_bg, bg="#999999", height=24, relief="flat", bd=0) | |
| self.swatch_frame.pack(fill="x", pady=(6, 0)) | |
| self.swatch_label = ttk.Label(self.swatch_frame, text="Cor: -") | |
| self.swatch_label.pack(anchor="w", padx=6, pady=3) | |
| # Exportação | |
| box_export = ttk.LabelFrame(left, text="Exportar", padding=10) | |
| box_export.pack(fill="x", pady=(0, 10)) | |
| ttk.Button(box_export, text="Salvar imagem final", command=self.save_final).pack(fill="x") | |
| ttk.Button(box_export, text="Resetar posição", command=self.reset_position).pack(fill="x", pady=(6, 0)) | |
| self.status = ttk.Label(left, text="Abra uma imagem e uma logo.", wraplength=285, justify="left") | |
| self.status.pack(fill="x", pady=(10, 10)) | |
| # Canvas de Prévia | |
| canvas_box = ttk.LabelFrame(right, text="Prévia", padding=6) | |
| canvas_box.pack(fill="both", expand=True) | |
| self.canvas = tk.Canvas(canvas_box, bg="#262626", highlightthickness=0) | |
| self.canvas.pack(fill="both", expand=True) | |
| self.canvas.bind("<Button-1>", self.on_canvas_click) | |
| self.canvas.bind("<B1-Motion>", self.on_canvas_drag) | |
| self.canvas.bind("<ButtonRelease-1>", self.on_canvas_release) | |
| # Trace dos campos X/Y | |
| self.x_var.trace_add("write", lambda *_: self._on_xy_field_entry()) | |
| self.y_var.trace_add("write", lambda *_: self._on_xy_field_entry()) | |
| self._refresh_fields() | |
| self._update_swatch() | |
| # --------- Load ---------- | |
| def open_base(self): | |
| path = filedialog.askopenfilename( | |
| title="Escolher imagem", | |
| filetypes=[ | |
| ("Imagens", "*.png *.jpg *.jpeg *.webp *.bmp"), | |
| ("Todos os arquivos", "*.*"), | |
| ], | |
| ) | |
| if not path: | |
| return | |
| try: | |
| self.base_original = Image.open(path).convert("RGBA") | |
| self.base_path = path | |
| self.base_label.config(text=f"Imagem: {os.path.basename(path)}") | |
| self.fit_preview_base() | |
| self.reset_position() | |
| self.update_preview() | |
| except Exception as e: | |
| messagebox.showerror("Erro", f"Não consegui abrir a imagem.\n\n{e}") | |
| def open_logo(self): | |
| path = filedialog.askopenfilename( | |
| title="Escolher logo", | |
| filetypes=[ | |
| ("Imagens", "*.png *.jpg *.jpeg *.webp *.bmp"), | |
| ("Todos os arquivos", "*.*"), | |
| ], | |
| ) | |
| if not path: | |
| return | |
| try: | |
| self.logo_original = Image.open(path).convert("RGBA") | |
| self.logo_path = path | |
| self.logo_label.config(text=f"Logo: {os.path.basename(path)}") | |
| self.update_preview() | |
| except Exception as e: | |
| messagebox.showerror("Erro", f"Não consegui abrir a logo.\n\n{e}") | |
| # --------- Preview / processing ---------- | |
| def fit_preview_base(self): | |
| if not self.base_original: | |
| return | |
| bw, bh = self.base_original.size | |
| ratio = min(PREVIEW_MAX_W / bw, PREVIEW_MAX_H / bh, 1.0) | |
| ratio = max(ratio, 0.01) | |
| self.preview_ratio = ratio | |
| pw = max(1, int(bw * ratio)) | |
| ph = max(1, int(bh * ratio)) | |
| self.preview_base = self.base_original.resize((pw, ph), Image.LANCZOS) | |
| self.canvas.config(width=pw, height=ph) | |
| self.clamp_logo_inside_preview() | |
| def get_bg_color(self): | |
| if self.bg_color is not None: | |
| return self.bg_color | |
| if self.auto_bg_var.get() and self.logo_original: | |
| return self.logo_original.getpixel((0, 0))[:3] | |
| return None | |
| def build_logo_image(self, target_scale, preview=False): | |
| if not self.logo_original: | |
| return None | |
| logo = self.logo_original.copy().convert("RGBA") | |
| if self.remove_bg_var.get(): | |
| bg = self.get_bg_color() | |
| if bg is not None: | |
| logo = remove_background_by_color(logo, bg, int(self.tolerance_var.get())) | |
| logo = apply_opacity(logo, float(self.opacity_var.get())) | |
| target_scale = max(0.01, float(target_scale)) | |
| w = max(1, int(logo.width * target_scale)) | |
| h = max(1, int(logo.height * target_scale)) | |
| if (w, h) != logo.size: | |
| logo = logo.resize((w, h), Image.LANCZOS) | |
| return logo | |
| def update_preview(self): | |
| if not self.base_original: | |
| self.canvas.delete("all") | |
| return | |
| if not self.preview_base: | |
| self.fit_preview_base() | |
| if not self.preview_base: | |
| return | |
| preview = self.preview_base.copy().convert("RGBA") | |
| self.preview_logo_cache = None | |
| if self.logo_original: | |
| preview_logo = self.build_logo_image( | |
| target_scale=float(self.scale_var.get()) * self.preview_ratio, | |
| preview=True | |
| ) | |
| if preview_logo is not None: | |
| self.preview_logo_cache = preview_logo | |
| x = int(self.logo_x_preview) | |
| y = int(self.logo_y_preview) | |
| preview.alpha_composite(preview_logo, (x, y)) | |
| self.preview_tk = ImageTk.PhotoImage(preview) | |
| self.canvas.delete("all") | |
| self.canvas.create_image(0, 0, image=self.preview_tk, anchor="nw") | |
| self._refresh_fields() | |
| self._update_status() | |
| self._update_swatch() | |
| def _update_status(self): | |
| parts = [] | |
| if self.base_original: | |
| parts.append(f"Imagem: {self.base_original.width}x{self.base_original.height}") | |
| if self.logo_original: | |
| parts.append(f"Logo: {self.logo_original.width}x{self.logo_original.height}") | |
| parts.append(f"Escala: {self.scale_var.get():.2f}x") | |
| parts.append(f"Opacidade: {int(self.opacity_var.get() * 100)}%") | |
| parts.append(f"Margem: {self.margin_var.get()} px") | |
| parts.append(f"Posição: X={self.logo_x_preview}, Y={self.logo_y_preview}") | |
| if self.remove_bg_var.get(): | |
| parts.append("Fundo: ligado") | |
| else: | |
| parts.append("Fundo: desligado") | |
| self.status.config(text=" | ".join(parts)) | |
| def _update_swatch(self): | |
| if self.bg_color is None: | |
| if self.auto_bg_var.get() and self.logo_original: | |
| c = self.logo_original.getpixel((0, 0))[:3] | |
| self.swatch_frame.configure(bg=hex_from_rgb(c)) | |
| self.swatch_label.configure(text=f"Cor: auto {hex_from_rgb(c)}") | |
| else: | |
| self.swatch_frame.configure(bg="#999999") | |
| self.swatch_label.configure(text="Cor: -") | |
| return | |
| self.swatch_frame.configure(bg=hex_from_rgb(self.bg_color)) | |
| self.swatch_label.configure(text=f"Cor: {hex_from_rgb(self.bg_color)}") | |
| # --------- Sync fields ---------- | |
| def _refresh_fields(self): | |
| if self._syncing_fields: | |
| return | |
| self._syncing_fields = True | |
| try: | |
| self.x_var.set(int(self.logo_x_preview)) | |
| self.y_var.set(int(self.logo_y_preview)) | |
| self.scale_value.config(text=f"{self.scale_var.get():.2f}x") | |
| self.opacity_value.config(text=f"{int(float(self.opacity_var.get()) * 100)}%") | |
| self.margin_value.config(text=f"{int(self.margin_var.get())} px") | |
| self.tolerance_value.config(text=str(int(self.tolerance_var.get()))) | |
| finally: | |
| self._syncing_fields = False | |
| def _on_xy_field_entry(self): | |
| if self._syncing_fields: | |
| return | |
| if not self.preview_base or not self.logo_original: | |
| return | |
| try: | |
| x = int(self.x_var.get()) | |
| y = int(self.y_var.get()) | |
| except Exception: | |
| return | |
| self.logo_x_preview = x | |
| self.logo_y_preview = y | |
| self.clamp_logo_inside_preview() | |
| self.update_preview() | |
| def _on_xy_changed(self): | |
| self._on_xy_field_entry() | |
| def _on_scale_change(self, _=None): | |
| self.scale_value.config(text=f"{float(self.scale_var.get()):.2f}x") | |
| self.clamp_logo_inside_preview() | |
| self.update_preview() | |
| def _on_opacity_change(self, _=None): | |
| self.opacity_value.config(text=f"{int(float(self.opacity_var.get()) * 100)}%") | |
| self.update_preview() | |
| def _on_margin_change(self, _=None): | |
| self.margin_value.config(text=f"{int(self.margin_var.get())} px") | |
| self.update_preview() | |
| def _on_tolerance_change(self, _=None): | |
| self.tolerance_value.config(text=str(int(self.tolerance_var.get()))) | |
| self.update_preview() | |
| def _on_bg_mode_change(self): | |
| if not self.auto_bg_var.get(): | |
| pass | |
| self.update_preview() | |
| # --------- Position helpers ---------- | |
| def current_preview_logo_size(self): | |
| logo = self.preview_logo_cache | |
| if logo is None: | |
| logo = self.build_logo_image(float(self.scale_var.get()) * self.preview_ratio, preview=True) | |
| if logo is None: | |
| return None | |
| return logo.width, logo.height | |
| def clamp_logo_inside_preview(self): | |
| if not self.preview_base or not self.logo_original: | |
| return | |
| size = self.current_preview_logo_size() | |
| if not size: | |
| return | |
| lw, lh = size | |
| bw, bh = self.preview_base.size | |
| self.logo_x_preview = clamp(int(self.logo_x_preview), 0, max(0, bw - lw)) | |
| self.logo_y_preview = clamp(int(self.logo_y_preview), 0, max(0, bh - lh)) | |
| def snap(self, hx, vy): | |
| if not self.preview_base or not self.logo_original: | |
| return | |
| size = self.current_preview_logo_size() | |
| if not size: | |
| return | |
| lw, lh = size | |
| bw, bh = self.preview_base.size | |
| margin = int(self.margin_var.get()) | |
| if hx == "left": | |
| x = margin | |
| elif hx == "center": | |
| x = (bw - lw) // 2 | |
| else: | |
| x = bw - lw - margin | |
| if vy == "top": | |
| y = margin | |
| elif vy == "center": | |
| y = (bh - lh) // 2 | |
| else: | |
| y = bh - lh - margin | |
| self.logo_x_preview = clamp(x, 0, max(0, bw - lw)) | |
| self.logo_y_preview = clamp(y, 0, max(0, bh - lh)) | |
| self.update_preview() | |
| def reset_position(self): | |
| if not self.base_original: | |
| return | |
| self.logo_x_preview = 20 | |
| self.logo_y_preview = 20 | |
| self.update_preview() | |
| def nudge(self, dx, dy): | |
| if not self.preview_base or not self.logo_original: | |
| return "break" | |
| self.logo_x_preview += int(dx) | |
| self.logo_y_preview += int(dy) | |
| self.clamp_logo_inside_preview() | |
| self.update_preview() | |
| return "break" | |
| # --------- Canvas interaction ---------- | |
| def enable_pick_color(self): | |
| if not self.logo_original: | |
| messagebox.showinfo("Falta a logo", "Abra a logo primeiro.") | |
| return | |
| self.pick_color_mode = True | |
| self.root.config(cursor="crosshair") | |
| self.status.config(text="Clique em cima da logo para pegar a cor a remover.") | |
| def on_canvas_click(self, event): | |
| if not self.base_original or not self.logo_original: | |
| return | |
| size = self.current_preview_logo_size() | |
| if not size: | |
| return | |
| lw, lh = size | |
| if self.pick_color_mode: | |
| self.pick_color_mode = False | |
| self.root.config(cursor="") | |
| inside = ( | |
| self.logo_x_preview <= event.x <= self.logo_x_preview + lw and | |
| self.logo_y_preview <= event.y <= self.logo_y_preview + lh | |
| ) | |
| if not inside: | |
| messagebox.showinfo("Fora da logo", "Clique em cima da logo para pegar a cor.") | |
| self.update_preview() | |
| return | |
| px = event.x - self.logo_x_preview | |
| py = event.y - self.logo_y_preview | |
| scale = max(0.0001, float(self.scale_var.get()) * self.preview_ratio) | |
| ox = clamp(int(px / scale), 0, self.logo_original.width - 1) | |
| oy = clamp(int(py / scale), 0, self.logo_original.height - 1) | |
| self.bg_color = self.logo_original.getpixel((ox, oy))[:3] | |
| self._update_swatch() | |
| self.update_preview() | |
| return | |
| if self.logo_x_preview <= event.x <= self.logo_x_preview + lw and self.logo_y_preview <= event.y <= self.logo_y_preview + lh: | |
| self.dragging = True | |
| self.drag_offset_x = event.x - self.logo_x_preview | |
| self.drag_offset_y = event.y - self.logo_y_preview | |
| def on_canvas_drag(self, event): | |
| if not self.dragging: | |
| return | |
| if not self.preview_base or not self.logo_original: | |
| return | |
| size = self.current_preview_logo_size() | |
| if not size: | |
| return | |
| lw, lh = size | |
| bw, bh = self.preview_base.size | |
| self.logo_x_preview = clamp(event.x - self.drag_offset_x, 0, max(0, bw - lw)) | |
| self.logo_y_preview = clamp(event.y - self.drag_offset_y, 0, max(0, bh - lh)) | |
| self.update_preview() | |
| def on_canvas_release(self, _event): | |
| self.dragging = False | |
| # --------- Export ---------- | |
| def save_final(self): | |
| if not self.base_original or not self.logo_original: | |
| messagebox.showinfo("Falta coisa", "Abra a imagem e a logo primeiro.") | |
| return | |
| out = filedialog.asksaveasfilename( | |
| title="Salvar imagem final", | |
| defaultextension=".png", | |
| filetypes=[ | |
| ("PNG", "*.png"), | |
| ("WebP", "*.webp"), | |
| ("JPG", "*.jpg *.jpeg"), | |
| ], | |
| ) | |
| if not out: | |
| return | |
| try: | |
| base = self.base_original.copy().convert("RGBA") | |
| logo = self.build_logo_image(float(self.scale_var.get()), preview=False) | |
| if logo is None: | |
| raise RuntimeError("Logo inválida.") | |
| x = int(self.logo_x_preview / max(self.preview_ratio, 0.0001)) | |
| y = int(self.logo_y_preview / max(self.preview_ratio, 0.0001)) | |
| layer = Image.new("RGBA", base.size, (0, 0, 0, 0)) | |
| layer.alpha_composite(logo, (x, y)) | |
| result = Image.alpha_composite(base, layer) | |
| ext = os.path.splitext(out)[1].lower() | |
| if ext in (".jpg", ".jpeg"): | |
| result = result.convert("RGB") | |
| result.save(out, quality=95) | |
| elif ext == ".webp": | |
| result.save(out, quality=95) | |
| else: | |
| result.save(out) | |
| messagebox.showinfo("Feito", f"Salvo em:\n{out}") | |
| except Exception as e: | |
| messagebox.showerror("Erro", f"Falhou ao salvar.\n\n{e}") | |
| def main(): | |
| root = tk.Tk() | |
| app = WatermarkApp(root) | |
| root.mainloop() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment