Now using pyside6

This commit is contained in:
Mr Finchum 2024-12-28 18:02:34 +01:00
parent 09f58c445c
commit a4c6fe3a65

338
main.py
View file

@ -1,207 +1,99 @@
import sys
import os import os
import re import re
import time
from datetime import datetime from datetime import datetime
from utils.utility import Utilities from utils.utility import Utilities
from utils.image_handler import ImageProcessor, ExifHandler from utils.image_handler import ImageProcessor, ExifHandler
from ui.tui import SimpleTUI
class Optima35: from ui.main_window import Ui_MainWindow
# 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): from PySide6 import QtWidgets
from PySide6.QtWidgets import (
QMessageBox,
QApplication,
QMainWindow,
QWidget,
QVBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QCheckBox,
QFileDialog,
QHBoxLayout,
QSpinBox,
)
class Optima35QT6(QMainWindow, Ui_MainWindow):
def __init__(self):
super(Optima35QT6, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.define_settings()
self.setWindowTitle(f"{self.name} v{self.version}")
self.define_gui_interaction()
def define_gui_interaction(self):
self.ui.input_folder_button.clicked.connect(self.browse_input_folder)
self.ui.output_folder_button.clicked.connect(self.browse_output_folder)
self.ui.start_button.clicked.connect(self.process)
def define_settings(self):
self.name = "OPTIMA-35" self.name = "OPTIMA-35"
self.version = "0.2.1" self.version = "0.3.0"
self.utilities = Utilities() self.utilities = Utilities()
self.image_processor = ImageProcessor() self.image_processor = ImageProcessor()
self.exif_handler = ExifHandler() self.exif_handler = ExifHandler()
self.tui = SimpleTUI()
self.settings = { self.settings = {
"input_folder": None, "input_folder": None,
"output_folder": None, "output_folder": None,
"file_format": None, "file_format": None,
"resize_percentage": None, "resize_percentage": False,
"copy_exif": None, "contrast_percentage": False,
"contrast_percentage": None, "brightness_percentage": False,
"brightness_percentage": None, "new_file_names": False,
"new_file_names": None,
"invert_image_order": False, "invert_image_order": False,
"watermark_text": None, "copy_exif": False,
"modifications": [], "own_exif": False,
"watermark": False,
"grayscale": False,
"jpg_quality": None,
"png_compression": None
} }
self.settings_to_save = [ self.exif_data = None
"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): def browse_input_folder(self):
"""Load settings from a YAML file or ask the user if not present or incomplete.""" folder = QFileDialog.getExistingDirectory(self, "Select Input Folder")
# Partially ChatGPT if folder:
if self.read_settings(self.settings_to_save): self.ui.input_path.setText(folder)
for item in self.settings_to_save:
print(f"{item}: {self.settings[item]}") def browse_output_folder(self):
use_saved = self.tui.yes_no_menu("Use these settings?") folder = QFileDialog.getExistingDirectory(self, "Select Output Folder")
if use_saved: if folder:
return self.ui.output_path.setText(folder)
def process(self):
self.check_options()
if os.path.exists(self.settings["input_folder"]) and os.path.exists(self.settings["output_folder"]):
print(self.settings)
else: else:
print("No settings found...") print(self.settings)
QMessageBox.warning(self, "Warning", "Input and/or output folder invalid...")
return
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"] input_folder = self.settings["input_folder"]
output_folder = self.settings["output_folder"] output_folder = self.settings["output_folder"]
image_files = [ image_files = [
f for f in os.listdir(input_folder) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp")) 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 i = 1
for image_file in image_files: for image_file in image_files:
input_path = os.path.join(input_folder, image_file) input_path = os.path.join(input_folder, image_file)
if "Rename images" in self.settings["modifications"]: if self.settings["new_file_names"] != False:
image_name = self.name_images(self.settings["new_file_names"], i, len(image_files), self.settings["invert_image_order"]) image_name = self.name_images(self.settings["new_file_names"], i, len(image_files), self.settings["invert_image_order"])
else: else:
image_name = os.path.splitext(image_file)[0] image_name = os.path.splitext(image_file)[0]
@ -210,25 +102,22 @@ class Optima35:
with self.image_processor.open_image(input_path) as img: with self.image_processor.open_image(input_path) as img:
processed_img = 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"]: if self.settings["resize_percentage"] != False:
processed_img = self.image_processor.resize_image(
image = processed_img, percent = self.settings["resize_percentage"]
)
if self.settings["watermark"] != False:
processed_img = self.image_processor.add_watermark(processed_img, self.settings["watermark"])
if self.settings["grayscale"] != False: # There is a problem, if we first to grayscale and then watermark it braeks
processed_img = self.image_processor.grayscale(processed_img)
if self.settings["brightness_percentage"] != False: # Does the order of brightness and contrast matter?
processed_img = self.image_processor.change_brightness(processed_img, self.settings["brightness_percentage"])
if self.settings["contrast_percentage"] != False: # Does the order of brightness and contrast matter?
processed_img = self.image_processor.change_contrast(processed_img, self.settings["contrast_percentage"])
if self.settings["copy_exif"] != False:
# When copying exif from original, make sure to change Piexel X & Y Dimension to fit new size # When copying exif from original, make sure to change Piexel X & Y Dimension to fit new size
try: try:
og_exif = self.exif_handler.get_exif_info(img) og_exif = self.exif_handler.get_exif_info(img)
@ -238,7 +127,7 @@ class Optima35:
# If an error happends it is because the picture does not have exif data # 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.") print("Copying EXIF data selected, but no EXIF data is available in the original image file.")
exif_data = None exif_data = None
elif "Change EXIF" not in self.settings["modifications"]: elif self.settings["copy_exif"] == False:
exif_data = None exif_data = None
self.image_processor.save_image( self.image_processor.save_image(
@ -248,10 +137,16 @@ class Optima35:
file_type = self.settings["file_format"], file_type = self.settings["file_format"],
jpg_quality = self.settings["jpg_quality"], jpg_quality = self.settings["jpg_quality"],
png_compressing = self.settings["png_compression"], png_compressing = self.settings["png_compression"],
optimize = self.settings["web_optimize"] optimize = False
) )
self.utilities.progress_bar(i, len(image_files)) self.handle_qprogressbar(i, len(image_files))
i += 1 i += 1
QMessageBox.information(self, "Information", "Finished")
self.ui.progressBar.setValue(0)
def handle_qprogressbar(self, current, total):
progress = int((100 / total) * current)
self.ui.progressBar.setValue(progress)
def name_images(self, base_name, current_image, total_images, invert): def name_images(self, base_name, current_image, total_images, invert):
""""Returns name, combination of base_name and ending number.""" """"Returns name, combination of base_name and ending number."""
@ -263,30 +158,39 @@ class Optima35:
ending = f"{ending_number:0{total_digits}}" ending = f"{ending_number:0{total_digits}}"
return f"{base_name}_{ending}" return f"{base_name}_{ending}"
def modify_timestamp_in_exif(self, exif_data, filename): def check_options(self):
""""Takes exif data and adjust time to fit ending of filename."""
try: try:
last_tree = filename[-3:len(filename)] self.settings["input_folder"] = self.ui.input_path.text()
total_seconds = int(re.sub(r'\D+', '', last_tree)) self.settings["output_folder"] = self.ui.output_path.text()
self.settings["file_format"] = self.ui.image_type.currentText()
self.settings["jpg_quality"] = int(self.ui.jpg_quality_spinBox.text())
self.settings["png_compression"] = int(self.ui.png_quality_spinBox.text())
self.settings["invert_image_order"] = self.ui.revert_checkbox.isChecked()
self.settings["grayscale"] = self.ui.grayscale_checkBox.isChecked()
self.settings["copy_exif"] = self.ui.copy_exif_checkBox.isChecked()
self.settings["own_exif"] = self.ui.exif_checkbox.isChecked()
minutes = total_seconds // 60 if self.ui.resize_checkbox.isChecked():
seconds = total_seconds % 60 self.settings["resize_percentage"] = int(self.ui.resize_spinBox.text())
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: if self.ui.brightness_checkbox.isChecked():
print("Modifying date went wrong, exiting...") self.settings["brightness_percentage"] = int(self.ui.brightness_spinBox.text())
exit()
def run(self): if self.ui.contrast_checkbox.isChecked():
"""Run the main program.""" self.settings["contrast_percentage"] = int(self.ui.contrast_spinBox.text())
self.load_or_ask_settings()
self.get_user_settings() if self.ui.rename_checkbox.isChecked() and self.ui.filename.text() != "":
self.process_images() self.settings["new_file_names"] = self.ui.filename.text()
print("Done")
if self.ui.watermark_checkbox.isChecked() and self.ui.watermark_lineEdit.text() != "":
self.settings["watermark"] = self.ui.watermark_lineEdit.text()
except Exception as e:
print(f"Whoops: {e}")
if __name__ == "__main__": if __name__ == "__main__":
app = Optima35("config/settings.yaml", "config/exif_options.yaml") app = QtWidgets.QApplication(sys.argv)
app.run()
window = Optima35QT6()
window.show()
app.exec()