From b792318a534a105eb0f4dfc71f131b583001c2f1 Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Sat, 28 Dec 2024 00:26:27 +0100 Subject: [PATCH 1/7] Adding ignore setting file. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c427ec0..197fa4e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ debug.* debug_log/ .ropeproject/ __pycache__/ +config/settings.yaml From ab68c8b56aed7bc7cf803434c8699fb0b1e66989 Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Sat, 28 Dec 2024 00:27:23 +0100 Subject: [PATCH 2/7] Adjusting file to work with new folder/filder structure. --- main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 0dc60ae..4c4b202 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,9 @@ import os import re from datetime import datetime -from utility import Utilities -from image_handler import ImageProcessor, ExifHandler -from tui import SimpleTUI +from utils.utility import Utilities +from utils.image_handler import ImageProcessor, ExifHandler +from ui.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. @@ -286,5 +286,5 @@ class Optima35: print("Done") if __name__ == "__main__": - app = Optima35("settings.yaml", "exif_options.yaml") + app = Optima35("config/settings.yaml", "config/exif_options.yaml") app.run() From 7701a8f2ea8b43236757f1ff36bd55f95f11042e Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Sat, 28 Dec 2024 00:27:35 +0100 Subject: [PATCH 3/7] Moving configs to this folder --- config/exif_options.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 config/exif_options.yaml diff --git a/config/exif_options.yaml b/config/exif_options.yaml new file mode 100644 index 0000000..9a80688 --- /dev/null +++ b/config/exif_options.yaml @@ -0,0 +1,28 @@ +make: + - Nikon +model: + - FG + - F50 +lens: + - Nikon LENS SERIES E 50mm + - AF NIKKOR 35-70mm +iso: # Numeric values cause errors with simple_term_menu and must be quoted. This issue will be resolved with the future UI switch. + - "100" + - "200" + - "400" + - "800" + - "1000" + - "1600" + - "3200" +image_description: + - ILFORD DELTA 3200 + - ILFORD ILFOCOLOR + - LomoChrome Turquoise +user_comment: + - Scanner.NORITSU-KOKI + - Scanner.NA +artist: + - Mr. Finchum +copyright_info: + - All Rights Reserved + - No Copyright From 3e8ec921ab2738feb37ce725d4555bede14531c9 Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Sat, 28 Dec 2024 00:27:52 +0100 Subject: [PATCH 4/7] Moving tui file, new ui will be placed here --- ui/__init__.py | 0 ui/tui.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 ui/__init__.py create mode 100644 ui/tui.py diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/tui.py b/ui/tui.py new file mode 100644 index 0000000..f8abd32 --- /dev/null +++ b/ui/tui.py @@ -0,0 +1,60 @@ +from simple_term_menu import TerminalMenu + +class SimpleTUI: + """TUI parts using library simple_term_menu""" + def __init__(self): + pass + + def choose_menu(self, menu_title, choices): + """ Dynamic function to display content of a list and returnes which was selected.""" + menu_options = choices + menu = TerminalMenu( + menu_entries = menu_options, + title = menu_title, + menu_cursor = "> ", + menu_cursor_style = ("fg_gray", "bold"), + menu_highlight_style = ("bg_gray", "fg_black"), + cycle_cursor = True, + clear_screen = False + ) + menu.show() + return menu.chosen_menu_entry + + def multi_select_menu(self, menu_title, choices): + """ Dynamic function to display content of a list and returnes which was selected.""" + menu_options = choices + menu = TerminalMenu( + menu_entries = menu_options, + title = menu_title, + multi_select=True, + show_multi_select_hint=True, + menu_cursor_style = ("fg_gray", "bold"), + menu_highlight_style = ("bg_gray", "fg_black"), + cycle_cursor = True, + clear_screen = False + ) + menu.show() + choisen_values = menu.chosen_menu_entries + + if choisen_values == None: + print("Exiting...") + exit() + else: + return menu.chosen_menu_entries + + def yes_no_menu(self, message): # oh + menu_options = ["[y] yes", "[n] no"] + menu = TerminalMenu( + menu_entries = menu_options, + title = f"{message}", + menu_cursor = "> ", + menu_cursor_style = ("fg_red", "bold"), + menu_highlight_style = ("bg_gray", "fg_black"), + cycle_cursor = True, + clear_screen = False + ) + menu_entry_index = menu.show() + if menu_entry_index == 0: + return True + elif menu_entry_index == 1: + return False From 5c61ff0587f3ddab37fe2455bcc7605fc7a9392a Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Sat, 28 Dec 2024 00:28:37 +0100 Subject: [PATCH 5/7] moving image handler and utility file to new folder --- utils/__init__.py | 0 utils/image_handler.py | 115 +++++++++++++++++++++++++++++++++++++++++ utils/utility.py | 47 +++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 utils/__init__.py create mode 100644 utils/image_handler.py create mode 100644 utils/utility.py diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/image_handler.py b/utils/image_handler.py new file mode 100644 index 0000000..6631a93 --- /dev/null +++ b/utils/image_handler.py @@ -0,0 +1,115 @@ +from PIL import Image, ImageDraw, ImageFont, ImageEnhance +import piexif +import time + +class ImageProcessor: + """Functions using pillow are in here.""" + def __init__(self): + pass + + def open_image(self, path): + """Open an image from path, returns image object.""" + return Image.open(path) + + def get_image_size(self, image): + """Simply get image size.""" + return image.size + + def grayscale(self, image): + """Change to grayscale""" + return image.convert("L") + + def change_contrast(self, image, change): + enhancer = ImageEnhance.Contrast(image) + new_img = enhancer.enhance(1 + (change/100)) + return new_img + + def change_brightness(self, image, change): + enhancer = ImageEnhance.Brightness(image) + new_img = enhancer.enhance(1 + (change/100)) + return new_img + + def resize_image(self, image, percent, resample = True): + """Resize an image by giving a percent.""" + new_size = tuple(int(x * (percent / 100)) for x in image.size) + if resample: + resized_image = image.resize(new_size) + else: + resized_image = image.resize((new_size),resample=Image.Resampling.NEAREST) + return resized_image + + def add_watermark(self, image, text, font_size_scale = 70): + drawer = ImageDraw.Draw(image) + imagewidth, imageheight = image.size + margin = (imageheight / 100 ) * 2 # margin dynamic, 2% of image size + font_size = imagewidth / font_size_scale # Scaling the font size + try: # Try loading front, if notaviable return unmodified image + font = ImageFont.truetype("OpenDyslexic3-Regular.ttf", font_size) + except: + print("Error loading font for watermark, please ensure font is installed...\n") + time.sleep(0.3) + return image + + c, w, textwidth, textheight, = drawer.textbbox(xy = (0, 0), text = text, font = font) # Getting text size, only need the last two values + x = imagewidth - textwidth - margin + y = imageheight - textheight - margin + drawer.text((x, y), text, font = font) + + return image + + def save_image(self, image, path, file_type, jpg_quality, png_compressing, optimize, exif_data = None): + # partly optimized by chatGPT + """ + Save an image to the specified path with optional EXIF data and optimization. + """ + file_type = file_type.lower() + save_params = {"optimize": optimize} + # Add file-specific parameters + if file_type == "jpg": + save_params["quality"] = jpg_quality + elif file_type == "png": + save_params["compress_level"] = png_compressing + elif file_type not in ["webp", "jpg", "png"]: + input(f"Type: {file_type} is not supported. Press Enter to continue...") + return + # Add EXIF data if available + if exif_data is not None: + save_params["exif"] = piexif.dump(exif_data) + if file_type == "webp": + print("File format webp does not support all exif features, some information might get lost...\n") + time.sleep(0.1) + try: + image.save(f"{path}.{file_type}", **save_params) + except Exception as e: + print(f"Failed to save image: {e}") + +class ExifHandler: + """Function using piexif are here.""" + def __init__(self): + pass + + def get_exif_info(self, image): + return(piexif.load(image.info['exif'])) + + def build_exif_dict(self, user_data, imagesize): + """Build a piexif-compatible EXIF dictionary from user data.""" + # Mostly made by ChatGPT, some adjustment + zeroth_ifd = { + piexif.ImageIFD.Make: user_data["make"], + piexif.ImageIFD.Model: user_data["model"], + piexif.ImageIFD.Software: user_data["software"], + piexif.ImageIFD.Copyright: user_data["copyright_info"], + piexif.ImageIFD.Artist: user_data["artist"], + piexif.ImageIFD.ImageDescription: user_data["image_description"], + piexif.ImageIFD.XResolution: (72, 1), + piexif.ImageIFD.YResolution: (72, 1), + } + exif_ifd = { + piexif.ExifIFD.UserComment: user_data["user_comment"], + piexif.ExifIFD.ISOSpeedRatings: int(user_data["iso"]), + piexif.ExifIFD.PixelXDimension: imagesize[0], + piexif.ExifIFD.PixelYDimension: imagesize[1], + } + if "date_time_original" in user_data: + exif_ifd[piexif.ExifIFD.DateTimeOriginal] = user_data["date_time_original"].encode("utf-8") + return {"0th": zeroth_ifd, "Exif": exif_ifd} diff --git a/utils/utility.py b/utils/utility.py new file mode 100644 index 0000000..6f09597 --- /dev/null +++ b/utils/utility.py @@ -0,0 +1,47 @@ +import yaml + +class Utilities: + def __init__(self): + pass + + def read_yaml(self, yaml_file): + try: + with open(yaml_file, "r") as file: + data = yaml.safe_load(file) + return data + except (FileNotFoundError, PermissionError) as e: + print(f"Error loading settings file: {e}") + return + + def write_yaml(self, yaml_file, data): + try: + with open(yaml_file, "w") as file: + yaml.dump(data, file) + except PermissionError as e: + print(f"Error saving setings: {e}") + + def yes_no(self, str): + """Ask user y/n question""" + while True: + choice = input(f"{str} (y/n): ") + if choice == "y": + return True + elif choice == "n": + return False + else: + print("Not a valid option, try again.") + + def progress_bar(self, current, total, barsize = 50): + if current > total: + print("\033[91mThis bar has exceeded its limits!\033[0m Maybe the current value needs some restraint?") + print(f"{(current - total) * '\033[92mHonk, Honk!\033[0m '}") + return + progress = int((barsize / total) * current) + rest = barsize - progress + if rest <= 2: rest = 0 + # Determine the number of digits in total + total_digits = len(str(total)) + # Format current with leading zeros + current_formatted = f"{current:0{total_digits}}" + print(f"{current_formatted}|{progress * '-'}>{rest * ' '}|{total}", end="\r") + if current == total: print("") From 41a9c5190e953350f9e3b39f329e1029bed55145 Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Sat, 28 Dec 2024 00:28:58 +0100 Subject: [PATCH 6/7] Moving file --- exif_options.yaml | 28 ----------- image_handler.py | 115 ---------------------------------------------- tui.py | 60 ------------------------ utility.py | 47 ------------------- 4 files changed, 250 deletions(-) delete mode 100644 exif_options.yaml delete mode 100644 image_handler.py delete mode 100644 tui.py delete mode 100644 utility.py diff --git a/exif_options.yaml b/exif_options.yaml deleted file mode 100644 index 9a80688..0000000 --- a/exif_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -make: - - Nikon -model: - - FG - - F50 -lens: - - Nikon LENS SERIES E 50mm - - AF NIKKOR 35-70mm -iso: # Numeric values cause errors with simple_term_menu and must be quoted. This issue will be resolved with the future UI switch. - - "100" - - "200" - - "400" - - "800" - - "1000" - - "1600" - - "3200" -image_description: - - ILFORD DELTA 3200 - - ILFORD ILFOCOLOR - - LomoChrome Turquoise -user_comment: - - Scanner.NORITSU-KOKI - - Scanner.NA -artist: - - Mr. Finchum -copyright_info: - - All Rights Reserved - - No Copyright diff --git a/image_handler.py b/image_handler.py deleted file mode 100644 index 6631a93..0000000 --- a/image_handler.py +++ /dev/null @@ -1,115 +0,0 @@ -from PIL import Image, ImageDraw, ImageFont, ImageEnhance -import piexif -import time - -class ImageProcessor: - """Functions using pillow are in here.""" - def __init__(self): - pass - - def open_image(self, path): - """Open an image from path, returns image object.""" - return Image.open(path) - - def get_image_size(self, image): - """Simply get image size.""" - return image.size - - def grayscale(self, image): - """Change to grayscale""" - return image.convert("L") - - def change_contrast(self, image, change): - enhancer = ImageEnhance.Contrast(image) - new_img = enhancer.enhance(1 + (change/100)) - return new_img - - def change_brightness(self, image, change): - enhancer = ImageEnhance.Brightness(image) - new_img = enhancer.enhance(1 + (change/100)) - return new_img - - def resize_image(self, image, percent, resample = True): - """Resize an image by giving a percent.""" - new_size = tuple(int(x * (percent / 100)) for x in image.size) - if resample: - resized_image = image.resize(new_size) - else: - resized_image = image.resize((new_size),resample=Image.Resampling.NEAREST) - return resized_image - - def add_watermark(self, image, text, font_size_scale = 70): - drawer = ImageDraw.Draw(image) - imagewidth, imageheight = image.size - margin = (imageheight / 100 ) * 2 # margin dynamic, 2% of image size - font_size = imagewidth / font_size_scale # Scaling the font size - try: # Try loading front, if notaviable return unmodified image - font = ImageFont.truetype("OpenDyslexic3-Regular.ttf", font_size) - except: - print("Error loading font for watermark, please ensure font is installed...\n") - time.sleep(0.3) - return image - - c, w, textwidth, textheight, = drawer.textbbox(xy = (0, 0), text = text, font = font) # Getting text size, only need the last two values - x = imagewidth - textwidth - margin - y = imageheight - textheight - margin - drawer.text((x, y), text, font = font) - - return image - - def save_image(self, image, path, file_type, jpg_quality, png_compressing, optimize, exif_data = None): - # partly optimized by chatGPT - """ - Save an image to the specified path with optional EXIF data and optimization. - """ - file_type = file_type.lower() - save_params = {"optimize": optimize} - # Add file-specific parameters - if file_type == "jpg": - save_params["quality"] = jpg_quality - elif file_type == "png": - save_params["compress_level"] = png_compressing - elif file_type not in ["webp", "jpg", "png"]: - input(f"Type: {file_type} is not supported. Press Enter to continue...") - return - # Add EXIF data if available - if exif_data is not None: - save_params["exif"] = piexif.dump(exif_data) - if file_type == "webp": - print("File format webp does not support all exif features, some information might get lost...\n") - time.sleep(0.1) - try: - image.save(f"{path}.{file_type}", **save_params) - except Exception as e: - print(f"Failed to save image: {e}") - -class ExifHandler: - """Function using piexif are here.""" - def __init__(self): - pass - - def get_exif_info(self, image): - return(piexif.load(image.info['exif'])) - - def build_exif_dict(self, user_data, imagesize): - """Build a piexif-compatible EXIF dictionary from user data.""" - # Mostly made by ChatGPT, some adjustment - zeroth_ifd = { - piexif.ImageIFD.Make: user_data["make"], - piexif.ImageIFD.Model: user_data["model"], - piexif.ImageIFD.Software: user_data["software"], - piexif.ImageIFD.Copyright: user_data["copyright_info"], - piexif.ImageIFD.Artist: user_data["artist"], - piexif.ImageIFD.ImageDescription: user_data["image_description"], - piexif.ImageIFD.XResolution: (72, 1), - piexif.ImageIFD.YResolution: (72, 1), - } - exif_ifd = { - piexif.ExifIFD.UserComment: user_data["user_comment"], - piexif.ExifIFD.ISOSpeedRatings: int(user_data["iso"]), - piexif.ExifIFD.PixelXDimension: imagesize[0], - piexif.ExifIFD.PixelYDimension: imagesize[1], - } - if "date_time_original" in user_data: - exif_ifd[piexif.ExifIFD.DateTimeOriginal] = user_data["date_time_original"].encode("utf-8") - return {"0th": zeroth_ifd, "Exif": exif_ifd} diff --git a/tui.py b/tui.py deleted file mode 100644 index f8abd32..0000000 --- a/tui.py +++ /dev/null @@ -1,60 +0,0 @@ -from simple_term_menu import TerminalMenu - -class SimpleTUI: - """TUI parts using library simple_term_menu""" - def __init__(self): - pass - - def choose_menu(self, menu_title, choices): - """ Dynamic function to display content of a list and returnes which was selected.""" - menu_options = choices - menu = TerminalMenu( - menu_entries = menu_options, - title = menu_title, - menu_cursor = "> ", - menu_cursor_style = ("fg_gray", "bold"), - menu_highlight_style = ("bg_gray", "fg_black"), - cycle_cursor = True, - clear_screen = False - ) - menu.show() - return menu.chosen_menu_entry - - def multi_select_menu(self, menu_title, choices): - """ Dynamic function to display content of a list and returnes which was selected.""" - menu_options = choices - menu = TerminalMenu( - menu_entries = menu_options, - title = menu_title, - multi_select=True, - show_multi_select_hint=True, - menu_cursor_style = ("fg_gray", "bold"), - menu_highlight_style = ("bg_gray", "fg_black"), - cycle_cursor = True, - clear_screen = False - ) - menu.show() - choisen_values = menu.chosen_menu_entries - - if choisen_values == None: - print("Exiting...") - exit() - else: - return menu.chosen_menu_entries - - def yes_no_menu(self, message): # oh - menu_options = ["[y] yes", "[n] no"] - menu = TerminalMenu( - menu_entries = menu_options, - title = f"{message}", - menu_cursor = "> ", - menu_cursor_style = ("fg_red", "bold"), - menu_highlight_style = ("bg_gray", "fg_black"), - cycle_cursor = True, - clear_screen = False - ) - menu_entry_index = menu.show() - if menu_entry_index == 0: - return True - elif menu_entry_index == 1: - return False diff --git a/utility.py b/utility.py deleted file mode 100644 index 6f09597..0000000 --- a/utility.py +++ /dev/null @@ -1,47 +0,0 @@ -import yaml - -class Utilities: - def __init__(self): - pass - - def read_yaml(self, yaml_file): - try: - with open(yaml_file, "r") as file: - data = yaml.safe_load(file) - return data - except (FileNotFoundError, PermissionError) as e: - print(f"Error loading settings file: {e}") - return - - def write_yaml(self, yaml_file, data): - try: - with open(yaml_file, "w") as file: - yaml.dump(data, file) - except PermissionError as e: - print(f"Error saving setings: {e}") - - def yes_no(self, str): - """Ask user y/n question""" - while True: - choice = input(f"{str} (y/n): ") - if choice == "y": - return True - elif choice == "n": - return False - else: - print("Not a valid option, try again.") - - def progress_bar(self, current, total, barsize = 50): - if current > total: - print("\033[91mThis bar has exceeded its limits!\033[0m Maybe the current value needs some restraint?") - print(f"{(current - total) * '\033[92mHonk, Honk!\033[0m '}") - return - progress = int((barsize / total) * current) - rest = barsize - progress - if rest <= 2: rest = 0 - # Determine the number of digits in total - total_digits = len(str(total)) - # Format current with leading zeros - current_formatted = f"{current:0{total_digits}}" - print(f"{current_formatted}|{progress * '-'}>{rest * ' '}|{total}", end="\r") - if current == total: print("") From b591d746400ab44c582ad2a9231c2f0e41776a66 Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Sat, 28 Dec 2024 00:43:50 +0100 Subject: [PATCH 7/7] v0.2.0 --- CHANGELOG.md | 5 +++++ main.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3790094..bcb2a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.x +### 0.2.0 +- **Cleaner folder structure** + - Moving files with classes to different folder to keep project cleaner. + ## 0.1.x ### 0.1.1 - **Add Original to add Timestamp to Images** diff --git a/main.py b/main.py index 4c4b202..058dd2b 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,7 @@ from ui.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.1" + self.version = "0.2.0" self.utilities = Utilities() self.image_processor = ImageProcessor() self.exif_handler = ExifHandler()