diff --git a/main.py b/main.py index 766c3d5..ee9a8d3 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ import os +import re from datetime import datetime -#from debug import my_debugging_tools # Removed for main push from utility import Utilities from image_handler import ImageProcessor, ExifHandler from tui import SimpleTUI @@ -8,8 +8,7 @@ from tui import SimpleTUI class Optima35: # The layout of class Optima35 was originally made by ChatGPT, but major adjustments have been made. To remain transparent, I disclose this. def __init__(self, settings_file, exif_options_file): - self.version = "0.1.0" - #self.debugger = my_debugging_tools() # Removed for main push + self.version = "0.1.1" self.utilities = Utilities() self.image_processor = ImageProcessor() self.exif_handler = ExifHandler() @@ -27,13 +26,20 @@ class Optima35: "watermark_text": None, "modifications": [], } - self.settings_to_save = ["resize_percentage", "jpg_quality", "png_compression", "web_optimize", "contrast_percentage", "brightness_percentage"] + self.settings_to_save = [ + "resize_percentage", + "jpg_quality", + "png_compression", + "web_optimize", + "contrast_percentage", + "brightness_percentage" + ] self.exif_choices = self.utilities.read_yaml(exif_options_file) self.setting_file = settings_file def load_or_ask_settings(self): """Load settings from a YAML file or ask the user if not present or incomplete.""" - # Partly ChatGPT + # Partially ChatGPT if self.read_settings(self.settings_to_save): for item in self.settings_to_save: print(f"{item}: {self.settings[item]}") @@ -44,12 +50,12 @@ class Optima35: print("No settings found...") print("Asking for new settings...\n") - self.settings["resize_percentage"] = int(input("Default resize percentage: ").strip()) - self.settings["contrast_percentage"] = int(input("Default contrast percentage: ").strip()) - self.settings["brightness_percentage"] = int(input("Default brightness percentage: ").strip()) - self.settings["jpg_quality"] = int(input("JPEG quality (1-100): ").strip()) - self.settings["png_compression"] = int(input("PNG compression level (0-9): ").strip()) - self.settings["web_optimize"] = self.tui.yes_no_menu("Optimize images for web?") + self.settings["resize_percentage"] = self.take_input_and_validate(question = "Default resize percentage (below 100 downscale, above upscale): ", accepted_type = int, min_value = 1, max_value = 200) + self.settings["contrast_percentage"] = self.take_input_and_validate(question = "Default contrast percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100) + self.settings["brightness_percentage"] = self.take_input_and_validate(question = "Default brighness percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100) + self.settings["jpg_quality"] = self.take_input_and_validate(question = "JPEG quality (1-100, 80 default): ", accepted_type = int, min_value = 1, max_value = 100) + self.settings["png_compression"] = self.take_input_and_validate(question = "PNG compression level (0-9, 6 default): ", accepted_type = int, min_value = 0, max_value = 9) + self.settings["web_optimize"] = self.tui.yes_no_menu("Optimize images i.e. compressing?") self.write_settings(self.settings_to_save) @@ -57,7 +63,6 @@ class Optima35: """"Write self.setting, but only specific values""" keys = keys_to_save filtered_settings = {key: self.settings[key] for key in keys if key in self.settings} - self.utilities.write_yaml(self.setting_file, filtered_settings) print("New settings saved successfully.") @@ -70,7 +75,6 @@ class Optima35: keys = keys_to_load if os.path.exists(self.setting_file): loaded_settings = self.utilities.read_yaml(self.setting_file) - for key in keys: if key in loaded_settings: self.settings[key] = loaded_settings[key] @@ -92,43 +96,107 @@ class Optima35: user_data[field] = choise.encode("utf-8") user_data["software"] = f"OPTIMA-35 {self.version}".encode("utf-8") - user_data["date_time_original"] = datetime.now().strftime("%Y:%m:%d %H:%M:%S").encode("utf-8") + new_date = self.get_date_input() + + if new_date: + user_data["date_time_original"] = new_date + return user_data + def get_date_input(self): + # Partially chatGPT + while True: + date_input = input("Enter a date (yyyy-mm-dd): ") + if date_input == "": + return None # Skip if input is empty + try: + new_date = datetime.strptime(date_input, "%Y-%m-%d") + return new_date.strftime("%Y:%m:%d 00:00:00") + except ValueError: + print("Invalid date format. Please enter the date in yyyy-mm-dd format.") + def get_user_settings(self): """Get initial settings from the user.""" - menu_options = ["Resize image", "Change EXIF", "Convert to grayscale", "Change contrast", "Change brightness", "Rename images", "Invert image order", "Add Watermark"] # new option can be added here. - self.settings["input_folder"] = input("Enter path of input folder: ").strip() # Add: check if folder exists + menu_options = [ + "Resize image", + "Change EXIF", + "Convert to grayscale", + "Change contrast", + "Change brightness", + "Rename images", + "Invert image order", + "Add Watermark" + ] # new option can be added here. + + self.settings["input_folder"] = input("Enter path of input folder: ").strip() # Add: check if folder exists. self.settings["output_folder"] = input("Enter path of output folder: ").strip() - self.settings["file_format"] = input("Enter export file format (e.g., jpg, png): ").strip() # Add: specific question depending on export file type, like jpg quality and png compression + self.settings["file_format"] = self.take_input_and_validate(question = "Enter export file format (jpg, png, webp): ", accepted_input = ["jpg", "png", "webp"], accepted_type = str) self.settings["modifications"] = self.tui.multi_select_menu( "Select what you want to do", menu_options ) if "Change EXIF" not in self.settings["modifications"]: self.settings["copy_exif"] = self.tui.yes_no_menu("Do you want to copy exif info from original file?") if "Rename images" in self.settings["modifications"]: - self.settings["new_file_names"] = input("What should be the name for the new images? ") + self.settings["new_file_names"] = input("What should be the name for the new images? ") # Need if "Invert image order" in self.settings["modifications"]: self.settings["invert_image_order"] = True if "Add Watermark" in self.settings["modifications"]: self.settings["watermark_text"] = input("Enter text for watermark. ") - os.makedirs(self.settings["output_folder"], exist_ok = True) + def take_input_and_validate(self, question, accepted_input = None, accepted_type = str, min_value = None, max_value = None): + """ + Asks the user a question, validates the input, and ensures it matches the specified criteria. + Args: + question (str): The question to ask the user. + accepted_input (list): A list of acceptable inputs (optional for non-numeric types). + accepted_type (type): The expected type of input (e.g., str, int, float). + min_value (int/float): Minimum value for numeric inputs (optional). + max_value (int/float): Maximum value for numeric inputs (optional). + + Returns: + The validated user input. + """ + # Main layout by chatGPT, but modified. + while True: + user_input = input(question).strip() + + try: + # Convert input to the desired type + if accepted_type in [int, float]: + user_input = accepted_type(user_input) + # Validate range for numeric types + if (min_value is not None and user_input < min_value) or (max_value is not None and user_input > max_value): + print(f"Input must be between {min_value} and {max_value}.") + continue + elif accepted_type == str: + # No conversion needed for strings + user_input = str(user_input) + else: + raise ValueError(f"Unsupported type: {accepted_type}") + + # Validate against accepted inputs if provided + if accepted_input is not None and user_input not in accepted_input: + print(f"Invalid input. Must be one of: {', '.join(map(str, accepted_input))}.") + continue + + return user_input # Input is valid + + except ValueError: + print(f"Invalid input. Must be of type {accepted_type.__name__}.") + def process_images(self): """Process images based on user settings.""" input_folder = self.settings["input_folder"] output_folder = self.settings["output_folder"] image_files = [ - f for f in os.listdir(input_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg')) + f for f in os.listdir(input_folder) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp")) ] - if "Change EXIF" in self.settings["modifications"]: selected_exif = self.collect_exif_data() i = 1 for image_file in image_files: - input_path = os.path.join(input_folder, image_file) if "Rename images" in self.settings["modifications"]: @@ -146,6 +214,8 @@ class Optima35: image = processed_img, percent = self.settings["resize_percentage"], resample = True ) elif mod == "Change EXIF" and selected_exif: + if "date_time_original" in selected_exif: + selected_exif = self.modify_timestamp_in_exif(selected_exif, image_name) exif_data = self.exif_handler.build_exif_dict(selected_exif, self.image_processor.get_image_size(processed_img)) elif mod == "Convert to grayscale": processed_img = self.image_processor.grayscale(processed_img) @@ -166,13 +236,17 @@ class Optima35: # If an error happends it is because the picture does not have exif data print("Copying EXIF data selected, but no EXIF data is available in the original image file.") exif_data = None - else: + elif "Change EXIF" not in self.settings["modifications"]: exif_data = None self.image_processor.save_image( - image = processed_img, path = output_path, exif_data = exif_data, - file_type = self.settings["file_format"], jpg_quality = self.settings["jpg_quality"], - png_compressing = self.settings["png_compression"], _optimize = self.settings["web_optimize"] + image = processed_img, + path = output_path, + exif_data = exif_data, + file_type = self.settings["file_format"], + jpg_quality = self.settings["jpg_quality"], + png_compressing = self.settings["png_compression"], + optimize = self.settings["web_optimize"] ) self.utilities.progress_bar(i, len(image_files)) i += 1 @@ -180,15 +254,30 @@ class Optima35: def name_images(self, base_name, current_image, total_images, invert): """"Returns name, combination of base_name and ending number.""" total_digits = len(str(total_images)) - if invert: ending_number = total_images - (current_image - 1) else: ending_number = current_image - ending = f"{ending_number:0{total_digits}}" return f"{base_name}_{ending}" + def modify_timestamp_in_exif(self, exif_data, filename): + """"Takes exif data and adjust time to fit ending of filename.""" + try: + last_tree = filename[-3:len(filename)] + total_seconds = int(re.sub(r'\D+', '', last_tree)) + + minutes = total_seconds // 60 + seconds = total_seconds % 60 + time = datetime.strptime(exif_data["date_time_original"], "%Y:%m:%d %H:%M:%S") # change date time string back to an time object for modification + new_time = time.replace(hour=12, minute=minutes, second=seconds) + exif_data["date_time_original"] = new_time.strftime("%Y:%m:%d %H:%M:%S") + return exif_data + + except ValueError: + print("Modifying date went wrong, exiting...") + exit() + def run(self): """Run the main program.""" self.load_or_ask_settings()