diff --git a/main_tui.py b/main_tui.py new file mode 100644 index 0000000..07c7537 --- /dev/null +++ b/main_tui.py @@ -0,0 +1,292 @@ +import os +import re +from datetime import datetime +from utils.utility import Utilities +from utils.image_handler import ImageProcessor, ExifHandler +from ui.tui import SimpleTUI +# legacy code, will be removed with then next minor version +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.name = "OPTIMA-35" + self.version = "0.2.1" + self.utilities = Utilities() + self.image_processor = ImageProcessor() + self.exif_handler = ExifHandler() + self.tui = SimpleTUI() + self.settings = { + "input_folder": None, + "output_folder": None, + "file_format": None, + "resize_percentage": None, + "copy_exif": None, + "contrast_percentage": None, + "brightness_percentage": None, + "new_file_names": None, + "invert_image_order": False, + "watermark_text": None, + "modifications": [], + } + 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.""" + # Partially ChatGPT + if self.read_settings(self.settings_to_save): + for item in self.settings_to_save: + print(f"{item}: {self.settings[item]}") + use_saved = self.tui.yes_no_menu("Use these settings?") + if use_saved: + return + else: + print("No settings found...") + + print("Asking for new settings...\n") + 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) + + def write_settings(self, keys_to_save): + """"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.") + + def read_settings(self, keys_to_load): + """ + Read settings from the settings file and update self.settings + with the values for specific keys without overwriting existing values. + """ + # First draft by ChatGPT, adjusted to fit my needs. + 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] + print("Settings loaded successfully.") + return True + else: + print("Settings file empty.") + return False + + def collect_exif_data(self): + """Collect EXIF data based on user input.""" + user_data = {} + fields = [ + "make", "model", "lens", "iso", "image_description", + "user_comment", "artist", "copyright_info" + ] + for field in fields: + choise = self.tui.choose_menu(f"Enter {field.replace('_', ' ').title()}", self.exif_choices[field]) + user_data[field] = choise.encode("utf-8") + + user_data["software"] = f"OPTIMA-35 {self.version}".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. + self.settings["output_folder"] = input("Enter path of output folder: ").strip() + 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( + f"\n{self.name} v.{self.version} \nSelect what you want to do (esc or q to exit)", + 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? ") # 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", ".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"]: + image_name = self.name_images(self.settings["new_file_names"], i, len(image_files), self.settings["invert_image_order"]) + else: + image_name = os.path.splitext(image_file)[0] + + output_path = os.path.join(output_folder, image_name) + + with self.image_processor.open_image(input_path) as img: + processed_img = img + for mod in self.settings["modifications"]: + if mod == "Resize image": + processed_img = self.image_processor.resize_image( + 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) + elif mod == "Change contrast": + processed_img = self.image_processor.change_contrast(processed_img, self.settings["contrast_percentage"]) + elif mod == "Change brightness": + processed_img = self.image_processor.change_brightness(processed_img, self.settings["brightness_percentage"]) + elif mod == "Add Watermark": + processed_img = self.image_processor.add_watermark(processed_img, self.settings["watermark_text"]) + + if self.settings["copy_exif"]: + # When copying exif from original, make sure to change Piexel X & Y Dimension to fit new size + try: + og_exif = self.exif_handler.get_exif_info(img) + og_exif["Exif"][40962], og_exif["Exif"][40963] = self.image_processor.get_image_size(processed_img) + exif_data = og_exif + except Exception: + # 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 + 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"] + ) + self.utilities.progress_bar(i, len(image_files)) + i += 1 + + 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() + self.get_user_settings() + self.process_images() + print("Done") + +if __name__ == "__main__": + app = Optima35("config/settings.yaml", "config/exif_options.yaml") + app.run()