From cc2aca0f4d4279a2a59c59716cbe601d1bc48986 Mon Sep 17 00:00:00 2001 From: QuBerto Date: Sat, 13 Jul 2024 22:11:42 +0200 Subject: [PATCH] Update files --- -.pre-commit-config.yaml | 39 +++++ .gitignore | 2 +- api/woocommerce.py | 202 ---------------------- api/woocommerce_api.py | 332 +++++++++++++++++++++++++++++++++++++ config/decrypt_config.py | 55 ++++-- config/encrypt_config.py | 29 +++- main.py | 46 +++-- readme.md | 2 +- ui/local_processing_tab.py | Bin 7503 -> 12579 bytes ui/log_window.py | 7 +- ui/options_window.py | 131 +++++++++++++++ ui/settings_tab.py | Bin 2155 -> 2284 bytes utils/file_operations.py | 191 +++++++++++++++------ utils/image_processing.py | 91 +++++++--- 14 files changed, 813 insertions(+), 314 deletions(-) create mode 100644 -.pre-commit-config.yaml delete mode 100644 api/woocommerce.py create mode 100644 api/woocommerce_api.py create mode 100644 ui/options_window.py diff --git a/-.pre-commit-config.yaml b/-.pre-commit-config.yaml new file mode 100644 index 0000000..c39a4ff --- /dev/null +++ b/-.pre-commit-config.yaml @@ -0,0 +1,39 @@ +# .pre-commit-config.yaml + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 # Use the latest version + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-added-large-files + +- repo: https://github.com/psf/black + rev: 24.4.2 # Use the latest version + hooks: + - id: black +- repo: https://github.com/PyCQA/autoflake + rev: v2.3.1 + hooks: + + - id: autoflake + args: [--remove-all-unused-imports, --remove-unused-variables] +- repo: https://github.com/hhatto/autopep8 + rev: v2.3.1 # select the tag or revision you want, or run `pre-commit autoupdate` + hooks: + - id: autopep8 +- repo: local + hooks: + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + require_serial: true + args: + [ + "-rn", # Only display messages + "-sn", # Don't display the score + ] diff --git a/.gitignore b/.gitignore index 77ff6bf..8006b98 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ config.enc */__pycache__ /build /dist -/temp \ No newline at end of file +/temp diff --git a/api/woocommerce.py b/api/woocommerce.py deleted file mode 100644 index 9f93de2..0000000 --- a/api/woocommerce.py +++ /dev/null @@ -1,202 +0,0 @@ -from cryptography.fernet import Fernet -import json -import os -import requests -import base64 -from woocommerce import API -from tkinter import messagebox -import tempfile -from utils.image_processing import resize_image -import pprint -credentials_file = 'credentials.json' - -# Hardcoded key (replace with your generated key) -key = b'u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=' - -def save_credentials(url, consumer_key, consumer_secret, username, password): - credentials = { - 'url': url, - 'consumer_key': consumer_key, - 'consumer_secret': consumer_secret, - 'username': username, - 'password': password - } - credentials_str = json.dumps(credentials) - fernet = Fernet(key) - encrypted = fernet.encrypt(credentials_str.encode()) - with open('config.enc', 'wb') as file: - file.write(encrypted) - -def load_credentials(): - if not os.path.exists('config.enc'): - return None - fernet = Fernet(key) - with open('config.enc', 'rb') as file: - encrypted = file.read() - decrypted = fernet.decrypt(encrypted).decode() - return json.loads(decrypted) - - -def get_wcapi(): - credentials = load_credentials() - if not credentials: - messagebox.showerror("Error", "No WooCommerce credentials found. Please set them in the settings.") - return None - return API( - url=credentials['url'], - consumer_key=credentials['consumer_key'], - consumer_secret=credentials['consumer_secret'], - version="wc/v3" - ) - -def get_product(id): - wcapi = get_wcapi() - if not wcapi: - return None - result = wcapi.get("products/"+str(id)) - - image_paths = {} - product = result.json() - if product.get('images'): - images = product.get('images') - - if not os.path.exists('temp'): - os.makedirs('temp') - - for index, image in enumerate(images): - image_url = image.get('src') - image_id = image.get('id') - response = requests.get(image_url) - if response.status_code == 200: - file_name = image_url.split('/')[-1] - file_path = os.path.join('temp', file_name) - image_paths[image_id] = file_path - with open(file_path, 'wb') as file: - file.write(response.content) - print(f"Image {index + 1}/{len(images)} downloaded and saved: {file_path}") - else: - print(f"Failed to download image {index + 1}/{len(images)}") - else: - if product.get('name'): - print(f"No images found for {product.get('name')}") - else: - print("No images found") - return image_paths, product - -def upload_image(imgPath): - data = open(imgPath, 'rb').read() - fileName = os.path.basename(imgPath) - credentials = load_credentials() - if not credentials: - messagebox.showerror("Error", "No WordPress credentials found. Please set them in the settings.") - return None - - username = credentials['username'] - password = credentials['password'] - credentials_base64 = base64.b64encode(f"{username}:{password}".encode()) - credentials_base64 = base64.b64encode(f"{username}:{password}".encode()) - url = f"{credentials['url']}/wp-json/wp/v2/media" - headers = { - 'Content-Type': 'image/jpg', - 'Content-Disposition': f'attachment; filename={fileName}', - 'Authorization': f'basic {credentials_base64.decode()}' - } - - try: - res = requests.post(url=url, data=data, headers=headers) - res.raise_for_status() # Raise an HTTPError if the HTTP request returned an unsuccessful status code - newDict = res.json() - newID = newDict.get('id') - link = newDict.get('guid').get("rendered") if newDict.get('guid') else None - print(newID, link) - return newID if newID else False - except requests.exceptions.RequestException as e: - print(f"Error uploading image: {e}") - return False - -def delete_img(image_id): - credentials = load_credentials() - if not credentials: - messagebox.showerror("Error", "No WordPress credentials found. Please set them in the settings.") - return None - - url = f"{credentials['url']}/wp-json/wp/v2/media/{image_id}" - username = credentials['username'] - password = credentials['password'] - credentials_base64 = base64.b64encode(f"{username}:{password}".encode()) - - res = requests.delete(url=url, - headers={'Authorization': f'basic {credentials_base64.decode()}'}, - params={'force': 'true'}) - - if res.status_code == 200: - print(f"Image with ID {image_id} deleted successfully.") - else: - print(f"Failed to delete image with ID {image_id}. Error: {res.text}") - -def update_product(image_ids, old_image_ids, product_id): - wcapi = get_wcapi() - if not wcapi: - return - - product = wcapi.get(f"products/{product_id}").json() - product['images'] = [{'id': image_id} for image_id in image_ids] - response = wcapi.put(f"products/{product_id}", data=product) - if response.status_code == 200: - print(f"Product with ID {product_id} updated successfully with new image IDs.") - else: - print(f"Failed to update product with ID {product_id}. Error: {response.text}") - -def process_product_images(id, name_template, canvas_width, canvas_height): - print(name_template) - image_paths, product = get_product(id) - if not image_paths: - return - - with tempfile.TemporaryDirectory() as temp_output_directory: - print(f"Using temporary directory: {temp_output_directory}") - - old_list = [] - new_list = [] - - for image_id, file_path in image_paths.items(): - output_path = generate_output_path(temp_output_directory, file_path, name_template, product, canvas_width, canvas_height) - resize_image(file_path, output_path, '') - new_id = upload_image(output_path) - if new_id: - old_list.append(image_id) - new_list.append(new_id) - - update_product(new_list, old_list, id) - print("Temporary files processed and uploaded successfully.") - -def generate_output_path(temp_output_directory, file_path, template, product, canvas_width, canvas_height): - # Generate the new filename based on the template - name, ext = os.path.splitext(os.path.basename(file_path)) - width = canvas_width - height = canvas_height - sku = product.get('sku', '') - slug = product.get('name', '') - title = product.get('slug', '') - pprint.pprint(product) - # Here you can add more attributes to the template if needed - new_filename = template.format(name=name, sku=sku, width=width, height=height, slug=slug, title=title) - return os.path.join(temp_output_directory, new_filename + ext) - -def process_all_products(name_template): - wcapi = get_wcapi() - if not wcapi: - return - - page = 1 - while True: - products = wcapi.get("products", params={"per_page": 100, "page": page}).json() - if not products: - break - - for product in products: - process_product_images(product['id'], name_template) - - page += 1 - - messagebox.showinfo("Process Complete", "All product images processing is complete.") diff --git a/api/woocommerce_api.py b/api/woocommerce_api.py new file mode 100644 index 0000000..7ba4a2e --- /dev/null +++ b/api/woocommerce_api.py @@ -0,0 +1,332 @@ +""" +Module for WooCommerce API interactions and image processing. +""" + +import json +import os +import base64 +import tempfile +import pprint +from tkinter import messagebox +from cryptography.fernet import Fernet +import requests +from woocommerce import API +from utils.image_processing import ImageProcessor + +CREDENTIALS_FILE = "credentials.json" + +# Hardcoded key (replace with your generated key) +KEY = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=" + + +def save_credentials(url, consumer_key, consumer_secret, username, password): + """ + Save WooCommerce and WordPress credentials to an encrypted file. + + Args: + url (str): The base URL for the WooCommerce store. + consumer_key (str): The consumer key for WooCommerce API. + consumer_secret (str): The consumer secret for WooCommerce API. + username (str): The username for WordPress. + password (str): The password for WordPress. + """ + credentials = { + "url": url, + "consumer_key": consumer_key, + "consumer_secret": consumer_secret, + "username": username, + "password": password, + } + credentials_str = json.dumps(credentials) + fernet = Fernet(KEY) + encrypted = fernet.encrypt(credentials_str.encode()) + with open("config.enc", "wb") as file: + file.write(encrypted) + + +def load_credentials(): + """ + Load WooCommerce and WordPress credentials from an encrypted file. + + Returns: + dict: The decrypted credentials, or None if the file does not exist. + """ + if not os.path.exists("config.enc"): + return None + fernet = Fernet(KEY) + with open("config.enc", "rb") as file: + encrypted = file.read() + decrypted = fernet.decrypt(encrypted).decode() + return json.loads(decrypted) + + +def get_wcapi(): + """ + Get a WooCommerce API client instance. + + Returns: + woocommerce.API: The WooCommerce API client instance, or None if credentials are missing. + """ + credentials = load_credentials() + if not credentials: + messagebox.showerror( + "Error", + "No WooCommerce credentials found. Please set them in the settings.", + ) + return None + return API( + url=credentials["url"], + consumer_key=credentials["consumer_key"], + consumer_secret=credentials["consumer_secret"], + version="wc/v3", + ) + + +def get_product(product_id): + """ + Get a WooCommerce product and download its images. + + Args: + product_id (int): The ID of the WooCommerce product. + + Returns: + tuple: A dictionary of image paths and the product data. + """ + wcapi = get_wcapi() + if not wcapi: + return None + result = wcapi.get(f"products/{product_id}") + + image_paths = {} + product = result.json() + if product.get("images"): + images = product.get("images") + + if not os.path.exists("temp"): + os.makedirs("temp") + + for index, image in enumerate(images): + image_url = image.get("src") + image_id = image.get("id") + response = requests.get(image_url, timeout=10) + if response.status_code == 200: + file_name = image_url.split("/")[-1] + file_path = os.path.join("temp", file_name) + image_paths[image_id] = file_path + with open(file_path, "wb") as file: + file.write(response.content) + print( + f"Image {index + 1}/{len(images)} downloaded and saved: {file_path}" + ) + else: + print(f"Failed to download image {index + 1}/{len(images)}") + else: + if product.get("name"): + print(f"No images found for {product.get('name')}") + else: + print("No images found") + return image_paths, product + + +def upload_image(img_path): + """ + Upload an image to WordPress. + + Args: + img_path (str): The path to the image file. + + Returns: + int: The ID of the uploaded image, or False if the upload failed. + """ + with open(img_path, "rb") as img_file: + data = img_file.read() + file_name = os.path.basename(img_path) + credentials = load_credentials() + if not credentials: + messagebox.showerror( + "Error", "No WordPress credentials found. Please set them in the settings." + ) + return None + + username = credentials["username"] + password = credentials["password"] + credentials_base64 = base64.b64encode(f"{username}:{password}".encode()) + url = f"{credentials['url']}/wp-json/wp/v2/media" + headers = { + "Content-Type": "image/jpg", + "Content-Disposition": f"attachment; filename={file_name}", + "Authorization": f"basic {credentials_base64.decode()}", + } + + try: + res = requests.post(url=url, data=data, headers=headers, timeout=10) + res.raise_for_status() + response_dict = res.json() + new_id = response_dict.get("id") + link = ( + response_dict.get("guid").get("rendered") + if response_dict.get("guid") + else None + ) + print(new_id, link) + return new_id if new_id else False + except requests.exceptions.RequestException as e: + print(f"Error uploading image: {e}") + return False + + +def delete_img(image_id): + """ + Delete an image from WordPress. + + Args: + image_id (int): The ID of the image to delete. + """ + credentials = load_credentials() + if not credentials: + messagebox.showerror( + "Error", "No WordPress credentials found. Please set them in the settings." + ) + return None + + url = f"{credentials['url']}/wp-json/wp/v2/media/{image_id}" + username = credentials["username"] + password = credentials["password"] + credentials_base64 = base64.b64encode(f"{username}:{password}".encode()) + + res = requests.delete( + url=url, + headers={"Authorization": f"basic {credentials_base64.decode()}"}, + params={"force": "true"}, + timeout=10, + ) + + if res.status_code == 200: + print(f"Image with ID {image_id} deleted successfully.") + else: + print(f"Failed to delete image with ID {image_id}. Error: {res.text}") + + +def update_product(image_ids, product_id): + """ + Update a WooCommerce product with new image IDs. + + Args: + image_ids (list): A list of new image IDs. + product_id (int): The ID of the WooCommerce product. + """ + wcapi = get_wcapi() + if not wcapi: + return + + product = wcapi.get(f"products/{product_id}").json() + product["images"] = [{"id": image_id} for image_id in image_ids] + response = wcapi.put(f"products/{product_id}", data=product) + if response.status_code == 200: + print( + f"Product with ID {product_id} updated successfully with new image IDs.") + else: + print( + f"Failed to update product with ID {product_id}. Error: {response.text}") + + +def process_product_images(product_id, name_template, canvas_width, canvas_height): + """ + Process images for a WooCommerce product by resizing and uploading them. + + Args: + product_id (int): The ID of the WooCommerce product. + name_template (str): The template for generating image filenames. + canvas_width (int): The width of the canvas for resizing images. + canvas_height (int): The height of the canvas for resizing images. + """ + print(name_template) + image_paths, product = get_product(product_id) + if not image_paths: + return + + with tempfile.TemporaryDirectory() as temp_output_directory: + print(f"Using temporary directory: {temp_output_directory}") + + old_list = [] + new_list = [] + + for image_id, file_path in image_paths.items(): + output_path = generate_output_path( + temp_output_directory, + file_path, + name_template, + product, + canvas_width, + canvas_height, + ) + resize_image(file_path, output_path, "") + new_id = upload_image(output_path) + if new_id: + old_list.append(image_id) + new_list.append(new_id) + + update_product(new_list, product_id) + print("Temporary files processed and uploaded successfully.") + + +def generate_output_path( + temp_output_directory, file_path, template, product, canvas_width, canvas_height +): + """ + Generate the output path for resized images based on a template. + + Args: + temp_output_directory (str): The path to the temporary output directory. + file_path (str): The original file path. + template (str): The template for generating the new filename. + product (dict): The WooCommerce product data. + canvas_width (int): The width of the canvas for resizing images. + canvas_height (int): The height of the canvas for resizing images. + + Returns: + str: The generated output path. + """ + name, ext = os.path.splitext(os.path.basename(file_path)) + width = canvas_width + height = canvas_height + sku = product.get("sku", "") + slug = product.get("name", "") + title = product.get("slug", "") + pprint.pprint(product) + new_filename = template.format( + name=name, sku=sku, width=width, height=height, slug=slug, title=title + ) + return os.path.join(temp_output_directory, new_filename + ext) + + +def process_all_products(name_template, canvas_width, canvas_height): + """ + Process images for all WooCommerce products by resizing and uploading them. + + Args: + name_template (str): The template for generating image filenames. + canvas_width (int): The width of the canvas for resizing images. + canvas_height (int): The height of the canvas for resizing images. + """ + wcapi = get_wcapi() + if not wcapi: + return + + page = 1 + while True: + products = wcapi.get("products", params={ + "per_page": 100, "page": page}).json() + if not products: + break + + for product in products: + process_product_images( + product["id"], name_template, canvas_width, canvas_height + ) + + page += 1 + + messagebox.showinfo( + "Process Complete", "All product images processing is complete." + ) diff --git a/config/decrypt_config.py b/config/decrypt_config.py index 712deed..b637c00 100644 --- a/config/decrypt_config.py +++ b/config/decrypt_config.py @@ -1,30 +1,63 @@ -from cryptography.fernet import Fernet +""" +Module for decrypting configuration files using Fernet symmetric encryption. +""" + import json import os +from cryptography.fernet import Fernet + class ConfigDecryptor: - def __init__(self, key): - self.key = key + """ + Class to handle decryption of configuration files. + """ + + def __init__(self, decryption_key): + """ + Initialize the ConfigDecryptor with a given decryption key. + + Args: + decryption_key (bytes): The key to use for decryption. + """ + self.decryption_key = decryption_key def decrypt(self): + """ + Decrypt the 'config.enc' file and return the configuration data. + + Returns: + dict: The decrypted configuration data. + + Raises: + FileNotFoundError: If the 'config.enc' file does not exist. + Exception: If any other error occurs during decryption. + """ if not os.path.exists("config.enc"): - raise FileNotFoundError("The encrypted configuration file 'config.enc' does not exist.") - - fernet = Fernet(self.key) + raise FileNotFoundError( + "The encrypted configuration file 'config.enc' does not exist." + ) + + fernet = Fernet(self.decryption_key) with open("config.enc", "rb") as encrypted_file: encrypted = encrypted_file.read() decrypted = fernet.decrypt(encrypted).decode() return json.loads(decrypted) + + def hello_world(self): + """ + Placeholder + """ + return "Hello world" + + # Define your key here -key = b'u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=' # Replace with your actual key +# Replace with your actual key +DECRYPTION_KEY = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=" if __name__ == "__main__": - key = b'u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI=' # Replace with your actual key - decryptor = ConfigDecryptor(key) + decryptor = ConfigDecryptor(DECRYPTION_KEY) try: config = decryptor.decrypt() print(config) except FileNotFoundError as e: print(e) - except Exception as e: - print(f"An error occurred: {e}") diff --git a/config/encrypt_config.py b/config/encrypt_config.py index 934695c..84d1be7 100644 --- a/config/encrypt_config.py +++ b/config/encrypt_config.py @@ -1,20 +1,45 @@ +""" +Module for encrypting configuration files using Fernet symmetric encryption. +""" + from cryptography.fernet import Fernet + class ConfigEncryptor: + """ + Class to handle encryption of configuration data. + """ + def __init__(self): + """ + Initialize the ConfigEncryptor with a generated encryption key. + """ self.key = Fernet.generate_key() def encrypt_config(self, data): + """ + Encrypt the configuration data and save it to 'config.enc'. + + Args: + data (str): The configuration data to be encrypted. + """ fernet = Fernet(self.key) encrypted = fernet.encrypt(data.encode()) with open("config.enc", "wb") as encrypted_file: encrypted_file.write(encrypted) def get_key(self): + """ + Get the generated encryption key. + + Returns: + str: The generated encryption key as a string. + """ return self.key.decode() + if __name__ == "__main__": - config_data = """ + CONFIG_DATA = """ { "url": "https://yourstore.com", "consumer_key": "ck_yourconsumerkey", @@ -25,4 +50,4 @@ if __name__ == "__main__": """ encryptor = ConfigEncryptor() print(f"Encryption key: {encryptor.get_key()}") - encryptor.encrypt_config(config_data) + encryptor.encrypt_config(CONFIG_DATA) diff --git a/main.py b/main.py index 8bc85f6..b84372d 100644 --- a/main.py +++ b/main.py @@ -1,46 +1,68 @@ +""" +Main module for the Image Processor application. +""" + import tkinter as tk from tkinter import ttk from ui.log_window import LogWindow from ui.local_processing_tab import LocalProcessingTab from ui.settings_tab import SettingsTab -from config.decrypt_config import ConfigDecryptor, key +from config.decrypt_config import ConfigDecryptor, DECRYPTION_KEY + class ImageProcessorApp: + """ + Main application class for the Image Processor. + """ + def __init__(self, root): + """ + Initialize the ImageProcessorApp. + + Args: + root (tk.Tk): The root Tkinter window. + """ self.root = root self.root.title("Image Processor") self.root.geometry("700x400") - + self.tab_parent = ttk.Notebook(self.root) self.log_window = None - self.local_processing_tab = LocalProcessingTab(self.tab_parent, "Local Processing", self.open_log_window) + self.local_processing_tab = LocalProcessingTab( + self.tab_parent, "Local Processing", self.open_log_window + ) self.settings_tab = SettingsTab(self.tab_parent, "Settings") - self.tab_parent.pack(expand=True, fill='both') + self.tab_parent.pack(expand=True, fill="both") def open_log_window(self): + """ + Open the log window. If it already exists, bring it to the front. + """ if self.log_window is None or not self.log_window.winfo_exists(): self.log_window = LogWindow(self.root) else: self.log_window.lift() def run(self): + """ + Run the Tkinter main loop. + """ self.root.mainloop() + if __name__ == "__main__": try: - decryptor = ConfigDecryptor(key) + decryptor = ConfigDecryptor(DECRYPTION_KEY) config = decryptor.decrypt() - wc_url = config['url'] - wc_consumer_key = config['consumer_key'] - wc_consumer_secret = config['consumer_secret'] - wp_username = config['username'] - wp_password = config['password'] + wc_url = config["url"] + wc_consumer_key = config["consumer_key"] + wc_consumer_secret = config["consumer_secret"] + wp_username = config["username"] + wp_password = config["password"] except FileNotFoundError as e: print(f"File not found: {e}") - except Exception as e: - print(f"An error occurred: {e}") root = tk.Tk() app = ImageProcessorApp(root) diff --git a/readme.md b/readme.md index 12ef16f..d965a96 100644 --- a/readme.md +++ b/readme.md @@ -69,4 +69,4 @@ pyinstaller --onefile --windowed main.py --windowed: Ensures the console window does not appear when running the GUI application. Locate the Executable: -After running the command, you will find the executable in the dist folder within your project directory. \ No newline at end of file +After running the command, you will find the executable in the dist folder within your project directory. diff --git a/ui/local_processing_tab.py b/ui/local_processing_tab.py index 0c41112cca6994314c86a8668888bf8beff8552c..2122f4bfa1876d5ded30333aa8e88caf4fbc012d 100644 GIT binary patch literal 12579 zcmeHKO>g5i5bZfY{sW=CD5xN8wNGi4)?0@eJ zMTxR(x7+TmpbxQ3%?xMWym@4zD7w7(NtT^pq>_r5H6wSjpn`l*vS3>CWMVQe7x11S<{O8RD!S+}Z-xPRChn5^_hEOeEw@XN+tT^1d-@pOA%Y1$)F7 z4@NJcy{4F+};hFSr8IpzZOH=WTPX`!*Gv^Ghu^~vE&@g>yTar)u1?6XPK6lWX;)FlQ`utYbiUoDzOP|efl7MfI(7vUK-5w+%+TB?X4hd$SjW0<6s=k8tG$|8Dk%+^ z4qhoRr;@pGBNSf|@(jjEsvc)+?Z~BA#h_gU=)nlc*u#{TrEP2qTlWpk5FXgxn-3)n zbj`@UY#`jsqM*$qMNXGy4XOU{{P}5vHRG!_7ISQY(j};2@G^SRqFa5+pY*1Kzc%Wp z-Ba)U3G{9##%5PC!3=ZL2i`YS=;I8v>~MU|zQr9z)rlXe=vWpB6yRu%e6eQ5W+`|1 zBURw#1#XsN0`P`f8x23)e5jW)SRi=GxEAKvIbFu%mgsCWj$A&fB@gEfNVKH z4#P@2VZg|W%Ta$cGhLf^e7b=JuWL)d9OTpb;p-k|E9ZLw%P-M|knl&cwyNWJOxSXuJUsEU#WDII2 zbPzk#zN5DKiciS63r{90JS_D(dX5((atLx-$fn{e2x8G|q^eI0?1%?%G*c!9b$x&T zE*X>mnnb@hXB(Mn20CnA&H{ay#@NKO_HMv<2_i-HMjwvYIG0%K*#2l|CdQv!@w6*>B%9i?mZnffqKI6MJ!+pAV6XwE!8tKEiPt zAzdk6#!7B8AaWr^S2y_7(sGx5XTSIOtwG*4dmz6j={PhlT&ZLWH>C!0!qf7ttd|lr z`P7);+V z`fUgq^<&(1360e(U)o!}YfOK|pY3gP^!JPCjdg_0@j{JaWer@ok}F`QRQue$^}U?K zc#wbx@x^C@=tDmvAEg9qKr}g-<{rAP^7;2m}NI0s(=5KtLcM5D*9m1Ox&C o0fB%(Kp-Fx5C{ka1Ofs9fq+0jARrJB2nYlO0s;Yn|33nM0SqVGoB#j- literal 7503 zcmb_hUvJws5P#37Ae@Idm?5McI&4K%py?khSWy&6wmxiwKufeum=*<*j*}Jr?zoy5CY+#;xW;W%#6!4Qr|9vR0j_c`J8{ z6%}t;sb%{(BTXwyrc_uec3iItA(u;DFK1-Qbg|&A(gjB~iiYY{ypdbAQh=k|NdaF; z>wBsS#s2~ITZW%jiWNW`p6#S8_ba8Y#L-;dcv@l(W)|=X0T>A%W=m*>zTSu0sOj>JJ{rttXcnY*)pmbdd5*r zFV7MRi}#LKdIchWxw;yCzG8g2(vF}{q2v}kQRv5pp`q*SR=42PKWIC>r(()Nj|M@Bbx!WmgXt|!UhXxSJ@7gV#RmdJ9znDwCx^ON-)RAEWxdPIFJ4^0Y1eXtF zv>_{Hz2M8KQ?-`uh6<2~nBrfbnAeK6dI~;z|LNU~IN9U}GWo0Chf<~(oI=B~Pi$WY z57$0&7ka!BOjEV)g`E#q7j524A4q60>6*4Y-GoPNs*y;jW&^Z$}Y(C=GG)YXH zv^zM4FH_TF8QHSs)f94i4sI@`*lue4*3jx9e_=i!@l$DDt{)*ZBJ4*MJN4W{N(iJ-xU&B_pmTPkJ zZans#M+;sR2jf4;8iao;=KnCnpcn~;-k^Q7=kq;&^?pbYO<962KmG?$Z-4uJ96@L& zIOY~AiW<6)!!W+7Dx$WF1%DX8I4WyQe>gR#r;}9rv4D>9jcElz`;=Buh~7b)ETB@3 zL#o}=!&rWRh8j^PoxlO4eOY5#e;1Zs?6DyB88TN^p)5?kDzRODE554r7XF*I;M>Dj z+bMiY^JpT*SDFyokrQm~QP4um z@w*&gWgd%uL>)s9y?6?6OzF`Lx<=?is1NY89TXuRSCho7FOSCWS924H0USsPFoyd1 zT`)S;j>R!1KLBKp)FTmQw}yX zsdM2v(`p<+xaW!b@jk%>@6P-r69&ip+|GeuZo}?5+a)fYGw}Kmox!!C8yx(YJ*W;h zh|}&C7H+GGoe%Bp_)_c!OhJvGPF15H?Q{Wiy2F|A4U!FjiI{^z0hc2aqQS+AIt`Wq zPf#MDazF_?E5dvH=(PlOi|_IFKAS^nZylwIOO> zZ`cal;f|BY+;auJy~l=Wp{pGJL=O77RDZL*d*5Lid$0rE4B5#+uhYl$8I*|mY#$pl z_B-2@v-g6k1P-kygzU-#4VpKBSD?Rb;R{sdtOvo z^ZR9{#8Ndu_fIan+>?w@T-iWmJGi1(8CFU6Aew%=l?1(D3=@5GTwR;=BepZhS3Nw^ s?p1_etBMg7qt`s0)^^D{d8qJy)q2C#as6VF-1x&yv$iV^s7iQO4G5`Po diff --git a/ui/log_window.py b/ui/log_window.py index 91dd43a..17d35a5 100644 --- a/ui/log_window.py +++ b/ui/log_window.py @@ -1,17 +1,18 @@ from tkinter import Toplevel, Text + class LogWindow(Toplevel): def __init__(self, master=None, **kwargs): super().__init__(master, **kwargs) self.title("Log Window") self.geometry("500x300") self.text = Text(self) - self.text.pack(expand=True, fill='both') + self.text.pack(expand=True, fill="both") self.protocol("WM_DELETE_WINDOW", self.hide) def log(self, message): - self.text.insert('end', message + '\n') - self.text.see('end') + self.text.insert("end", message + "\n") + self.text.see("end") def hide(self): self.withdraw() diff --git a/ui/options_window.py b/ui/options_window.py new file mode 100644 index 0000000..08afe2f --- /dev/null +++ b/ui/options_window.py @@ -0,0 +1,131 @@ +import tkinter as tk +from tkinter import ttk + + +class OptionsWindow(tk.Toplevel): + def __init__(self, parent, apply_callback, current_options): + super().__init__(parent) + self.title("Options") + self.geometry("400x400") + + self.apply_callback = apply_callback + self.options = current_options + self.inputs = {} + + self.setup_ui() + + def setup_ui(self): + """ + Set up the UI components. + """ + self.row_index = 0 + for name, details in self.options.items(): + if details["type"] == "number": + self.add_number_input( + name, + details["label"], + details["default"], + details["min"], + details["max"], + ) + elif details["type"] == "text": + self.add_text_input(name, details["label"], details["default"]) + elif details["type"] == "checkbox": + self.add_checkbox(name, details["label"], details["default"]) + + self.create_apply_button() + + def add_number_input(self, name, label, default, min_val, max_val): + """ + Add a number input field. + + Args: + name (str): The name of the input field. + label (str): The label for the input field. + default (int): The default value. + min_val (int): The minimum value. + 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") + + entry = tk.Entry(self) + entry.insert(0, str(default)) + entry.grid(row=self.row_index, column=1, padx=5, pady=5, sticky="w") + + self.inputs[name] = { + "type": "number", + "widget": entry, + "min": min_val, + "max": max_val, + } + self.row_index += 1 + + def add_text_input(self, name, label, default): + """ + Add a text input field. + + Args: + name (str): The name of the input field. + label (str): The label for the input field. + 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") + + entry = tk.Entry(self) + entry.insert(0, default) + entry.grid(row=self.row_index, column=1, padx=5, pady=5, sticky="w") + + self.inputs[name] = {"type": "text", "widget": entry} + self.row_index += 1 + + def add_checkbox(self, name, label, default): + """ + Add a checkbox. + + Args: + name (str): The name of the input field. + label (str): The label for the input field. + default (bool): The default value. + """ + var = tk.BooleanVar(value=default) + chk = tk.Checkbutton(self, text=label, variable=var) + chk.grid(row=self.row_index, column=0, + columnspan=2, padx=5, pady=5, sticky="w") + + self.inputs[name] = {"type": "checkbox", "variable": var} + self.row_index += 1 + + def create_apply_button(self): + """ + Create the apply button. + """ + apply_button = tk.Button( + self, text="Apply", command=self.apply_options) + apply_button.grid(row=self.row_index, column=0, columnspan=2, pady=10) + + def apply_options(self): + """ + Apply the options and call the callback function. + """ + options = {} + for name, details in self.inputs.items(): + if details["type"] == "number": + value = int(details["widget"].get()) + min_val = details["min"] + max_val = details["max"] + if min_val <= value <= max_val: + options[name] = value + else: + messagebox.showerror( + "Error", f"{name} must be between {min_val} and {max_val}" + ) + return + elif details["type"] == "text": + options[name] = details["widget"].get() + elif details["type"] == "checkbox": + options[name] = details["variable"].get() + + self.apply_callback(options) + self.destroy() diff --git a/ui/settings_tab.py b/ui/settings_tab.py index 51191fed7533e499c12818088c40313ec79d3c0c..820a444650b5ea37197651bf7a7246d12473f037 100644 GIT binary patch literal 2284 gcmZQz7zLvtFd71*Aut*OqaiRF0;3@?Y(szx00{N~3jhEB literal 2155 zcmb7_yN;YN6oz}A!lD_75K5C~C7O(qX17>nvPx-rz_YOc8`&n4dHeVR##{^=D$D-Q z#h(x7khWu70$CF(F$YMfWpv}Lbz5XmjQgk~`JJ(nwJqi)2E(~Pe=saLt}vAZH6jBI zLzTZ1MP-A802eGJq3_}+x;{lfeHFd|2ni(;!bITamH~;butQw=QfvHIrdFxdT9B)A zPyi|G{5wZ2P9}Y7Tx#L8m!qmOj^xF^`|7@%Y`7+5nZBD4bTqd70OV(qMzhjx4l=75;(l~{0~qUW3b7}_Z5h1>wSEp!5~OX)y8jj zFXB&LNrpXq9G{K5PteaTaG73JFjZ~fnBLo<^s&vY10n2BGsX#mTK*$5gF`N3CYwOU z!`*V~RD-=UULBKo9h5$ax%D0irwoz^mdxDrkjW_DWHKJ^GBK0s+Dpl(ZH!;d+I7ZZ zAjB6n!N7T%U~vZYkVCX8%4z`UmRau-5

') - - x_offset = int((canvas_width - img.width) / 2) - y_offset = int((canvas_height - img.height) / 2) - - with Image(width=canvas_width, height=canvas_height, background=Color('transparent')) 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(output_path)[1] - # Construct the final output path - 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}") -set_canvas_size(900, 900) \ No newline at end of file + def set_background_color(self, color): + """ + Set the background color. + """ + self.background_color = Color(color) + + def resize_image(self, image_path, output_path, additional_name=None): + """ + Resize and process the image. + """ + # 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}>") + + 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: + 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(output_path)[1] + # Construct the final output path + 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}") + + +# Example usage +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")