Merge branch 'chore/cleaning' into 'main'

Chore/cleaning

See merge request CodeByMrFinchum/OptimaLab35!6
This commit is contained in:
Mr Finchum 2025-01-16 18:08:03 +00:00
commit b4c979bdb7
7 changed files with 443 additions and 417 deletions

View file

@ -1,6 +1,9 @@
# Changelog # Changelog
## 0.2.x ## 0.2.x
### 0.2.3
- Refactored code for improved readability.
### 0.2.2 ### 0.2.2
- Moved processing images into a different thread, making the UI responsiable while processing - Moved processing images into a different thread, making the UI responsiable while processing

View file

@ -9,7 +9,7 @@ authors = [{ name = "Mr. Finchum" }]
description = "User interface for optima35." description = "User interface for optima35."
readme = "pip_README.md" readme = "pip_README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = ["optima35>=0.6.6", "pyside6", "PyYAML", "packaging"] dependencies = ["optima35>=0.6.7", "pyside6", "PyYAML", "packaging"]
classifiers = [ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",

View file

@ -1 +1 @@
__version__ = "0.2.2" __version__ = "0.2.3"

View file

@ -2,8 +2,6 @@ import sys
import os import os
from datetime import datetime from datetime import datetime
import time
from optima35.core import OptimaManager from optima35.core import OptimaManager
from OptimaLab35.utils.utility import Utilities from OptimaLab35.utils.utility import Utilities
from OptimaLab35.ui.main_window import Ui_MainWindow from OptimaLab35.ui.main_window import Ui_MainWindow
@ -12,7 +10,7 @@ from OptimaLab35.ui.exif_handler_window import ExifEditor
from OptimaLab35.ui.simple_dialog import SimpleDialog # Import the SimpleDialog class from OptimaLab35.ui.simple_dialog import SimpleDialog # Import the SimpleDialog class
from OptimaLab35 import __version__ from OptimaLab35 import __version__
from PySide6.QtCore import QRunnable, QThreadPool, Signal, QObject from PySide6.QtCore import QRunnable, QThreadPool, Signal, QObject, QRegularExpression
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@ -31,7 +29,402 @@ from PySide6.QtWidgets import (
QProgressBar, QProgressBar,
) )
from PySide6.QtGui import QPixmap, QIcon from PySide6.QtGui import QPixmap, QRegularExpressionValidator
class OptimaLab35(QMainWindow, Ui_MainWindow):
def __init__(self):
super(OptimaLab35, self).__init__()
self.name = "OptimaLab35"
self.version = __version__
self.o = OptimaManager()
self.u = Utilities()
self.u.program_configs()
self.thread_pool = QThreadPool() # multi thread ChatGPT
# Initiate internal object
self.exif_file = os.path.expanduser("~/.config/OptimaLab35/exif.yaml")
self.available_exif_data = None
self.settings = {}
# UI elements
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.sd = SimpleDialog()
self.preview_window = PreviewWindow()
# Change UI elements
self.change_statusbar(f"Using {self.o.name} v{self.o.version}", 5000)
self.setWindowTitle(f"{self.name} v{self.version}")
self.default_ui_layout()
self.define_gui_interaction()
# Init function
def default_ui_layout(self):
self.ui.png_quality_spinBox.setVisible(False)
self.ui.png_quality_Slider.setVisible(False)
self.ui.quality_label_2.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.start_process)
self.ui.insert_exif_Button.clicked.connect(self.startinsert_exif)
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)
)
self.ui.tabWidget.currentChanged.connect(self.on_tab_changed)
self.ui.edit_exif_button.clicked.connect(self.open_exif_editor)
self.ui.actionAbout.triggered.connect(self.info_window)
self.ui.actionPreview.triggered.connect(self.open_preview_window)
self.ui.preview_Button.clicked.connect(self.open_preview_window)
regex = QRegularExpression(r"^\d{1,2}\.\d{1,6}$")
validator = QRegularExpressionValidator(regex)
self.ui.lat_lineEdit.setValidator(validator)
self.ui.long_lineEdit.setValidator(validator)
#layout.addWidget(self.ui.lat_lineEdit)
#layout.addWidget(self.ui.long_lineEdit)
# UI related function, changing parts, open, etc.
def open_preview_window(self):
self.preview_window.values_selected.connect(self.update_values)
self.preview_window.show()
def update_values(self, value1, value2, checkbox_state):
# Update main window's widgets with the received values
# ChatGPT
self.ui.brightness_spinBox.setValue(value1)
self.ui.contrast_spinBox.setValue(value2)
self.ui.grayscale_checkBox.setChecked(checkbox_state)
def info_window(self):
# ChatGPT, mainly
info_text = f"""
<h3>{self.name} v{self.version}</h3>
<p>(C) 2024-2025 Mr. Finchum aka CodeByMrFinchum</p>
<p>{self.name} is a GUI for <b>{self.o.name}</b> (v{self.o.version}).</p>
<p> Both projects are in active development, for more details, visit:</p>
<ul>
<li><a href="https://gitlab.com/CodeByMrFinchum/OptimaLab35">OptimaLab35 GitLab</a></li>
<li><a href="https://gitlab.com/CodeByMrFinchum/optima35">optima35 GitLab</a></li>
</ul>
"""
self.sd.show_dialog(f"{self.name} v{self.version}", info_text)
def handle_qprogressbar(self, value):
self.ui.progressBar.setValue(value)
def toggle_buttons(self, state):
self.ui.start_button.setEnabled(state)
if self.ui.exif_checkbox.isChecked():
self.ui.insert_exif_Button.setEnabled(state)
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):
"""Handle tab changes."""
# chatgpt
if index == 1: # EXIF Tab
self.handle_exif_file("read")
elif index == 0: # Main Tab
self.handle_exif_file("write")
def sort_dict_of_lists(self, input_dict):
# Partily ChatGPT
sorted_dict = {}
for key, lst in input_dict.items():
# Sort alphabetically for strings, numerically for numbers
if key == "iso":
lst = [int(x) for x in lst]
lst = sorted(lst)
lst = [str(x) for x in lst]
sorted_dict["iso"] = lst
elif all(isinstance(x, str) for x in lst):
sorted_dict[key] = sorted(lst, key=str.lower) # Case-insensitive sort for strings
return sorted_dict
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.available_exif_data.get(field, [])))
def open_exif_editor(self):
"""Open the EXIF Editor."""
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):
"""Update the EXIF data."""
self.exif_data = updated_exif_data
self.populate_exif()
def populate_exif(self):
# partly chatGPT
# Mapping of EXIF fields to comboboxes in the UI
print("populate")
combo_mapping = {
"make": self.ui.make_comboBox,
"model": self.ui.model_comboBox,
"lens": self.ui.lens_comboBox,
"iso": self.ui.iso_comboBox,
"image_description": self.ui.image_description_comboBox,
"user_comment": self.ui.user_comment_comboBox,
"artist": self.ui.artist_comboBox,
"copyright_info": self.ui.copyright_info_comboBox,
}
self.populate_comboboxes(combo_mapping)
def update_quality_options(self):
"""Update visibility of quality settings based on selected format."""
# Partly ChatGPT
selected_format = self.ui.image_type.currentText()
# Hide all quality settings
self.ui.png_quality_spinBox.setVisible(False)
self.ui.jpg_quality_spinBox.setVisible(False)
self.ui.jpg_quality_Slider.setVisible(False)
self.ui.png_quality_Slider.setVisible(False)
self.ui.quality_label_1.setVisible(False)
self.ui.quality_label_2.setVisible(False)
# Show relevant settings
if selected_format == "jpg":
self.ui.jpg_quality_spinBox.setVisible(True)
self.ui.jpg_quality_Slider.setVisible(True)
self.ui.quality_label_1.setVisible(True)
elif selected_format == "webp":
self.ui.jpg_quality_spinBox.setVisible(True)
self.ui.jpg_quality_Slider.setVisible(True)
self.ui.quality_label_1.setVisible(True)
elif selected_format == "png":
self.ui.png_quality_spinBox.setVisible(True)
self.ui.png_quality_Slider.setVisible(True)
self.ui.quality_label_2.setVisible(True)
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):
folder = QFileDialog.getExistingDirectory(self, "Select Output Folder")
if folder:
self.ui.output_path.setText(folder)
def change_statusbar(self, msg, timeout = 500):
self.ui.statusBar.showMessage(msg, timeout)
# Core functions
def on_processing_finished(self):
self.toggle_buttons(True)
self.handle_qprogressbar(0)
QMessageBox.information(self, "Information", "Finished!")
def image_list_from_folder(self, path):
image_files = [
f for f in os.listdir(path) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp"))
]
return image_files
def control_before_start(self, process):
input_folder = self.settings["input_folder"]
output_folder = self.settings["output_folder"]
image_list = self.image_list_from_folder(input_folder)
input_folder_valid = os.path.exists(input_folder)
if isinstance(output_folder, str):
output_folder_valid = os.path.exists(output_folder)
if process == "image":
if not input_folder or not output_folder:
QMessageBox.warning(self, "Warning", "Input or output folder not selected")
return False
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 False
if len(self.image_list_from_folder(output_folder)) != 0:
reply = QMessageBox.question(
self,
"Confirmation",
"Output folder containes images, which might get overritten, continue?",
QMessageBox.Yes | QMessageBox.No,
)
if reply == QMessageBox.No:
return False
elif process == "exif":
if not input_folder:
QMessageBox.warning(self, "Warning", "Input not selected")
return False
if output_folder:
reply = QMessageBox.question(
self,
"Confirmation",
"Output folder selected, but insert exif is done to images in input folder, Continue?",
QMessageBox.Yes | QMessageBox.No,
)
if reply == QMessageBox.No:
return False
if not input_folder_valid :
QMessageBox.warning(self, "Warning", f"Input location {input_folder_valid}")
return False
else:
print("Something went wrong")
if len(image_list) == 0:
QMessageBox.warning(self, "Warning", "Selected folder has no supported files.")
return False
return True
def start_process(self):
self.toggle_buttons(False)
self.update_settings() # Get all user selected data
if self.control_before_start("image") == False:
self.toggle_buttons(True)
return
image_list = self.image_list_from_folder(self.settings["input_folder"])
# Create a worker ChatGPT
worker = ImageProcessorRunnable(image_list, self.settings, self.handle_qprogressbar)
worker.signals.finished.connect(self.on_processing_finished)
# Start worker in thread pool ChatGPT
self.thread_pool.start(worker)
def insert_exif(self, image_files):
input_folder = self.settings["input_folder"]
i = 1
for image_file in image_files:
input_path = os.path.join(input_folder, image_file)
self.o.insert_dict_to_image(
exif_dict = self.settings["user_selected_exif"],
image_path = input_path,
gps = self.settings["gps"])
self.change_statusbar(image_file, 100)
self.handle_qprogressbar(int((i / len(image_files)) * 100))
i += 1
self.ui.progressBar.setValue(0)
def startinsert_exif(self):
self.toggle_buttons(False)
self.update_settings() # Get all user selected data
if self.control_before_start("exif") == False:
self.toggle_buttons(True)
return
image_list = self.image_list_from_folder(self.settings["input_folder"])
self.insert_exif(image_list)
self.toggle_buttons(True)
QMessageBox.information(self, "Information", "Finished")
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
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
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
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
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):
user_data = {}
user_data["make"] = self.ui.make_comboBox.currentText()
user_data["model"] = self.ui.model_comboBox.currentText()
user_data["lens"] = self.ui.lens_comboBox.currentText()
user_data["iso"] = self.ui.iso_comboBox.currentText()
user_data["image_description"] = self.ui.image_description_comboBox.currentText()
user_data["user_comment"] = self.ui.user_comment_comboBox.currentText()
user_data["artist"] = self.ui.artist_comboBox.currentText()
user_data["copyright_info"] = self.ui.copyright_info_comboBox.currentText()
user_data["software"] = f"{self.name} (v{self.version}) & {self.o.name} (v{self.o.version})"
return user_data
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"] = [float(self.ui.lat_lineEdit.text()), float(self.ui.long_lineEdit.text())]
else:
self.settings["gps"] = None
return selected_exif
def update_settings(self):
"""Update .settings from all GUI elements."""
# Basic
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()
# Quality
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["resize"] = int(self.ui.resize_spinBox.text()) if self.ui.resize_spinBox.text() != "100" else None
self.settings["optimize"] = self.get_checkbox_value(self.ui.optimize_checkBox)
# Changes for image
self.settings["brightness"] = int(self.ui.brightness_spinBox.text()) if self.ui.brightness_spinBox.text() != "0" else None
self.settings["contrast"] = int(self.ui.contrast_spinBox.text()) if self.ui.contrast_spinBox.text() != "0" else None
self.settings["grayscale"] = self.get_checkbox_value(self.ui.grayscale_checkBox)
# Watermark
self.settings["font_size"] = self.get_combobox_value(self.ui.font_size_comboBox)
self.settings["watermark"] = self.get_text_value(self.ui.watermark_lineEdit)
# Naming
new_name = self.get_text_value(self.ui.filename, False) if self.ui.rename_checkbox.isChecked() else False
if isinstance(new_name, str): new_name = new_name.replace(" ", "_")
self.settings["new_file_names"] = new_name
self.settings["invert_image_order"] = self.get_checkbox_value(self.ui.revert_checkbox) if new_name is not False else None
# Handle EXIF data selection
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["own_date"] = self.get_checkbox_value(self.ui.add_date_checkBox)
if self.settings["own_exif"]:
self.settings["user_selected_exif"] = self.get_selected_exif()
else:
self.settings["user_selected_exif"] = None
self.settings["gps"] = None
# Helper functions, low level
def handle_exif_file(self, do):
# TODO: add check if data is missing.
if do == "read":
file_dict = self.u.read_yaml(self.exif_file)
self.available_exif_data = self.sort_dict_of_lists(file_dict)
elif do == "write":
self.u.write_yaml(self.exif_file, self.available_exif_data)
class PreviewWindow(QMainWindow, Ui_Preview_Window): class PreviewWindow(QMainWindow, Ui_Preview_Window):
values_selected = Signal(int, int, bool) values_selected = Signal(int, int, bool)
@ -42,21 +435,21 @@ class PreviewWindow(QMainWindow, Ui_Preview_Window):
self.ui.setupUi(self) self.ui.setupUi(self)
self.o = OptimaManager() self.o = OptimaManager()
## Ui interaction ## Ui interaction
self.ui.load_Button.clicked.connect(self._browse_file) self.ui.load_Button.clicked.connect(self.browse_file)
self.ui.update_Button.clicked.connect(self._update_preview) self.ui.update_Button.clicked.connect(self.update_preview)
self.ui.close_Button.clicked.connect(self._close_window) self.ui.close_Button.clicked.connect(self.close_window)
self.ui.reset_brightness_Button.clicked.connect(lambda: self.ui.brightness_spinBox.setValue(0)) self.ui.reset_brightness_Button.clicked.connect(lambda: self.ui.brightness_spinBox.setValue(0))
self.ui.reset_contrast_Button.clicked.connect(lambda: self.ui.contrast_spinBox.setValue(0)) self.ui.reset_contrast_Button.clicked.connect(lambda: self.ui.contrast_spinBox.setValue(0))
self.preview_image = None self.preview_image = None
def _browse_file(self): def browse_file(self):
file = QFileDialog.getOpenFileName(self, caption = "Select File", filter = ("Images (*.png *.webp *.jpg *.jpeg)")) file = QFileDialog.getOpenFileName(self, caption = "Select File", filter = ("Images (*.png *.webp *.jpg *.jpeg)"))
if file[0]: if file[0]:
self.ui.image_path_lineEdit.setText(file[0]) self.ui.image_path_lineEdit.setText(file[0])
self._update_preview() self.update_preview()
def _update_preview(self): def update_preview(self):
path = self.ui.image_path_lineEdit.text() path = self.ui.image_path_lineEdit.text()
if not os.path.isfile(path): if not os.path.isfile(path):
return return
@ -77,7 +470,7 @@ class PreviewWindow(QMainWindow, Ui_Preview_Window):
self.preview_image = QPixmap.fromImage(img) self.preview_image = QPixmap.fromImage(img)
self.ui.QLabel.setPixmap(self.preview_image) self.ui.QLabel.setPixmap(self.preview_image)
def _close_window(self): def close_window(self):
# Emit the signal with the values from the spinboxes and checkbox # Emit the signal with the values from the spinboxes and checkbox
if self.ui.checkBox.isChecked(): if self.ui.checkBox.isChecked():
self.values_selected.emit(self.ui.brightness_spinBox.value(), self.ui.contrast_spinBox.value(), self.ui.grayscale_checkBox.isChecked()) self.values_selected.emit(self.ui.brightness_spinBox.value(), self.ui.contrast_spinBox.value(), self.ui.grayscale_checkBox.isChecked())
@ -132,405 +525,6 @@ class ImageProcessorRunnable(QRunnable):
self.signals.finished.emit() self.signals.finished.emit()
class OptimaLab35(QMainWindow, Ui_MainWindow):
def __init__(self):
super(OptimaLab35, self).__init__()
self.name = "OptimaLab35"
self.version = __version__
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.o = OptimaManager()
self.check_version()
self.u = Utilities()
self.u.program_configs()
self.exif_file = os.path.expanduser("~/.config/OptimaLab35/exif.yaml")
self.available_exif_data = None
self.settings = {}
self.setWindowTitle(f"{self.name} v{self.version}")
self._default_ui_layout()
self._define_gui_interaction()
self.sd = SimpleDialog()
self._change_statusbar(f"Using {self.o.name} v{self.o.version}", 5000)
# Instantiate the second window
self.preview_window = PreviewWindow()
self.thread_pool = QThreadPool() # multi thread ChatGPT
def open_preview_window(self):
self.preview_window.values_selected.connect(self.update_values)
self.preview_window.show()
def update_values(self, value1, value2, checkbox_state):
# Update main window's widgets with the received values
# ChatGPT
self.ui.brightness_spinBox.setValue(value1)
self.ui.contrast_spinBox.setValue(value2)
self.ui.grayscale_checkBox.setChecked(checkbox_state)
def _default_ui_layout(self):
self.ui.png_quality_spinBox.setVisible(False)
self.ui.png_quality_Slider.setVisible(False)
self.ui.quality_label_2.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._start_process)
self.ui.insert_exif_Button.clicked.connect(self._start_insert_exif)
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)
)
self.ui.tabWidget.currentChanged.connect(self._on_tab_changed)
self.ui.edit_exif_button.clicked.connect(self._open_exif_editor)
self.ui.actionAbout.triggered.connect(self._info_window)
self.ui.actionPreview.triggered.connect(self.open_preview_window)
self.ui.preview_Button.clicked.connect(self.open_preview_window)
def _info_window(self):
# ChatGPT, mainly
info_text = f"""
<h3>{self.name} v{self.version}</h3>
<p>(C) 2024-2025 Mr. Finchum aka CodeByMrFinchum</p>
<p>{self.name} is a GUI for <b>{self.o.name}</b> (v{self.o.version}).</p>
<p> Both projects are in active development, for more details, visit:</p>
<ul>
<li><a href="https://gitlab.com/CodeByMrFinchum/OptimaLab35">OptimaLab35 GitLab</a></li>
<li><a href="https://gitlab.com/CodeByMrFinchum/optima35">optima35 GitLab</a></li>
</ul>
"""
self.sd.show_dialog(f"{self.name} v{self.version}", info_text)
def _prepear_image(self):
pass
def _image_list_from_folder(self, path):
image_files = [
f for f in os.listdir(path) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp"))
]
return image_files
def _start_process(self):
self._toggle_buttons(False)
self._update_settings() # Get all user selected data
input_folder = self.settings["input_folder"]
output_folder = self.settings["output_folder"]
if not input_folder or not output_folder:
QMessageBox.warning(self, "Warning", "Input or output folder not selected")
self._toggle_buttons(True)
return
input_folder_valid = os.path.exists(input_folder)
output_folder_valid = os.path.exists(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}...")
self._toggle_buttons(True)
return
image_list = self._image_list_from_folder(input_folder)
if len(image_list) == 0:
QMessageBox.warning(self, "Warning", "Selected folder has no supported files.")
self._toggle_buttons(True)
return
if len(self._image_list_from_folder(output_folder)) != 0:
reply = QMessageBox.question(
self,
"Confirmation",
"Output folder containes images, which might get overritten, continue?",
QMessageBox.Yes | QMessageBox.No,
)
if reply == QMessageBox.No:
self._toggle_buttons(True)
return
# Create a worker ChatGPT
worker = ImageProcessorRunnable(image_list, self.settings, self._handle_qprogressbar)
worker.signals.finished.connect(self._on_processing_finished)
# Start worker in thread pool ChatGPT
self.thread_pool.start(worker)
def _handle_qprogressbar(self, value):
self.ui.progressBar.setValue(value)
def _on_processing_finished(self):
self._toggle_buttons(True)
self._handle_qprogressbar(0)
QMessageBox.information(self, "Information", "Finished!")
def _toggle_buttons(self, state):
self.ui.start_button.setEnabled(state)
if self.ui.exif_checkbox.isChecked():
self.ui.insert_exif_Button.setEnabled(state)
def _insert_exif(self, image_files):
input_folder = self.settings["input_folder"]
i = 1
for image_file in image_files:
input_path = os.path.join(input_folder, image_file)
self.o.insert_dict_to_image(
exif_dict = self.settings["user_selected_exif"],
image_path = input_path,
gps = self.settings["gps"])
self._change_statusbar(image_file, 100)
self._handle_qprogressbar(int((i / len(image_files)) * 100))
i += 1
self.ui.progressBar.setValue(0)
def _start_insert_exif(self):
self._toggle_buttons(False)
self._update_settings() # Get all user selected data
input_folder = self.settings["input_folder"]
output_folder = self.settings["output_folder"]
if not input_folder:
QMessageBox.warning(self, "Warning", "Input not selected")
self._toggle_buttons(True)
return
if output_folder:
reply = QMessageBox.question(
self,
"Confirmation",
"Output folder selected, but insert exif is done to images in input folder, Continue?",
QMessageBox.Yes | QMessageBox.No,
)
if reply == QMessageBox.No:
self._toggle_buttons(True)
return
input_folder_valid = os.path.exists(input_folder)
if not input_folder_valid :
QMessageBox.warning(self, "Warning", f"Input location {input_folder_valid}")
self._toggle_buttons(True)
return
image_list = self._image_list_from_folder(input_folder)
if len(image_list) == 0:
QMessageBox.warning(self, "Warning", "Selected folder has no supported files.")
self._toggle_buttons(True)
return
self._insert_exif(image_list)
self._toggle_buttons(True)
QMessageBox.information(self, "Information", "Finished")
def _open_exif_editor(self):
"""Open the EXIF Editor."""
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):
"""Update the EXIF data."""
self.exif_data = updated_exif_data
self._populate_exif()
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):
"""Handle tab changes."""
# chatgpt
if index == 1: # EXIF Tab
self._handle_exif_file("read")
elif index == 0: # Main Tab
self._handle_exif_file("write")
def _sort_dict_of_lists(self, input_dict):
# Partily ChatGPT
sorted_dict = {}
for key, lst in input_dict.items():
# Sort alphabetically for strings, numerically for numbers
if key == "iso":
lst = [int(x) for x in lst]
lst = sorted(lst)
lst = [str(x) for x in lst]
sorted_dict["iso"] = lst
elif all(isinstance(x, str) for x in lst):
sorted_dict[key] = sorted(lst, key=str.lower) # Case-insensitive sort for strings
return sorted_dict
def _handle_exif_file(self, do):
if do == "read":
file_dict = self.u.read_yaml(self.exif_file)
self.available_exif_data = self._sort_dict_of_lists(file_dict)
elif do == "write":
self.u.write_yaml(self.exif_file, self.available_exif_data)
def _populate_exif(self):
# partly chatGPT
# Mapping of EXIF fields to comboboxes in the UI
combo_mapping = {
"make": self.ui.make_comboBox,
"model": self.ui.model_comboBox,
"lens": self.ui.lens_comboBox,
"iso": self.ui.iso_comboBox,
"image_description": self.ui.image_description_comboBox,
"user_comment": self.ui.user_comment_comboBox,
"artist": self.ui.artist_comboBox,
"copyright_info": self.ui.copyright_info_comboBox,
}
self._populate_comboboxes(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.available_exif_data.get(field, [])))
def _update_quality_options(self):
"""Update visibility of quality settings based on selected format."""
# ChatGPT
selected_format = self.ui.image_type.currentText()
# Hide all quality settings
self.ui.png_quality_spinBox.setVisible(False)
self.ui.jpg_quality_spinBox.setVisible(False)
self.ui.jpg_quality_Slider.setVisible(False)
self.ui.png_quality_Slider.setVisible(False)
self.ui.quality_label_1.setVisible(False)
self.ui.quality_label_2.setVisible(False)
# Show relevant settings
if selected_format == "jpg":
self.ui.jpg_quality_spinBox.setVisible(True)
self.ui.jpg_quality_Slider.setVisible(True)
self.ui.quality_label_1.setVisible(True)
elif selected_format == "webp":
self.ui.jpg_quality_spinBox.setVisible(True)
self.ui.jpg_quality_Slider.setVisible(True)
self.ui.quality_label_1.setVisible(True)
elif selected_format == "png":
self.ui.png_quality_spinBox.setVisible(True)
self.ui.png_quality_Slider.setVisible(True)
self.ui.quality_label_2.setVisible(True)
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):
folder = QFileDialog.getExistingDirectory(self, "Select Output Folder")
if folder:
self.ui.output_path.setText(folder)
def _change_statusbar(self, msg, timeout = 500):
self.ui.statusBar.showMessage(msg, timeout)
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
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
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
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
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)
# Conditional settings with logic
self.settings["resize"] = int(self.ui.resize_spinBox.text()) if self.ui.resize_spinBox.text() != "100" else None
self.settings["brightness"] = int(self.ui.brightness_spinBox.text()) if self.ui.brightness_spinBox.text() != "0" else None
self.settings["contrast"] = int(self.ui.contrast_spinBox.text()) if self.ui.contrast_spinBox.text() != "0" else None
new_name = self._get_text_value(self.ui.filename, False) if self.ui.rename_checkbox.isChecked() else False
if isinstance(new_name, str): new_name = new_name.replace(" ", "_")
self.settings["new_file_names"] = new_name
self.settings["watermark"] = self.ui.watermark_lineEdit.text() if len(self.ui.watermark_lineEdit.text()) != 0 else None
# Handle EXIF data selection
if self.settings["own_exif"]:
self.settings["user_selected_exif"] = self._get_selected_exif()
else:
self.settings["user_selected_exif"] = None
self.settings["gps"] = None
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):
user_data = {}
user_data["make"] = self.ui.make_comboBox.currentText()
user_data["model"] = self.ui.model_comboBox.currentText()
user_data["lens"] = self.ui.lens_comboBox.currentText()
user_data["iso"] = self.ui.iso_comboBox.currentText()
user_data["image_description"] = self.ui.image_description_comboBox.currentText()
user_data["user_comment"] = self.ui.user_comment_comboBox.currentText()
user_data["artist"] = self.ui.artist_comboBox.currentText()
user_data["copyright_info"] = self.ui.copyright_info_comboBox.currentText()
user_data["software"] = f"{self.name} {self.version} with {self.o.name} {self.o.version}"
return user_data
def closeEvent(self, event):
self.preview_window.close()
def check_version(self, min_version="0.6.6"):
# Mainly ChatGPT
from packaging import version # Use `packaging` for robust version comparison
current_version = self.o.version
if version.parse(current_version) < version.parse(min_version):
msg = (
f"optima35 version {current_version} detected.\n"
f"Minimum required version is {min_version}.\n"
"Please update the core package to continue.\n"
"https://pypi.org/project/optima35/"
)
QMessageBox.critical(None, "Version Error", msg)
sys.exit(1)
def main(): def main():
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)

View file

@ -221,7 +221,7 @@ class OptimaLab35_lite():
long = input("Enter Longitude (xx.xxxxxx): ") long = input("Enter Longitude (xx.xxxxxx): ")
try: try:
self.o.exif_handler.add_geolocation_to_exif(test_exif, float(lat), float(long)) self.o.exif_handler.add_geolocation_to_exif(test_exif, float(lat), float(long))
return [lat, long] return [float(lat), float(long)]
except Exception: except Exception:
print("Invalid GPS formate, try again...") print("Invalid GPS formate, try again...")

View file

@ -282,6 +282,7 @@ class Ui_MainWindow(object):
self.revert_checkbox = QCheckBox(self.rename_group) self.revert_checkbox = QCheckBox(self.rename_group)
self.revert_checkbox.setObjectName(u"revert_checkbox") self.revert_checkbox.setObjectName(u"revert_checkbox")
self.revert_checkbox.setEnabled(False)
self.gridLayout_6.addWidget(self.revert_checkbox, 0, 1, 1, 1) self.gridLayout_6.addWidget(self.revert_checkbox, 0, 1, 1, 1)
@ -507,12 +508,14 @@ class Ui_MainWindow(object):
self.lat_lineEdit = QLineEdit(self.gps_groupBox) self.lat_lineEdit = QLineEdit(self.gps_groupBox)
self.lat_lineEdit.setObjectName(u"lat_lineEdit") self.lat_lineEdit.setObjectName(u"lat_lineEdit")
self.lat_lineEdit.setEnabled(False) self.lat_lineEdit.setEnabled(False)
self.lat_lineEdit.setMaxLength(8)
self.horizontalLayout_4.addWidget(self.lat_lineEdit) self.horizontalLayout_4.addWidget(self.lat_lineEdit)
self.long_lineEdit = QLineEdit(self.gps_groupBox) self.long_lineEdit = QLineEdit(self.gps_groupBox)
self.long_lineEdit.setObjectName(u"long_lineEdit") self.long_lineEdit.setObjectName(u"long_lineEdit")
self.long_lineEdit.setEnabled(False) self.long_lineEdit.setEnabled(False)
self.long_lineEdit.setMaxLength(8)
self.horizontalLayout_4.addWidget(self.long_lineEdit) self.horizontalLayout_4.addWidget(self.long_lineEdit)
@ -584,6 +587,7 @@ class Ui_MainWindow(object):
self.exif_checkbox.toggled.connect(self.edit_exif_button.setEnabled) self.exif_checkbox.toggled.connect(self.edit_exif_button.setEnabled)
self.add_date_checkBox.toggled.connect(self.dateEdit.setEnabled) self.add_date_checkBox.toggled.connect(self.dateEdit.setEnabled)
self.jpg_quality_spinBox.valueChanged.connect(self.jpg_quality_Slider.setValue) self.jpg_quality_spinBox.valueChanged.connect(self.jpg_quality_Slider.setValue)
self.rename_checkbox.toggled.connect(self.revert_checkbox.setEnabled)
self.tabWidget.setCurrentIndex(0) self.tabWidget.setCurrentIndex(0)
self.font_size_comboBox.setCurrentIndex(2) self.font_size_comboBox.setCurrentIndex(2)
@ -652,9 +656,9 @@ class Ui_MainWindow(object):
self.gps_groupBox.setTitle(QCoreApplication.translate("MainWindow", u"GPS", None)) self.gps_groupBox.setTitle(QCoreApplication.translate("MainWindow", u"GPS", None))
self.gps_checkBox.setText(QCoreApplication.translate("MainWindow", u"add gps", None)) self.gps_checkBox.setText(QCoreApplication.translate("MainWindow", u"add gps", None))
self.lat_lineEdit.setText("") self.lat_lineEdit.setText("")
self.lat_lineEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"latitude [S, N]", None)) self.lat_lineEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"xx.xxxxxx latitude", None))
self.long_lineEdit.setText("") self.long_lineEdit.setText("")
self.long_lineEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"longitude [W, E]", None)) self.long_lineEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"xx.xxxxxx longitude", None))
self.date_groupBox.setTitle(QCoreApplication.translate("MainWindow", u"Optional", None)) self.date_groupBox.setTitle(QCoreApplication.translate("MainWindow", u"Optional", None))
self.add_date_checkBox.setText(QCoreApplication.translate("MainWindow", u"add date", None)) self.add_date_checkBox.setText(QCoreApplication.translate("MainWindow", u"add date", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QCoreApplication.translate("MainWindow", u"EXIF", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QCoreApplication.translate("MainWindow", u"EXIF", None))

View file

@ -476,6 +476,9 @@
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QCheckBox" name="revert_checkbox"> <widget class="QCheckBox" name="revert_checkbox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text"> <property name="text">
<string>Revert order</string> <string>Revert order</string>
</property> </property>
@ -753,8 +756,11 @@
<property name="text"> <property name="text">
<string/> <string/>
</property> </property>
<property name="maxLength">
<number>8</number>
</property>
<property name="placeholderText"> <property name="placeholderText">
<string>latitude [S, N]</string> <string>xx.xxxxxx latitude</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -766,8 +772,11 @@
<property name="text"> <property name="text">
<string/> <string/>
</property> </property>
<property name="maxLength">
<number>8</number>
</property>
<property name="placeholderText"> <property name="placeholderText">
<string>longitude [W, E]</string> <string>xx.xxxxxx longitude</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -1211,5 +1220,21 @@
</hint> </hint>
</hints> </hints>
</connection> </connection>
<connection>
<sender>rename_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>revert_checkbox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>124</x>
<y>592</y>
</hint>
<hint type="destinationlabel">
<x>315</x>
<y>592</y>
</hint>
</hints>
</connection>
</connections> </connections>
</ui> </ui>