diff --git a/ui/local_processing_tab.py b/ui/local_processing_tab.py index 2122f4b..6f8baa1 100644 Binary files a/ui/local_processing_tab.py and b/ui/local_processing_tab.py differ 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 820a444..57d2318 100644 Binary files a/ui/settings_tab.py and b/ui/settings_tab.py differ 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")