Files
siti-image-convertor/controller.py

653 lines
25 KiB
Python

import tempfile
import threading
from utils.file_operations import FileProcessor
from utils.image_processing import ImageProcessor
from ui.options_window import OptionsWindow
from config.encrypt_config import ConfigEncryptor
from api.woocommerce_api import get_first_image
from PIL import Image, ImageTk
# Enable AVIF support for Pillow previews when the optional plugin is installed.
try:
import pillow_avif # type: ignore
except Exception:
pillow_avif = None
from pprint import pformat
from api.woocommerce_api import process_product_images, process_all_products, search_product, get_first_image_path, get_product
import customtkinter as ctk
import os
class AppController:
"""
The controller class for managing the overall state and interactions of the application.
"""
def __init__(self, root):
"""
Initialize the AppController.
Args:
root (ctk.CTk): The root CustomTkinter window.
"""
key = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI="
self.root = root
self.file = FileProcessor()
self.image = ImageProcessor()
self.menu_bar = None
self.local_processing_tab = None
self.settings_tab = None
self.preview_bar = None
self.log = None
self.canvas_width = 900
self.canvas_height = 900
self.template = "{slug}_{sku}_{width}x{height}"
self.delete_images = False
self.transparent = True
self.background_color = "#000000"
self.image_format = "AUTO"
self.image_size = "contain"
self.config = ConfigEncryptor(key)
self.type = None
self.destination_path = None
self.found_products = None
self.selected_directory = None
self.current_product = 0
self.status = "stopped"
self.load_config()
def load_config(self):
config = self.config.load_config()
if config:
if options := config.get("options"):
self.canvas_width = options.get("canvas_width", 900)
self.canvas_height = options.get("canvas_height", 900)
self.template = options.get("template", "{slug}_{sku}_{width}x{height}")
self.delete_images = options.get("delete_images", False)
self.transparent = options.get("transparent", True)
self.background_color = options.get("background_color", "#000000")
self.image_format = options.get("image_format", "AUTO")
self.image_size = options.get("image_size", "contain")
def set_menu_bar(self, menu_bar):
"""
Set the MenuBar for the application.
Args:
menu_bar (MenuBar): The MenuBar instance.
"""
self.log_message("Init menu bar")
self.menu_bar = menu_bar
def set_log(self, log):
"""
Set the MenuBar for the application.
Args:
menu_bar (MenuBar): The MenuBar instance.
"""
self.log = log
self.log_message("Init Logs")
def set_local_processing_tab(self, local_processing_tab):
"""
Set the LocalProcessingTab for the application.
Args:
local_processing_tab (LocalProcessingTab): The LocalProcessingTab instance.
"""
self.local_processing_tab = local_processing_tab
self.log_message("Init main")
def set_preview_bar(self, preview):
"""
Set the MenuBar for the application.
Args:
menu_bar (MenuBar): The MenuBar instance.
"""
self.preview_bar = preview
self.log_message("Init previews")
def set_info_bar(self, info):
"""
Set the MenuBar for the application.
Args:
menu_bar (MenuBar): The MenuBar instance.
"""
self.info_bar = info
self.log_message("Init info")
def set_settings_tab(self, settings_tab):
"""
Set the SettingsTab for the application.
Args:
settings_tab (SettingsTab): The SettingsTab instance.
"""
self.settings_tab = settings_tab
self.log_message("Init settings")
def show_local_processing_tab(self):
"""
Display the local processing tab.
"""
if self.local_processing_tab:
self.log_message("Show main tab")
self.local_processing_tab.tkraise() # Make sure to raise the correct tab frame
def show_settings_tab(self):
"""
Display the settings tab.
"""
if self.settings_tab:
self.log_message("Show settings tab")
self.settings_tab.tab.tkraise() # Make sure to raise the correct tab frame
def show_local_processing_options(self):
"""
Display the options window in the local processing tab.
"""
if self.local_processing_tab:
self.log_message("Open options")
self.open_options_window()
def log_message(self, obj):
"""
Log a formatted message to the log window using pprint.
Args:
obj (object): The object to format and log.
"""
if self.log:
formatted_message = pformat(obj)
self.log.log_message(formatted_message)
import threading
def start_processing(self):
"""
Start the image processing based on the selected options.
"""
source = self.type
options = self.get_options()
self.log_message(f"Start import source: {source}")
self.status = "started"
self.menu_bar.start_button.configure(fg_color="red", text="Running")
# Wrapper to process and update status after completion
def process_and_update_status(target_func, *args):
try:
# Execute the actual processing function
target_func(*args)
finally:
# Update status to 'stopped' after processing is done
self.status = "stopped"
self.menu_bar.start_button.configure(fg_color="#008000", text="Start")
self.log_message(f"Processing completed for source: {source}")
if source == "directory":
threading.Thread(
target=process_and_update_status, args=(self.file.process_directory_with_logging, options)
).start()
elif source == "product":
threading.Thread(
target=process_and_update_status, args=(process_product_images, options)
).start()
elif source == "file":
threading.Thread(
target=process_and_update_status, args=(self.file.proces_single_image, options)
).start()
elif source == "all_products":
threading.Thread(
target=process_and_update_status, args=(process_all_products, options)
).start()
def update_options(self, text=None):
"""
Update the UI elements based on the selected source type.
"""
self.type = text
self.log_message(f"Update options {text}")
if text:
self.update_info(text)
# if self.local_processing_tab:
# self.local_processing_tab.product_id_button.grid_remove()
# self.local_processing_tab.product_id_entry.grid_remove()
# self.local_processing_tab.additional_name_label.grid_remove()
# self.local_processing_tab.additional_name_entry.grid_remove()
# self.local_processing_tab.browse_button.grid_remove()
# self.local_processing_tab.browse_file_button.grid_remove()
# if self.type == "directory":
# self.local_processing_tab.browse_button.grid()
# elif self.type == "file":
# self.local_processing_tab.browse_button.grid()
# elif self.type == "all_products":
# pass
# elif self.type == "wp_image":
# self.local_processing_tab.product_id_button.grid()
# self.local_processing_tab.product_id_entry.grid()
# elif self.type == "product":
# self.local_processing_tab.product_id_button.grid()
# self.local_processing_tab.product_id_entry.grid()
self.update_previews()
def update_previews(self, before_path=None, after_path=None):
"""
Update the image previews.
Args:
before_path (str, optional): The path to the 'before' image.
after_path (str, optional): The path to the 'after' image.
"""
first_image_path = False
if self.status != "started":
if self.type == "all_products":
first_image_path = get_first_image()
elif self.type == "product" and self.found_products:
first_image_path = get_first_image_path(self.found_products[self.current_product])
else:
print("getting first path")
first_image_path = self.file.get_first_image_path()
if before_path :
before_img = Image.open(before_path)
before_img.thumbnail((200, 200))
before_photo = ImageTk.PhotoImage(before_img)
self.preview_bar.before_image_label.configure(image=before_photo)
self.preview_bar.before_image_label.image = before_photo
dir_name = os.path.basename(before_path)
if len(dir_name) > 35:
dir_name = f"...{dir_name[-35:]}"
self.preview_bar.before_filename_label.configure(text=dir_name)
if after_path:
after_img = Image.open(after_path)
after_img.thumbnail((200, 200))
after_photo = ImageTk.PhotoImage(after_img)
self.preview_bar.after_image_label.configure(image=after_photo)
self.preview_bar.after_image_label.image = after_photo
dir_name = os.path.basename(after_path)
if len(dir_name) > 35:
dir_name = f"...{dir_name[-35:]}"
self.preview_bar.before_filename_label.configure(text=dir_name)
if first_image_path:
fd, output_path = tempfile.mkstemp(suffix=".jpg")
os.close(fd)
self.image.resize_image(
first_image_path, output_path, self.get_options()
)
before_img = Image.open(first_image_path)
before_img.thumbnail((200, 200))
before_photo = ImageTk.PhotoImage(before_img)
self.preview_bar.before_image_label.configure(image=before_photo)
self.preview_bar.before_image_label.image = before_photo
after_img = Image.open(output_path)
after_img.thumbnail((200, 200))
after_photo = ImageTk.PhotoImage(after_img)
self.preview_bar.after_image_label.configure(image=after_photo)
self.preview_bar.after_image_label.image = after_photo
name = self.file.generate_output_path("/",first_image_path,self.get_options())
dir_name = os.path.basename(name)
if len(dir_name) > 35:
dir_name = f"...{dir_name[-35:]}"
self.preview_bar.after_filename_label.configure(text=dir_name)
dir_name = os.path.basename(first_image_path)
if len(dir_name) > 35:
dir_name = f"...{dir_name[-35:]}"
self.preview_bar.before_filename_label.configure(text=dir_name)
def set_image_preview(self, image_path, label):
"""
Set the image preview for a given label.
Args:
image_path (str): The path to the image file.
label (ctk.CTkLabel): The label to set the image on.
"""
img = Image.open(image_path)
img.thumbnail((150, 150))
photo = ImageTk.PhotoImage(img)
label.configure(image=photo)
label.image = photo
def browse_file_command(self):
"""
Command to browse for a file.
"""
file = self.file.browse_files()
if file:
file_name = os.path.basename(file)
if len(file_name) > 35:
file_name = f"...{file_name[-35:]}"
self.info_bar.selected_button_label.configure(text=file_name)
self.apply_options(self.get_options())
self.update_previews()
def browse_directory_command(self):
"""
Command to browse for a directory.
"""
directory = self.file.browse_directory()
if directory:
dir_name = os.path.basename(directory)
if len(dir_name) > 35:
dir_name = f"...{dir_name[-35:]}"
# self.browse_button.configure(text=dir_name)
self.selected_directory = directory
self.apply_options(self.get_options())
self.update_previews()
def browse_destination_command(self):
"""
Open directory dialog to select a destination directory.
"""
destination_path = self.file.browse_directory()
if destination_path:
self.info_bar.destination_label.configure(text=f"Destination: {destination_path}")
print(f"Selected destination: {destination_path}")
self.destination_path = destination_path
def apply_canvas_size(self):
"""
Apply the canvas size settings and update previews.
"""
self.image.set_canvas_size(self.canvas_width, self.canvas_height)
def apply_image_size(self):
"""
Apply the canvas size settings and update previews.
"""
self.image.set_image_size(self.image_size)
def apply_background_color(self):
"""
Apply the canvas size settings and update previews.
"""
self.image.set_background_color(self.background_color)
def get_options(self) -> dict:
"""
Get the current processing options.
Returns:
dict: The current processing options.
"""
if not self.destination_path:
self.destination_path = False
product = None
product_id = 0
if self.found_products and len(self.found_products) >= self.current_product:
product_id = self.found_products[self.current_product]['id']
product = self.found_products[self.current_product]
options = {
# "selected_directory": self.local_processing_tab.browse_button.cget("text"),
"canvas_width": self.canvas_width,
"canvas_height": self.canvas_height,
"log_message": self.log, # Use the log method from the log_window
"format_log_message": self.log_message,
"update_previews": self.update_previews,
"product_id": product_id,
"product": product,
"template": self.template,
"delete_images": self.delete_images,
"background_color": self.background_color,
"image_format": self.image_format,
"image_size": self.image_size,
"selected_directory": self.selected_directory,
"destination_path" : self.destination_path
}
return options
def open_options_window(self):
"""
Open the options window.
"""
current_options = {
"canvas_width": {
"type": "number",
"label": "Width:",
"default": self.canvas_width,
"min": 1,
"max": 2540,
},
"canvas_height": {
"type": "number",
"label": "Height:",
"default": self.canvas_height,
"min": 1,
"max": 2540,
},
"template": {
"type": "text",
"label": "Filename Template:",
"default": self.template,
},
"delete_images": {
"type": "checkbox",
"label": "Delete image when done",
"default": self.delete_images,
},
"background_color": {
"type": "color",
"label": "Background Color:",
"default": self.background_color,
},
"image_format": {
"type": "dropdown",
"label": "Image Format:",
"options": ["AUTO", "JPEG", "PNG", "GIF", "DZI", "AVIF", "WEBP"],
"default": self.image_format,
},
"image_size": {
"type": "dropdown",
"label": "Image Size:",
"options": ["contain", "cover"],
"default": self.image_size,
},
}
OptionsWindow(self.root, self.apply_options, current_options)
def apply_options(self, options):
"""
Apply the selected options from the options window.
Args:
options (dict): The options to apply.
"""
# if self.log_window:
# self.log_window.clear() # Clear the log window if it exists
self.canvas_width = options["canvas_width"]
self.canvas_height = options["canvas_height"]
self.template = options["template"]
self.delete_images = options["delete_images"]
self.background_color = options["background_color"]
self.image_size = options["image_size"]
self.image_format = options["image_format"]
self.apply_canvas_size()
self.apply_background_color()
self.apply_image_size()
key = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI="
self.config.save_options(self.get_options())
self.update_previews()
def process_product(self, input):
cleaned_input = (input or "").strip()
self.found_products = None
self.current_product = 0
if cleaned_input.isdigit():
product = get_product(int(cleaned_input))
if product:
self.found_products = [product]
else:
self.found_products = search_product(cleaned_input)
if self.found_products:
count_products = len(self.found_products)
print(f"Found {count_products} products")
print(f"Current product {self.current_product}")
print(self.found_products[self.current_product])
self.info_bar.selected_button_label.configure(text=self.found_products[self.current_product]['name'] + " (id: "+ str(self.found_products[self.current_product]['id']) +")" )
number = self.current_product
number += 1
text = f"Viewing product {number}/{count_products}"
self.info_bar.destination_label.configure(text=text)
self.update_previews()
self.update_product_nav_buttons()
return self.found_products[self.current_product]
self.info_bar.destination_label.configure(text="No products found")
self.update_product_nav_buttons()
pass
def change_product(self, data):
if not self.found_products:
self.update_product_nav_buttons()
return
count_products = len(self.found_products)
self.current_product = max(0, min(self.current_product + data, count_products - 1))
if count_products > 0:
count_products = len(self.found_products)
print(self.found_products[self.current_product])
self.info_bar.selected_button_label.configure(text=self.found_products[self.current_product]['name'] + " (id: "+ str(self.found_products[self.current_product]['id'])+")" )
number = self.current_product
number += 1
text = f"Viewing product {number}/{count_products}"
self.info_bar.destination_label.configure(text=text)
self.update_previews()
self.update_product_nav_buttons()
pass
def update_product_nav_buttons(self):
if not getattr(self.info_bar, "next_button", None) or not getattr(self.info_bar, "prev_button", None):
return
total = len(self.found_products) if self.found_products else 0
can_prev = total > 0 and self.current_product > 0
can_next = total > 0 and self.current_product < (total - 1)
self.info_bar.prev_button.configure(state="normal" if can_prev else "disabled")
self.info_bar.next_button.configure(state="normal" if can_next else "disabled")
def update_info(self, selected_option):
"""
Update the info frame based on the selected option.
Args:
selected_option (str): The currently selected option (e.g., "product", "file", etc.).
"""
# Clear previous description and input fields
if self.info_bar.input_field:
self.info_bar.input_field.grid_forget()
if self.info_bar.input_button:
self.info_bar.input_button.grid_forget()
if self.info_bar.next_button:
self.info_bar.next_button.grid_forget()
if self.info_bar.prev_button:
self.info_bar.prev_button.grid_forget()
if self.info_bar.destination_button:
self.info_bar.destination_button.grid_forget()
if self.info_bar.destination_label:
self.info_bar.destination_label.grid_forget()
display_label = selected_option.replace("_", " ").title()
self.info_bar.selected_button_label.configure(text=display_label)
# Update the description and input fields based on the selected option
if selected_option == "product":
self.info_bar.description_label.configure(text="Search")
self.info_bar.input_field = ctk.CTkEntry(self.info_bar.parent_frame)
self.info_bar.input_field.grid(row=1, column=1, columnspan=2, padx=5, pady=5, sticky="ew")
self.info_bar.input_button = ctk.CTkButton(
self.info_bar.parent_frame,
text="Search product:",
command=lambda: self.process_product(self.info_bar.input_field.get())
)
self.info_bar.input_button.grid(row=1, column=3, padx=5, pady=5, sticky="ew")
self.info_bar.next_button = ctk.CTkButton(
self.info_bar.parent_frame,
text="Next",
command=lambda: self.change_product(1)
)
self.info_bar.next_button.grid(row=2, column=3, padx=5, pady=5, sticky="ew")
self.info_bar.prev_button = ctk.CTkButton(
self.info_bar.parent_frame,
text="Prev",
command=lambda: self.change_product(-1)
)
self.info_bar.prev_button.grid(row=2, column=2, padx=5, pady=5, sticky="ew")
# Destination Directory Label (to show the selected destination)
self.info_bar.destination_label = ctk.CTkLabel(
self.info_bar.parent_frame, text="No products found"
)
self.info_bar.destination_label.grid(row=2, column=0, columnspan=2, padx=5, pady=5, sticky="w")
self.update_product_nav_buttons()
elif selected_option == "file":
self.info_bar.description_label.configure(text="Choose a file to process:")
# Browse File Button
self.info_bar.input_button = ctk.CTkButton(
self.info_bar.parent_frame,
text="Browse File",
command=self.browse_file_command
)
self.info_bar.input_button.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
# Destination Directory Button
self.info_bar.destination_button = ctk.CTkButton(
self.info_bar.parent_frame,
text="Select Destination",
command=self.browse_destination_command # Command to browse destination directory
)
self.info_bar.destination_button.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
# Destination Directory Label (to show the selected destination)
self.info_bar.destination_label = ctk.CTkLabel(
self.info_bar.parent_frame, text="No destination selected"
)
self.info_bar.destination_label.grid(row=2, column=0, padx=5, pady=5, sticky="w")
elif selected_option == "directory":
self.info_bar.description_label.configure(text="Choose a directory to process:")
# Browse Directory Button
self.info_bar.input_button = ctk.CTkButton(
self.info_bar.parent_frame,
text="Browse Directory",
command=self.browse_directory_command
)
self.info_bar.input_button.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
# Destination Directory Button
self.info_bar.destination_button = ctk.CTkButton(
self.info_bar.parent_frame,
text="Select Destination",
command=self.browse_destination_command # Command to browse destination directory
)
self.info_bar.destination_button.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
# Destination Directory Label (to show the selected destination)
self.info_bar.destination_label = ctk.CTkLabel(
self.info_bar.parent_frame, text="No destination selected"
)
self.info_bar.destination_label.grid(row=2, column=0, padx=5, pady=5, sticky="w")
def run(self):
"""
Run the main event loop.
"""
self.root.mainloop()