Merge branch 'chore/optimizing' into 'main'

Chore/optimizing

See merge request CodeByMrFinchum/optima-35!8
This commit is contained in:
Mr Finchum 2025-01-02 14:20:44 +00:00
commit 7af3191585
11 changed files with 327 additions and 327 deletions

View file

@ -1,5 +1,15 @@
# Changelog
## 0.5.x
### **OPTIMA35 0.5.0: Code Cleaning and Preparation for Split**
- Cleaned up the codebase, following **PEP8**, adding indication for only internal functions.
- Refactored the project in preparation for splitting it into **OPTIMA35 (core functionality)** and **UI (graphical and text interfaces)**.
- Moved `image_handler.py` into the `optima` folder/package to integrate it as an essential part of the OPTIMA35 package, rather than just a utility.
### **UI 0.1.0: GUI and TUI Updates**
- Updated **GUI** and **TUI** to work seamlessly with the new **OPTIMA35** class.
- Ensured compatibility with the newly organized codebase in the OPTIMA35 package.
## 0.4.x
### 0.4.1: Finished GUI and TUI
- Both **GUI** and **TUI** now fully utilize the `optima35` class for core functionality.

View file

@ -1,34 +1,34 @@
artist:
- Mr. Finchum
- John Doe
- Mr. Finchum
- John Doe
copyright_info:
- All Rights Reserved
- CC BY-NC 4.0
- No Copyright
- All Rights Reserved
- CC BY-NC 4.0
- No Copyright
image_description:
- ILFORD DELTA 3200
- ILFORD ILFOCOLOR
- LomoChrome Turquoise
- Kodak 200
- ILFORD DELTA 3200
- ILFORD ILFOCOLOR
- LomoChrome Turquoise
- Kodak 200
iso:
- '100'
- '200'
- '400'
- '800'
- '1000'
- '1600'
- '3200'
- "100"
- "200"
- "400"
- "800"
- "1000"
- "1600"
- "3200"
lens:
- Nikon LENS SERIES E 50mm
- AF NIKKOR 35-70mm
- Canon FD 50mm f/1.4 S.S.C
- Nikon LENS SERIES E 50mm
- AF NIKKOR 35-70mm
- Canon FD 50mm f/1.4 S.S.C
make:
- Nikon
- Canon
- Nikon
- Canon
model:
- FG
- F50
- AE-1
- FG
- F50
- AE-1
user_comment:
- Scanner.NORITSU-KOKI
- Scanner.NA
- Scanner.NORITSU-KOKI
- Scanner.NA

226
gui.py
View file

@ -26,48 +26,48 @@ from PySide6.QtWidgets import (
class Optima35GUI(QMainWindow, Ui_MainWindow):
def __init__(self, exif_file):
super(Optima35GUI, self).__init__()
self.name = "GUI35"
self.version = "0.1.0"
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.o = OPTIMA35()
self.u = Utilities()
self.exif_file = exif_file
self.exif_data = None
self.setWindowTitle(f"{self.o.name} {self.o.version}")
self.default_ui_layout()
self.define_gui_interaction()
self.available_exif_data = None
self.settings = {}
self.setWindowTitle(f"{self.name} v{self.version} for {self.o.name} {self.o.version}")
self._default_ui_layout()
self._define_gui_interaction()
if exif_file == "config/exif_example.yaml":
self.change_statusbar("Using example exif...", 10000)
self._change_statusbar("Using example exif...", 10000)
def default_ui_layout(self):
def _default_ui_layout(self):
self.ui.png_quality_spinBox.setVisible(False)
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)
self.ui.image_type.currentIndexChanged.connect(self.update_quality_options)
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)
self.ui.image_type.currentIndexChanged.connect(self._update_quality_options)
self.ui.exif_checkbox.stateChanged.connect(
lambda state: self.handle_checkbox_state(state, 2, self.populate_exif)
lambda state: self._handle_checkbox_state(state, 2, self._populate_exif)
)
self.ui.tabWidget.currentChanged.connect(self.on_tab_changed)
self.ui.edit_exif_button.clicked.connect(self.open_exif_editor)
self.ui.restart_button.clicked.connect(self.restart_app)
self.ui.tabWidget.currentChanged.connect(self._on_tab_changed)
self.ui.edit_exif_button.clicked.connect(self._open_exif_editor)
def process(self):
#self.ui.start_button.setEnabled(False)
#self.ui.restart_button.setEnabled(False)
self.check_options() # Get all user selected data
input_folder_valid = os.path.exists(self.o.settings["input_folder"])
output_folder_valid = os.path.exists(self.o.settings["output_folder"])
def _process(self):
self.ui.start_button.setEnabled(False)
self._update_settings() # Get all user selected data
input_folder_valid = os.path.exists(self.settings["input_folder"])
output_folder_valid = os.path.exists(self.settings["output_folder"])
if not input_folder_valid or not output_folder_valid:
QMessageBox.warning(self, "Warning", f"Input location {input_folder_valid}\nOutput folder {output_folder_valid}...")
return
input_folder = self.o.settings["input_folder"]
output_folder = self.o.settings["output_folder"]
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"))
@ -75,52 +75,66 @@ class Optima35GUI(QMainWindow, Ui_MainWindow):
i = 1
for image_file in image_files:
input_path = os.path.join(input_folder, image_file)
if self.o.settings["new_file_names"] != False:
image_name = self.o.name_images(self.o.settings["new_file_names"], i, len(image_files), self.o.settings["invert_image_order"])
if self.settings["new_file_names"] != False:
image_name = self.u.append_number_to_name(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)
self.o.process(input_path, output_path)
self.handle_qprogressbar(i, len(image_files))
self.o.process_image(
image_input_file = input_path,
image_output_file = output_path,
file_type = self.settings["file_format"],
quality = self.settings["jpg_quality"],
compressing = self.settings["png_compression"],
optimize = self.ui.optimize_checkBox.isChecked(),
resize = self.settings["resize"],
watermark = self.settings["watermark"],
font_size = self.settings["font_size"],
grayscale = self.settings["grayscale"],
brightness = self.settings["brightness"],
contrast = self.settings["contrast"],
dict_for_exif = self.user_selected_exif,
gps = self.settings["gps"],
copy_exif = self.settings["copy_exif"])
self._handle_qprogressbar(i, len(image_files))
i += 1
QMessageBox.information(self, "Information", "Finished")
#self.ui.start_button.setEnabled(True)
#self.ui.restart_button.setEnabled(True)
self.ui.start_button.setEnabled(True)
self.ui.progressBar.setValue(0)
def open_exif_editor(self):
def _open_exif_editor(self):
"""Open the EXIF Editor."""
self.exif_editor = ExifEditor(self.exif_data)
self.exif_editor.exif_data_updated.connect(self.update_exif_data)
self.exif_editor = ExifEditor(self.available_exif_data)
self.exif_editor.exif_data_updated.connect(self._update_exif_data)
self.exif_editor.show()
def update_exif_data(self, updated_exif_data):
def _update_exif_data(self, updated_exif_data):
"""Update the EXIF data."""
self.exif_data = updated_exif_data
self.populate_exif()
self._populate_exif()
def handle_checkbox_state(self, state, desired_state, action):
def _handle_checkbox_state(self, state, desired_state, action):
"""Perform an action based on the checkbox state and a desired state. Have to use lambda when calling."""
if state == desired_state:
action()
def on_tab_changed(self, index):
def _on_tab_changed(self, index):
"""Handle tab changes."""
# chatgpt
if index == 1: # EXIF Tab
self.handle_exif_file("read")
self._handle_exif_file("read")
elif index == 0: # Main Tab
self.handle_exif_file("write")
self._handle_exif_file("write")
def handle_exif_file(self, do):
def _handle_exif_file(self, do):
if do == "read":
self.exif_data = self.u.read_yaml(self.exif_file)
self.available_exif_data = self.u.read_yaml(self.exif_file)
elif do == "write":
self.u.write_yaml(self.exif_file, self.exif_data)
self.u.write_yaml(self.exif_file, self.available_exif_data)
def populate_exif(self):
def _populate_exif(self):
# partly chatGPT
# Mapping of EXIF fields to comboboxes in the UI
combo_mapping = {
@ -133,16 +147,16 @@ class Optima35GUI(QMainWindow, Ui_MainWindow):
"artist": self.ui.artist_comboBox,
"copyright_info": self.ui.copyright_info_comboBox,
}
self.populate_comboboxes(combo_mapping)
self._populate_comboboxes(combo_mapping)
def populate_comboboxes(self, combo_mapping):
def _populate_comboboxes(self, combo_mapping):
"""Populate comboboxes with EXIF data."""
# ChatGPT
for field, comboBox in combo_mapping.items():
comboBox.clear() # Clear existing items
comboBox.addItems(map(str, self.exif_data.get(field, [])))
comboBox.addItems(map(str, self.available_exif_data.get(field, [])))
def update_quality_options(self):
def _update_quality_options(self):
"""Update visibility of quality settings based on selected format."""
# ChatGPT
selected_format = self.ui.image_type.currentText()
@ -157,82 +171,88 @@ class Optima35GUI(QMainWindow, Ui_MainWindow):
elif selected_format == "png":
self.ui.png_quality_spinBox.setVisible(True)
def browse_input_folder(self):
def _browse_input_folder(self):
folder = QFileDialog.getExistingDirectory(self, "Select Input Folder")
if folder:
self.ui.input_path.setText(folder)
def browse_output_folder(self):
def _browse_output_folder(self):
folder = QFileDialog.getExistingDirectory(self, "Select Output Folder")
if folder:
self.ui.output_path.setText(folder)
def change_statusbar(self, msg, timeout = 500):
def _change_statusbar(self, msg, timeout = 500):
self.ui.statusBar.showMessage(msg, timeout)
def handle_qprogressbar(self, current, total):
def _handle_qprogressbar(self, current, total):
progress = int((100 / total) * current)
self.ui.progressBar.setValue(progress)
def check_options(self):
try:
self.o.settings["input_folder"] = self.ui.input_path.text()
self.o.settings["output_folder"] = self.ui.output_path.text()
self.o.settings["file_format"] = self.ui.image_type.currentText()
self.o.settings["jpg_quality"] = int(self.ui.jpg_quality_spinBox.text())
self.o.settings["png_compression"] = int(self.ui.png_quality_spinBox.text())
self.o.settings["invert_image_order"] = self.ui.revert_checkbox.isChecked()
self.o.settings["grayscale"] = self.ui.grayscale_checkBox.isChecked()
self.o.settings["copy_exif"] = self.ui.exif_copy_checkBox.isChecked()
self.o.settings["own_exif"] = self.ui.exif_checkbox.isChecked()
self.o.settings["font_size"] = self.ui.font_size_comboBox.currentIndex() + 1
self.o.settings["optimize"] = self.ui.optimize_checkBox.isChecked()
self.o.settings["own_date"] = self.ui.add_date_checkBox.isChecked()
def _get_checkbox_value(self, checkbox, default=None):
"""Helper function to get the value of a checkbox or a default value."""
return checkbox.isChecked() if checkbox else default
if self.ui.resize_checkbox.isChecked():
self.o.settings["resize"] = int(self.ui.resize_spinBox.text())
def _get_spinbox_value(self, spinbox, default=None):
"""Helper function to get the value of a spinbox and handle empty input."""
return int(spinbox.text()) if spinbox.text() else default
if self.ui.brightness_checkbox.isChecked():
self.o.settings["brightness"] = int(self.ui.brightness_spinBox.text())
def _get_combobox_value(self, combobox, default=None):
"""Helper function to get the value of a combobox."""
return combobox.currentIndex() + 1 if combobox.currentIndex() != -1 else default
if self.ui.contrast_checkbox.isChecked():
self.o.settings["contrast"] = int(self.ui.contrast_spinBox.text())
def _get_text_value(self, lineedit, default=None):
"""Helper function to get the value of a text input field."""
return lineedit.text() if lineedit.text() else default
if self.ui.rename_checkbox.isChecked():
if self.ui.filename.text() != "":
self.o.settings["new_file_names"] = self.ui.filename.text()
else:
self.o.settings["new_file_names"] = False
else:
self.o.settings["new_file_names"] = False
def _get_selected_exif(self):
"""Collect selected EXIF data and handle date and GPS if necessary."""
selected_exif = self._collect_selected_exif() if self.ui.exif_checkbox.isChecked() else None
if selected_exif:
if self.ui.add_date_checkBox.isChecked():
selected_exif["date_time_original"] = self._get_date()
if self.ui.gps_checkBox.isChecked():
self.settings["gps"] = [self.ui.lat_lineEdit.text(), self.ui.long_lineEdit.text()]
else:
self.settings["gps"] = None
return selected_exif
def _update_settings(self):
"""Update .settings from all GUI elements."""
# General settings
self.settings["input_folder"] = self._get_text_value(self.ui.input_path)
self.settings["output_folder"] = self._get_text_value(self.ui.output_path)
self.settings["file_format"] = self.ui.image_type.currentText()
self.settings["jpg_quality"] = self._get_spinbox_value(self.ui.jpg_quality_spinBox)
self.settings["png_compression"] = self._get_spinbox_value(self.ui.png_quality_spinBox)
self.settings["invert_image_order"] = self._get_checkbox_value(self.ui.revert_checkbox)
self.settings["grayscale"] = self._get_checkbox_value(self.ui.grayscale_checkBox)
self.settings["copy_exif"] = self._get_checkbox_value(self.ui.exif_copy_checkBox)
self.settings["own_exif"] = self._get_checkbox_value(self.ui.exif_checkbox)
self.settings["font_size"] = self._get_combobox_value(self.ui.font_size_comboBox)
self.settings["optimize"] = self._get_checkbox_value(self.ui.optimize_checkBox)
self.settings["own_date"] = self._get_checkbox_value(self.ui.add_date_checkBox)
if self.ui.watermark_checkbox.isChecked():
if self.ui.watermark_lineEdit.text() != "":
self.o.settings["watermark"] = self.ui.watermark_lineEdit.text()
else:
self.o.settings["watermark"] = False
else:
self.o.settings["watermark"] = False
# Conditional settings with logic
self.settings["resize"] = self._get_spinbox_value(self.ui.resize_spinBox) if self.ui.resize_checkbox.isChecked() else None
self.settings["brightness"] = self._get_spinbox_value(self.ui.brightness_spinBox) if self.ui.brightness_checkbox.isChecked() else None
self.settings["contrast"] = self._get_spinbox_value(self.ui.contrast_spinBox) if self.ui.contrast_checkbox.isChecked() else None
if self.o.settings["own_exif"]:
self.o.selected_exif = self.collect_selected_exif()
if self.ui.add_date_checkBox.isChecked():
self.o.selected_exif["date_time_original"] = self.get_date()
if self.ui.gps_checkBox.isChecked():
self.o.settings["gps"] = [self.ui.lat_lineEdit.text(), self.ui.long_lineEdit.text()]
else:
self.o.settings["gps"] = False
self.settings["new_file_names"] = self._get_text_value(self.ui.filename, False) if self.ui.rename_checkbox.isChecked() else False
self.settings["watermark"] = self._get_text_value(self.ui.watermark_lineEdit) if self.ui.watermark_checkbox.isChecked() else None
except Exception as e:
print(f"Whoops: {e}")
# Handle EXIF data selection
if self.settings["own_exif"]:
self.user_selected_exif = self._get_selected_exif()
else:
self.user_selected_exif = None
self.settings["gps"] = None
def get_date(self):
def _get_date(self):
date_input = self.ui.dateEdit.date().toString("yyyy-MM-dd")
new_date = datetime.strptime(date_input, "%Y-%m-%d")
return new_date.strftime("%Y:%m:%d 00:00:00")
def collect_selected_exif(self):
def _collect_selected_exif(self):
user_data = {}
user_data["make"] = self.ui.make_comboBox.currentText()
user_data["model"] = self.ui.model_comboBox.currentText()
@ -245,18 +265,6 @@ class Optima35GUI(QMainWindow, Ui_MainWindow):
user_data["software"] = f"{self.o.name} {self.o.version}"
return user_data
def rebuild_ui(self):
# Define the bash script to execute
bash_script = "/home/sam/git/gitlab_public/optima-35/rebuild_ui.sh"
os.system(bash_script)
def restart_app(self):
"""Restarts the application."""
self.rebuild_ui()
# chatGPT
python = sys.executable # Path to the Python interpreter
os.execv(python, [python] + sys.argv)
def main(exif_file):
app = QtWidgets.QApplication(sys.argv)
window = Optima35GUI(exif_file=exif_file)

View file

@ -1,6 +1,5 @@
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
import piexif
import time
from fractions import Fraction
class ImageProcessor:
@ -21,11 +20,13 @@ class ImageProcessor:
return image.convert("L")
def change_contrast(self, image, change):
"""Change contrast by percent."""
enhancer = ImageEnhance.Contrast(image)
new_img = enhancer.enhance(1 + (change/100))
return new_img
def change_brightness(self, image, change):
"""Changes brightness by percent"""
enhancer = ImageEnhance.Brightness(image)
new_img = enhancer.enhance(1 + (change/100))
return new_img
@ -40,7 +41,7 @@ class ImageProcessor:
return resized_image
def add_watermark(self, image, text, font_size_percentage):
# Still not happy about this function..
"""Addes a watermark to the image using default os font."""
drawer = ImageDraw.Draw(image)
imagewidth, imageheight = image.size
margin = (imageheight / 100 ) * 2 # margin dynamic, 2% of image size
@ -50,7 +51,6 @@ class ImageProcessor:
font = ImageFont.load_default(font_size)
except Exception as e:
print(f"Error {e}\nloading font for watermark, please ensure font is installed...\n")
time.sleep(0.1)
return image
c, w, textwidth, textheight, = drawer.textbbox(xy = (0, 0), text = text, font = font) # Getting text size, only need the last two values
@ -67,12 +67,11 @@ class ImageProcessor:
return image
def save_image(self, image, path, file_type, jpg_quality, png_compressing, optimize, exif_data):
def save_image(self, image, path, file_type, jpg_quality, png_compressing, optimize, piexif_exif_data):
# partly optimized by chatGPT
"""
Save an image to the specified path with optional EXIF data and optimization.
Save an image to the specified path with optional EXIF data.
"""
file_type = file_type.lower()
save_params = {"optimize": optimize}
# Add file-specific parameters
if file_type == "jpg" or "webp":
@ -83,11 +82,10 @@ class ImageProcessor:
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 piexif_exif_data is not None:
save_params["exif"] = piexif.dump(piexif_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:
@ -102,7 +100,7 @@ class ExifHandler:
return(piexif.load(image.info['exif']))
def build_exif_dict(self, user_data, imagesize):
"""Build a piexif-compatible EXIF dictionary from user data."""
"""Build a piexif-compatible EXIF dictionary from a dicts."""
# Mostly made by ChatGPT, some adjustment
zeroth_ifd = {
piexif.ImageIFD.Make: user_data["make"].encode("utf-8"),
@ -125,7 +123,7 @@ class ExifHandler:
return {"0th": zeroth_ifd, "Exif": exif_ifd}
def deg_to_dms(self, decimal_coordinate, cardinal_directions):
def _deg_to_dms(self, decimal_coordinate, cardinal_directions):
"""
This function converts decimal coordinates into the DMS (degrees, minutes and seconds) format.
It also determines the cardinal direction of the coordinates.
@ -147,7 +145,7 @@ class ExifHandler:
seconds = Fraction((decimal_minutes - minutes) * 60).limit_denominator(100)
return degrees, minutes, seconds, compass_direction
def dms_to_exif_format(self, dms_degrees, dms_minutes, dms_seconds):
def _dms_to_exif_format(self, dms_degrees, dms_minutes, dms_seconds):
"""
This function converts DMS (degrees, minutes and seconds) to values that can
be used with the EXIF (Exchangeable Image File Format).
@ -176,12 +174,12 @@ class ExifHandler:
:param longitude: the eastwest position coordinate
"""
# converts the latitude and longitude coordinates to DMS
latitude_dms = self.deg_to_dms(latitude, ["S", "N"])
longitude_dms = self.deg_to_dms(longitude, ["W", "E"])
latitude_dms = self._deg_to_dms(latitude, ["S", "N"])
longitude_dms = self._deg_to_dms(longitude, ["W", "E"])
# convert the DMS values to EXIF values
exif_latitude = self.dms_to_exif_format(latitude_dms[0], latitude_dms[1], latitude_dms[2])
exif_longitude = self.dms_to_exif_format(longitude_dms[0], longitude_dms[1], longitude_dms[2])
exif_latitude = self._dms_to_exif_format(latitude_dms[0], latitude_dms[1], latitude_dms[2])
exif_longitude = self._dms_to_exif_format(longitude_dms[0], longitude_dms[1], longitude_dms[2])
try:
# https://exiftool.org/TagNames/GPS.html

View file

@ -1,113 +1,108 @@
import re
import os
from datetime import datetime
# My packages
from utils.image_handler import ImageProcessor, ExifHandler
from optima.image_handler import ImageProcessor, ExifHandler
class OPTIMA35:
def __init__(self):
self.name = "OPTIMA-35"
self.version = "0.4.1"
self.version = "0.5.0"
self.image_processor = ImageProcessor()
self.exif_handler = ExifHandler()
self.settings = {
"input_folder": None,
"output_folder": None,
"file_format": None,
"resize": False,
"contrast": False,
"brightness": False,
"new_file_names": False,
"invert_image_order": False,
"copy_exif": False,
"own_exif": False,
"watermark": False,
"grayscale": False,
"jpg_quality": None,
"png_compression": None,
"font_size": None,
"optimize": False,
"gps": False
}
self.selected_exif = {}
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
def modify_timestamp_in_exif(self, data_for_exif: dict, filename: str):
""""Takes a dict formated for exif use by piexif and adjusts the date_time_original, changing the minutes and seconds to fit the number of the filname."""
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(data_for_exif["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)
data_for_exif["date_time_original"] = new_time.strftime("%Y:%m:%d %H:%M:%S")
return data_for_exif
except ValueError:
print("Modifying date went wrong, exiting...")
exit()
def process(self, image_input_file, image_output_file):
def process_image(self,
image_input_file,
image_output_file,
file_type,
quality,
compressing,
optimize,
resize = None,
watermark = None,
font_size = 2,
grayscale = False,
brightness = None,
contrast = None,
dict_for_exif = None,
gps = None,
copy_exif = False):
# Partly optimized by ChatGPT
# Open the image file
with self.image_processor.open_image(image_input_file) as img:
processed_img = img
image_name = os.path.basename(image_output_file)
image_name = os.path.basename(image_output_file) # for date adjustment
if self.settings["resize"] != False:
# Resize
if resize is not None:
processed_img = self.image_processor.resize_image(
image = processed_img, percent = self.settings["resize"]
image=processed_img, percent=resize
)
if self.settings["watermark"] != False:
processed_img = self.image_processor.add_watermark(processed_img, self.settings["watermark"], int(self.settings["font_size"]))
if self.settings["grayscale"] != False: # There is a problem, if we first to grayscale and then watermark it braeks
# Watermark
if watermark is not None:
processed_img = self.image_processor.add_watermark(
processed_img, watermark, int(font_size)
)
# Grayscale
if grayscale:
processed_img = self.image_processor.grayscale(processed_img)
if self.settings["brightness"] != False: # Does the order of brightness and contrast matter?
processed_img = self.image_processor.change_brightness(processed_img, self.settings["brightness"])
if self.settings["contrast"] != False: # Does the order of brightness and contrast matter?
processed_img = self.image_processor.change_contrast(processed_img, self.settings["contrast"])
if self.settings["own_exif"] != False:
selected_exif = self.selected_exif
if "date_time_original" in self.selected_exif:
# Brightness
if brightness is not None:
processed_img = self.image_processor.change_brightness(
processed_img, brightness
)
# Contrast
if contrast is not None:
processed_img = self.image_processor.change_contrast(
processed_img, contrast
)
# EXIF data handling
exif_piexif_format = None
if dict_for_exif: # todo: maybe move to ui and only accept complete exif dicts..
selected_exif = dict_for_exif
if "date_time_original" in dict_for_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))
if self.settings["gps"] != False:
latitude = float(self.settings["gps"][0])
longitude = float(self.settings["gps"][1])
exif_data = self.exif_handler.add_geolocation_to_exif(exif_data, latitude, longitude)
exif_piexif_format = self.exif_handler.build_exif_dict(
selected_exif, self.image_processor.get_image_size(processed_img)
)
elif self.settings["copy_exif"] == True:
# When copying exif from original, make sure to change Piexel X & Y Dimension to fit new size
# GPS data
if gps is not None:
latitude = float(gps[0])
longitude = float(gps[1])
exif_piexif_format = self.exif_handler.add_geolocation_to_exif(exif_piexif_format, latitude, longitude)
# Copy EXIF data if selected, and ensure size is correct in exif data
elif copy_exif:
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
exif_piexif_format = 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 self.settings["copy_exif"] == False:
exif_data = None
# Save the processed image
self.image_processor.save_image(
image = processed_img,
path = image_output_file,
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["optimize"]
piexif_exif_data = exif_piexif_format,
file_type = file_type,
jpg_quality = quality,
png_compressing = compressing,
optimize = optimize
)
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}"
if __name__ == "__main__":
print("Please load OPTIMA35 into the ui class...")
exit()

View file

@ -1,4 +1,3 @@
pyyaml
piexif
pillow
pyside6

View file

@ -1,4 +0,0 @@
pyyaml
piexif
pillow
simple_term_menu

135
tui.py
View file

@ -7,11 +7,13 @@ from ui.simple_tui import SimpleTUI
class Optima35TUI():
def __init__(self, exif_file, settings_file):
self.name = "TUI35"
self.version = "0.1.0"
self.o = OPTIMA35()
self.u = Utilities()
self.tui = SimpleTUI()
self.exif_file = exif_file
self.exif_data = self.u.read_yaml(exif_file)
self.available_exif_data = self.u.read_yaml(exif_file)
self.setting_file = settings_file
self.settings = {
"input_folder": None,
@ -36,19 +38,16 @@ class Optima35TUI():
"brightness"
]
def process(self):
if "Change EXIF" in self.settings["modifications"]:
self.selected_exif = self.collect_exif_data()
self.check_options() # Get all user selected data
input_folder_valid = os.path.exists(self.o.settings["input_folder"])
output_folder_valid = os.path.exists(self.o.settings["output_folder"])
def _process(self):
self._check_options() # Get all user selected data
input_folder_valid = os.path.exists(self.settings["input_folder"])
output_folder_valid = os.path.exists(self.settings["output_folder"])
if not input_folder_valid or not output_folder_valid:
print("Warning", f"Input location {input_folder_valid}\nOutput folder {output_folder_valid}...")
return
input_folder = self.o.settings["input_folder"]
output_folder = self.o.settings["output_folder"]
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"))
@ -56,80 +55,90 @@ class Optima35TUI():
i = 1
for image_file in image_files:
input_path = os.path.join(input_folder, image_file)
if self.o.settings["new_file_names"] != False:
image_name = self.o.name_images(self.o.settings["new_file_names"], i, len(image_files), self.o.settings["invert_image_order"])
if self.settings["new_file_names"] != False:
image_name = self.u.append_number_to_name(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)
self.o.process(input_path, output_path)
self.o.process_image(
image_input_file = input_path,
image_output_file = output_path,
file_type = self.settings["file_format"],
quality = self.settings["jpg_quality"],
compressing = self.settings["png_compression"],
optimize = self.settings["optimize"],
resize = self.settings["resize"],
watermark = self.settings["watermark"],
font_size = self.settings["font_size"],
grayscale = self.settings["grayscale"],
brightness = self.settings["brightness"],
contrast = self.settings["contrast"],
dict_for_exif = self.selected_exif,
gps = self.settings["gps"],
copy_exif = self.settings["copy_exif"])
self.u.progress_bar(i, len(image_files))
i += 1
def check_options(self):
def _check_options(self):
try:
if "Resize image" in self.settings["modifications"]:
self.o.settings["resize"] = self.settings["resize"]
self.settings["resize"] = self.settings["resize"]
else:
self.o.settings["resize"] = False
self.settings["resize"] = None
if "Convert to grayscale" in self.settings["modifications"]:
self.o.settings["grayscale"] = True
self.settings["grayscale"] = True
else:
self.o.settings["grayscale"] = False
self.settings["grayscale"] = False
if "Change contrast" in self.settings["modifications"]:
self.o.settings["contrast"] = self.settings["contrast"]
self.settings["contrast"] = self.settings["contrast"]
else:
self.o.settings["contrast"] = False
self.settings["contrast"] = None
if "Change brightness" in self.settings["modifications"]:
self.o.settings["brightness"] = self.settings["brightness"]
self.settings["brightness"] = self.settings["brightness"]
else:
self.o.settings["brightness"] = False
self.settings["brightness"] = None
if "Rename images" in self.settings["modifications"]:
self.o.settings["new_file_names"] = self.settings["new_file_names"]
self.settings["new_file_names"] = self.settings["new_file_names"]
else:
self.o.settings["new_file_names"] = False
self.settings["new_file_names"] = False
if "Invert image order" in self.settings["modifications"]:
self.o.settings["invert_image_order"] = True
self.settings["invert_image_order"] = True
else:
self.o.settings["invert_image_order"] = False
self.settings["invert_image_order"] = False
if "Add Watermark" in self.settings["modifications"]:
self.o.settings["watermark"] = self.settings["watermark"]
self.settings["watermark"] = self.settings["watermark"]
else:
self.o.settings["watermark"] = False
self.settings["watermark"] = None
self.o.settings["optimize"] = self.settings["optimize"]
self.o.settings["png_compression"] = self.settings["png_compression"]
self.o.settings["jpg_quality"] = self.settings["jpg_quality"]
self.settings["optimize"] = self.settings["optimize"]
self.settings["png_compression"] = self.settings["png_compression"]
self.settings["jpg_quality"] = self.settings["jpg_quality"]
self.o.settings["input_folder"] = self.settings["input_folder"]
self.o.settings["output_folder"] = self.settings["output_folder"]
self.o.settings["file_format"] = self.settings["file_format"]
self.o.settings["font_size"] = 2 # need to add option to select size
self.settings["input_folder"] = self.settings["input_folder"]
self.settings["output_folder"] = self.settings["output_folder"]
self.settings["file_format"] = self.settings["file_format"]
self.settings["font_size"] = 2 # need to add option to select size
self.o.settings["copy_exif"] = self.settings["copy_exif"]
self.settings["copy_exif"] = self.settings["copy_exif"]
if "Change EXIF" in self.settings["modifications"]: #missing
self.o.selected_exif = self.selected_exif #
self.o.settings["own_exif"] = True
if self.settings["gps"] != None:
self.o.settings["gps"] = self.settings["gps"]
else:
self.o.settings["gps"] = False
self.selected_exif = self._collect_exif_data() #
else:
self.o.settings["own_exif"] = False
self.selected_exif = None
except Exception as e:
print(f"Whoops: {e}")
def load_or_ask_settings(self):
def _load_or_ask_settings(self):
"""Load settings from a YAML file or ask the user if not present or incomplete."""
try:
if self.read_settings(self.settings_to_save):
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?")
@ -137,12 +146,12 @@ class Optima35TUI():
return
else:
print("No settings found...")
self.ask_for_settings()
self._ask_for_settings()
except Exception as e:
print(f"Error: {e}")
self.ask_for_settings()
self._ask_for_settings()
def ask_for_settings(self):
def _ask_for_settings(self):
print("Asking for new settings...\n")
self.settings["resize"] = self.take_input_and_validate(question = "Default resize percentage (below 100 downscale, above upscale): ", accepted_type = int, min_value = 10, max_value = 200)
self.settings["contrast"] = self.take_input_and_validate(question = "Default contrast percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100)
@ -151,16 +160,16 @@ class Optima35TUI():
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["optimize"] = self.tui.yes_no_menu("Optimize images i.e. compressing?")
self.write_settings(self.settings_to_save)
self._write_settings(self.settings_to_save)
def write_settings(self, keys_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.u.write_yaml(self.setting_file, filtered_settings)
print("New settings saved successfully.")
def read_settings(self, keys_to_load):
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.
@ -178,7 +187,7 @@ class Optima35TUI():
print("Settings file empty.")
return False
def collect_exif_data(self):
def _collect_exif_data(self):
"""Collect EXIF data based on user input."""
user_data = {}
fields = [
@ -187,24 +196,24 @@ class Optima35TUI():
]
for field in fields:
choise = self.tui.choose_menu(f"Enter {field.replace('_', ' ').title()}", self.exif_data[field])
choise = self.tui.choose_menu(f"Enter {field.replace('_', ' ').title()}", self.available_exif_data[field])
user_data[field] = choise
user_data["software"] = f"{self.o.name} {self.o.version}"
new_date = self.get_date_input()
new_date = self._get_date_input()
if new_date:
user_data["date_time_original"] = new_date
self.settings["gps"] = self.get_gps_input(user_data)
self.settings["gps"] = self._get_gps_input(user_data)
return user_data
def get_gps_input(self, test_exif):
def _get_gps_input(self, test_exif):
while True:
lat = input("Enter Latitude (xx.xxxxxx): ")
if lat == "":
return False
return None
long = input("Enter Longitude (xx.xxxxxx): ")
try:
self.o.exif_handler.add_geolocation_to_exif(test_exif, float(lat), float(long))
@ -212,7 +221,7 @@ class Optima35TUI():
except Exception:
print("Invalid GPS formate, try again...")
def get_date_input(self):
def _get_date_input(self):
# Partially chatGPT
while True:
date_input = input("Enter a date (yyyy-mm-dd): ")
@ -224,7 +233,7 @@ class Optima35TUI():
except ValueError:
print("Invalid date format. Please enter the date in yyyy-mm-dd format.")
def get_user_settings(self):
def _get_user_settings(self):
"""Get initial settings from the user."""
menu_options = [
"Resize image",
@ -241,7 +250,7 @@ class Optima35TUI():
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.o.name} v.{self.o.version} \nSelect what you want to do (esc or q to exit)",
f"\n{self.name} v{self.version} for {self.o.name} v.{self.o.version} \nSelect what you want to do (esc or q to exit)",
menu_options
)
if "Change EXIF" not in self.settings["modifications"]:
@ -304,9 +313,9 @@ class Optima35TUI():
def run(self):
"""Run the main program."""
self.load_or_ask_settings()
self.get_user_settings()
self.process()
self._load_or_ask_settings()
self._get_user_settings()
self._process()
print("Done")

View file

@ -217,13 +217,6 @@ class Ui_MainWindow(object):
self.widget_9.setMaximumSize(QSize(400, 50))
self.horizontalLayout_3 = QHBoxLayout(self.widget_9)
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
self.restart_button = QPushButton(self.widget_9)
self.restart_button.setObjectName(u"restart_button")
self.restart_button.setEnabled(False)
self.restart_button.setFlat(False)
self.horizontalLayout_3.addWidget(self.restart_button)
self.progressBar = QProgressBar(self.widget_9)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setEnabled(True)
@ -485,7 +478,6 @@ class Ui_MainWindow(object):
self.tabWidget.setCurrentIndex(0)
self.font_size_comboBox.setCurrentIndex(2)
self.restart_button.setDefault(False)
QMetaObject.connectSlotsByName(MainWindow)
@ -522,7 +514,6 @@ class Ui_MainWindow(object):
self.revert_checkbox.setText(QCoreApplication.translate("MainWindow", u"Revert order", None))
self.filename.setText("")
self.filename.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Enter file name", None))
self.restart_button.setText(QCoreApplication.translate("MainWindow", u"Restart", None))
self.start_button.setText(QCoreApplication.translate("MainWindow", u"Convert", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_1), QCoreApplication.translate("MainWindow", u"Main", None))
self.exif_group.setTitle(QCoreApplication.translate("MainWindow", u"EXIF EXPERIMENTAL", None))

View file

@ -365,22 +365,6 @@
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="restart_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Restart</string>
</property>
<property name="default">
<bool>false</bool>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="enabled">

View file

@ -20,6 +20,16 @@ class Utilities:
except PermissionError as e:
print(f"Error saving setings: {e}")
def append_number_to_name(self, base_name: str, current_image: int, total_images: int, invert: bool):
""""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 yes_no(self, str):
"""Ask user y/n question"""
while True: