From 0702b934050968da4bef1263b850c880de8eb057 Mon Sep 17 00:00:00 2001 From: QuBerto Date: Sun, 14 Jul 2024 04:07:50 +0200 Subject: [PATCH] update --- ui/local_processing_tab.py | Bin 12579 -> 14250 bytes ui/log_window.py | 4 +- ui/options_window.py | 156 +++++++++++++++++++++++++++++++++++-- ui/settings_tab.py | Bin 2284 -> 2158 bytes utils/file_operations.py | 41 ++++++++-- utils/image_processing.py | 120 ++++++++++++++++++++++------ 6 files changed, 282 insertions(+), 39 deletions(-) diff --git a/ui/local_processing_tab.py b/ui/local_processing_tab.py index 2122f4bfa1876d5ded30333aa8e88caf4fbc012d..6f8baa13e0ef6ec18fd2681fee86bdf5ef4a464e 100644 GIT binary patch literal 14250 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 diff --git a/ui/log_window.py b/ui/log_window.py index 17d35a5..9486c0d 100644 --- a/ui/log_window.py +++ b/ui/log_window.py @@ -1,5 +1,5 @@ from tkinter import Toplevel, Text - +from pprint import pprint class LogWindow(Toplevel): def __init__(self, master=None, **kwargs): @@ -11,7 +11,7 @@ class LogWindow(Toplevel): self.protocol("WM_DELETE_WINDOW", self.hide) def log(self, message): - self.text.insert("end", message + "\n") + self.text.insert("end", pprint(message) + "\n") self.text.see("end") def hide(self): diff --git a/ui/options_window.py b/ui/options_window.py index 08afe2f..836cea7 100644 --- a/ui/options_window.py +++ b/ui/options_window.py @@ -1,12 +1,11 @@ import tkinter as tk -from tkinter import ttk - - +from tkinter import ttk, colorchooser, messagebox +from pprint import pprint class OptionsWindow(tk.Toplevel): def __init__(self, parent, apply_callback, current_options): super().__init__(parent) self.title("Options") - self.geometry("400x400") + self.geometry("400x500") self.apply_callback = apply_callback self.options = current_options @@ -32,6 +31,12 @@ class OptionsWindow(tk.Toplevel): self.add_text_input(name, details["label"], details["default"]) elif details["type"] == "checkbox": self.add_checkbox(name, details["label"], details["default"]) + elif details["type"] == "dropdown": + self.add_dropdown( + name, details["label"], details["options"], details["default"] + ) + elif details["type"] == "color": + self.add_color_picker(name, details["label"], details["default"]) self.create_apply_button() @@ -47,11 +52,11 @@ class OptionsWindow(tk.Toplevel): max_val (int): The maximum value. """ lbl = tk.Label(self, text=label) - lbl.grid(row=self.row_index, column=0, padx=5, pady=5, sticky="w") + lbl.grid(row=self.row_index, columnspan=1,column=0, padx=5, pady=5, sticky="w") entry = tk.Entry(self) entry.insert(0, str(default)) - entry.grid(row=self.row_index, column=1, padx=5, pady=5, sticky="w") + entry.grid(row=self.row_index, columnspan=2, column=1, padx=5, pady=5, sticky="w") self.inputs[name] = { "type": "number", @@ -75,7 +80,7 @@ class OptionsWindow(tk.Toplevel): entry = tk.Entry(self) entry.insert(0, default) - entry.grid(row=self.row_index, column=1, padx=5, pady=5, sticky="w") + entry.grid(row=self.row_index, columnspan=2, column=1, padx=5, pady=5, sticky="w") self.inputs[name] = {"type": "text", "widget": entry} self.row_index += 1 @@ -94,7 +99,78 @@ class OptionsWindow(tk.Toplevel): chk.grid(row=self.row_index, column=0, columnspan=2, padx=5, pady=5, sticky="w") - self.inputs[name] = {"type": "checkbox", "variable": var} + self.inputs[name] = {"type": "checkbox", "variable": var, 'label': label, 'default': default} + self.row_index += 1 + + def add_dropdown(self, name, label, options, default): + """ + Add a dropdown field. + + Args: + name (str): The name of the dropdown. + label (str): The label for the dropdown. + options (list): The list of options. + default (str): The default value. + """ + lbl = tk.Label(self, text=label) + lbl.grid(row=self.row_index, column=0, padx=5, pady=5, sticky="w") + + combo = ttk.Combobox(self, values=options, state="readonly") + combo.set(default) + combo.grid(row=self.row_index,columnspan=2, column=1, padx=5, pady=5, sticky="w") + + self.inputs[name] = {"type": "dropdown", "widget": combo} + self.row_index += 1 + + def check_transparent(self, var, color_entry, pick_button, color_preview): + if var.get(): + color_entry.config(state="disabled") + pick_button.config(state="disabled") + color_preview.config(bg="white") + else: + color_entry.config(state="normal") + pick_button.config(state="normal") + color_preview.config(bg=color_entry.get()) + + def pick_color(self, color_entry, color_preview): + color_code = colorchooser.askcolor(title="Choose color")[1] + if color_code: + color_entry.delete(0, tk.END) + color_entry.insert(0, color_code) + color_preview.config(bg=color_code) + + def add_color_picker(self, name, label, default): + """ + Add a color picker. + + Args: + name (str): The name of the color picker. + label (str): The label for the color picker. + default (str): The default color. + """ + if default == "transparent": + default = "#00000000" + var = tk.BooleanVar(value=True) + else: + var = tk.BooleanVar(value=False) + lbl = tk.Label(self, text=label) + lbl.grid(row=self.row_index, column=0, padx=5, pady=5, sticky="w") + + color_preview = tk.Label(self, bg=default, width=2, height=1) + color_preview.grid(row=self.row_index, column=1, padx=5, pady=5, sticky="w") + + color_entry = tk.Entry(self) + color_entry.insert(0, default) + color_entry.grid(row=self.row_index, column=2, padx=5, pady=5, sticky="w") + + pick_button = tk.Button(self, text="Pick", command=lambda: self.pick_color(color_entry, color_preview)) + pick_button.grid(row=self.row_index, column=3, padx=5, pady=5, sticky="w") + + + chk = tk.Checkbutton(self, text="Transparent", variable=var, command=lambda: self.check_transparent(var, color_entry, pick_button, color_preview)) + chk.grid(row=self.row_index, column=4, padx=5, pady=5, sticky="w") + + self.inputs[name] = {"type": "color", "entry": color_entry, "transparent_var": var} self.row_index += 1 def create_apply_button(self): @@ -126,6 +202,70 @@ class OptionsWindow(tk.Toplevel): options[name] = details["widget"].get() elif details["type"] == "checkbox": options[name] = details["variable"].get() + elif details["type"] == "dropdown": + options[name] = details["widget"].get() + elif details["type"] == "color": + if "value" in details: + options[name] = details["value"] + else: + options[name] = "transparent" self.apply_callback(options) self.destroy() + + def add_conditional_setting(self, name, condition): + """ + Add a conditional setting that is displayed based on another setting. + + Args: + name (str): The name of the conditional setting. + condition (function): The condition function that returns a boolean. + """ + if condition(): + if self.inputs[name]["type"] == "number": + self.add_number_input( + name, + self.inputs[name]["label"], + self.inputs[name]["default"], + self.inputs[name]["min"], + self.inputs[name]["max"], + ) + elif self.inputs[name]["type"] == "text": + self.add_text_input( + name, self.inputs[name]["label"], self.inputs[name]["default"] + ) + elif self.inputs[name]["type"] == "checkbox": + self.add_checkbox( + name, self.inputs[name]["label"], self.inputs[name]["default"] + ) + elif self.inputs[name]["type"] == "dropdown": + self.add_dropdown( + name, + self.inputs[name]["label"], + self.inputs[name]["options"], + self.inputs[name]["default"], + ) + elif self.inputs[name]["type"] == "color": + self.add_color_picker( + name, self.inputs[name]["label"], self.inputs[name]["default"] + ) + + +# Example usage +if __name__ == "__main__": + def apply_options(options): + print(options) + + root = tk.Tk() + current_options = { + "canvas_width": {"type": "number", "label": "Width:", "default": 900, "min": 1, "max": 2540}, + "canvas_height": {"type": "number", "label": "Height:", "default": 900, "min": 1, "max": 2540}, + "template": {"type": "text", "label": "Filename Template:", "default": "{slug}_{sku}_{width}x{height}"}, + "delete_images": {"type": "checkbox", "label": "Delete image when done", "default": False}, + "background_color": {"type": "color", "label": "Background Color:", "default": "#FFFFFF"}, + "image_format": {"type": "dropdown", "label": "Image Format:", "options": ["JPEG", "PNG", "GIF"], "default": "JPEG"} + } + + app = OptionsWindow(root, apply_options, current_options) + + root.mainloop() diff --git a/ui/settings_tab.py b/ui/settings_tab.py index 820a444650b5ea37197651bf7a7246d12473f037..57d2318a0affa8dc8bb83dc08e1b770874257c0c 100644 GIT binary patch literal 2158 zcmb7_yN;YN6oz}A!lD_75K5C~C7O(qX17>nvPx-rz_YOc8`&n4dHeVR##{`WsXX?7 zF8+KthqN8z63CiRi8(+*Eu$M}t=l4lV%$d^$?uGntZgwbF;xG-pb_W~h9$=prjnpW zWT0WF@^_-BY>*J(f~6$%UHnAXrwFKDg>L{tLWzVh5xBW!K%y(`5Ldp`8vm84Rcf^s zz~ zHh!~v5r6VZGVI~w_-x#Lf_`p+%k-*(scHkq^xg)gk8N%p2w^{(F-{QF@*9~M9C8^m z*#t5k?v_)h8tk3%>X^jqp!7-1t@lVcWspR$Wag%aOh)-8lksquiJ45-UP?x7WBh8? zt}_k;A-<>y2F}w2i!-1H9imlXUVk~^ytme~&qmmQrpMdT)&%$F!|L1(rge8WZ8^=> g3fD_?$2AXgES-H_y5Da(UX)%)ws;LAb6KYU0II^WW&i*H literal 2284 gcmZQz7zLvtFd71*Aut*OqaiRF0;3@?Y(szx00{N~3jhEB diff --git a/utils/file_operations.py b/utils/file_operations.py index d5bf007..59178a6 100644 --- a/utils/file_operations.py +++ b/utils/file_operations.py @@ -2,7 +2,7 @@ import os import shutil from tkinter import filedialog, messagebox from utils.image_processing import ImageProcessor - +from pprint import pprint class FileProcessor: """ @@ -134,17 +134,44 @@ class FileProcessor: log (function): The log function to use. """ image = ImageProcessor() + image.set_background_color(options.get("background_color", "transparent")) + image.set_image_size(options.get("image_size", "contain")) for file_path in image_paths: - output_path = os.path.join( - output_directory, os.path.relpath( - file_path, self.selected_directory) - ) + # output_path = os.path.join( + # output_directory, os.path.relpath( + # file_path, self.selected_directory) + # ) + output_path = self.generate_output_path(output_directory, file_path, options) os.makedirs(os.path.dirname(output_path), exist_ok=True) + self.log_message(f"Running: {file_path}", log) image.resize_image( - file_path, output_path, options.get("additional_name", "") + file_path, output_path, options ) - if os.path.exists(file_path) and options.get("is_checked", False): + if os.path.exists(file_path) and options.get("delete_images", False): self.log_message(f"Removing: {file_path}", log) os.remove(file_path) self.log_message(f"Processed: {file_path}", log) + + def generate_output_path(self, output_directory, file_path, options, product = None): + """ + Generate the output path for resized images based on a template. + + + Returns: + str: The generated output path. + """ + sku = slug = title = "" + name, ext = os.path.splitext(os.path.basename(file_path)) + width = options.get("canvas_width") + height = options.get("canvas_height") + if product: + sku = product.get("sku", "") + slug = product.get("name", "") + title = product.get("slug", "") + + new_filename = options.get('template', '{name}').format( + name=name, sku=sku, width=width, height=height, slug=slug, title=title + ) + pprint(new_filename) + return os.path.join(output_directory, new_filename + ext) diff --git a/utils/image_processing.py b/utils/image_processing.py index 05c2bfa..d45204c 100644 --- a/utils/image_processing.py +++ b/utils/image_processing.py @@ -4,15 +4,14 @@ from wand.color import Color class ImageProcessor: - def __init__( - self, canvas_width=900, canvas_height=900, background_color="transparent" - ): + def __init__(self, canvas_width=900, canvas_height=900, background_color="transparent", image_size="fit"): """ Initialize the ImageProcessor with default values. """ self.canvas_width = canvas_width self.canvas_height = canvas_height - self.background_color = background_color + self.background_color = Color(background_color) + self.image_size = image_size def set_canvas_size(self, width, height): """ @@ -27,40 +26,106 @@ class ImageProcessor: """ self.background_color = Color(color) - def resize_image(self, image_path, output_path, additional_name=None): + def set_image_size(self, size): + """ + Set the image size mode. + """ + self.image_size = size + + def resize_image(self, image_path, output_path, options): """ Resize and process the image. + + Args: + image_path (str): The path to the input image. + output_path (str): The path to the output image. + additional_name (str, optional): Additional name to append to the output filename. + mode (str, optional): The resizing mode ("contain", "cover", "fit"). Default is "contain". """ + log = options.get("log_message", None) + # Normalize the paths to ensure consistency image_path = os.path.normpath(image_path) output_path = os.path.normpath(output_path) - print(image_path) - print(output_path) + with Image(filename=image_path) as img: - img.transform(resize=f"{self.canvas_width}x{self.canvas_height}>") + self.log_message(f"Original image size: {img.width}x{img.height}", log) + if self.image_size == "contain": + self._contain(img) + elif self.image_size == "cover": + self._cover(img) + # elif self.image_size == "fit": + # self._fit(img) x_offset = int((self.canvas_width - img.width) / 2) y_offset = int((self.canvas_height - img.height) / 2) - with Image( - width=self.canvas_width, - height=self.canvas_height, - background=self.background_color, - ) as canvas: + with Image(width=self.canvas_width, height=self.canvas_height, background=self.background_color) as canvas: canvas.composite(img, left=x_offset, top=y_offset) # Create a new filename - new_filename = os.path.splitext( - os.path.basename(output_path))[0] - if additional_name: - new_filename += " - " + additional_name.strip() + new_filename = os.path.splitext(os.path.basename(output_path))[0] + new_filename += os.path.splitext(output_path)[1] # Construct the final output path - final_output_path = os.path.join( - os.path.dirname(output_path), new_filename - ) + final_output_path = os.path.join(os.path.dirname(output_path), new_filename) # Save the image to the final output path canvas.save(filename=final_output_path) - print(f"Saved to: {final_output_path}") + self.log_message(f"Saved to: {final_output_path}", log) + + + def _cover(self, img:Image): + """ + Resize the image to cover the entire canvas. + """ + base_width = self.canvas_width + base_heigth = self.canvas_height + wpercent = (base_width / float(img.size[0])) + hpercent = (base_heigth / float(img.size[1])) + hsize = int((float(img.size[1]) * float(wpercent))) + wsize = int((float(img.size[0]) * float(hpercent))) + img.resize(wsize, base_heigth) + + + aspect_ratio_img = img.width / img.height + aspect_ratio_canvas = self.canvas_width / self.canvas_height + print(f"Image aspect ratio: {aspect_ratio_img}, Canvas aspect ratio: {aspect_ratio_canvas}") + print(f"Cover resized image size: {img.width}x{img.height}") + + + def _contain(self, img): + """ + Resize the image to cover the entire canvas. + """ + aspect_ratio_img = img.width / img.height + aspect_ratio_canvas = self.canvas_width / self.canvas_height + print(f"Image aspect ratio: {aspect_ratio_img}, Canvas aspect ratio: {aspect_ratio_canvas}") + + if aspect_ratio_img > aspect_ratio_canvas: + img.transform(resize=f"{self.canvas_width}x") + else: + img.transform(resize=f"x{self.canvas_height}") + print(f"Cover resized image size: {img.width}x{img.height}") + + # def _fit(self, img): + # """ + # Fit the image within the canvas without scaling up if it's smaller. + # """ + # if img.width > self.canvas_width or img.height > self.canvas_height: + # img.transform(resize=f"{self.canvas_width}x{self.canvas_height}>") + # print(f"Fit resized image size: {img.width}x{img.height}") + + def log_message(self, message, log=None): + """ + Log a message or print it if no log function is provided. + + Args: + message (str): The message to log or print. + log (function, optional): The log function to use. Defaults to None. + """ + if log: + log(message) + else: + print(message) # Example usage @@ -68,4 +133,15 @@ if __name__ == "__main__": processor = ImageProcessor() processor.set_canvas_size(900, 900) processor.set_background_color("white") - processor.resize_image("input_image.jpg", "output_image.jpg", "example") + + # Contain mode + processor.set_image_size("contain") + processor.resize_image("input_image.jpg", "output_image_contain.jpg", "example") + + # Cover mode + processor.set_image_size("cover") + processor.resize_image("input_image.jpg", "output_image_cover.jpg", "example") + + # Fit mode + processor.set_image_size("fit") + processor.resize_image("input_image.jpg", "output_image_fit.jpg", "example")