Feature/gui

This commit is contained in:
Mr Finchum 2024-12-30 20:19:49 +00:00
parent c36ab9b733
commit 1512e8efeb
15 changed files with 2328 additions and 299 deletions

View file

@ -1,5 +1,52 @@
# Changelog
## 0.3.x
### 0.3.4: Features Finalized
- Core Features Completed:
- All functions are now available, though minor bugs may exist.
- GUI State:
- Interface is in a polished state but still needs refinement.
**Implemented Features:**
- Image Processing:
- Resizing
- Renaming with order adjustment
- Grayscale conversion
- Brightness adjustment
- Contrast adjustment
- EXIF Management:
- Copy EXIF data
- Add custom EXIF information
- Add GPS data
- Add date to EXIF
- Watermarking:
- Watermark functionality is now finalized and no longer experimental.
### 0.3.3: Exif implemented
- New EXIF settings tab in the GUI.
- Popup window for editing EXIF data.
- Added options for:
- Adding date to EXIF.
- Adding GPS coordinates to EXIF.
### 0.3.2: New ui
- Major overhaul of the gui
- Adding preview to readme
- All options on the first tab work
- Watermark still experimentel, font selecting will be added
- Second tab is for exif control, copy option works already
### 0.3.1: license change
- Changed license from CC BY-NC 4.0 to AGPL-3.0.
### 0.3.0: Qt GUI Transition (PySide6)
- Shifted from a TUI approach to a GUI-based layout.
- Adopted **PySide6** for the GUI and **Qt Designer** for designing layouts.
- Introduced a proof-of-concept UI, and adding own exif does not work
- Watermark is still in testing / alpha
- Original TUI version was forked and is still aviable, currently this branch includes the TUI version until the next minor version change.
## 0.2.x
### 0.2.1: Merge from TUI fork
- Ensure watermark is white with black borders.

View file

@ -1,15 +1,20 @@
**Software License:**
# GNU Affero General Public License v3.0
**Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0):**
- **Share:** Copy and redistribute the material in any medium or format.
- **Adapt:** Remix, transform, and build upon the material.
- **Attribution:** Give appropriate credit, provide a link to the license, and indicate if changes were made. Do not suggest that the licensor endorses you or your use.
- **NonCommercial:** Do not use the material for commercial purposes.
- **No Additional Restrictions:** Do not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
Copyright (C) [2024] [Mr Finchum]
**Read the full license here:** [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/legalcode)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License.
**Additional Terms (Custom License for Software):**
- **No Warranty:** The material is provided "as is," and the licensor makes no representations or warranties regarding its fitness for a particular purpose. The licensor shall not be liable for any damages or losses resulting from the use of the material, including but not limited to errors, data loss, corrupted data, or unintended consequences. By using the material, you agree that you do so at your own risk, and you assume full responsibility for any outcomes.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
- **License Change Policy:** The custom license may change at any time, and changes will apply only to new code added after the change. All code created prior to the license change will remain available under the previous license to ensure fair use. Users are responsible for regularly checking the license file for updates.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
---
## Additional Notes:
This program is licensed under the AGPL-3.0, which requires that any modifications or derivative works distributed or used over a network must also be licensed under the AGPL-3.0.

View file

@ -8,12 +8,20 @@ This project is a *port* of my earlier work, an collection of [bash script](http
**Please check** if a new branch is available and read the **changelog** to see the progress and current features of the program. The README might sometimes lag behind.
OPTIMA-35 is evolving! The project is transitioning from a terminal-based user interface (TUI) to a graphical user interface (GUI) using Qt (via PySide6). First TUI version was forked to [OPTIMA-35 TUI](https://gitlab.com/python_projects3802849/optima-35-tui). I intend to keep the TUI version functional since it is usefull for headless setup.
**GUI for OPTIMA-35 v0.3.4** with KvArcDark theme
*Last preview until GUI is finished.*
![img](https://gitlab.com/python_projects3802849/optima-35/-/raw/feature/GUI/media/main_tab.png){width=40%}
![img](https://gitlab.com/python_projects3802849/optima-35/-/raw/feature/GUI/media/exif_tab.png){width=40%}
![img](https://gitlab.com/python_projects3802849/optima-35/-/raw/feature/GUI/media/exifeditor.png){width=40%}
## **Current Status**
- While the program works and core features are available, there are currently no safety checks in place. For example, the program will write / save an image without verifying if a file with the same name already exists.
- Additionally, while EXIF data/metadata should be implemented correctly, there is a possibility of overlooked issues. In the worst case, a program might throw an error when handling EXIF data, though this has not occurred so far.
**The README is temporarily outdated** while the GUI version is under development. For the latest updates, please check the **changelog**—I always maintain a detailed log of changes.
### Available Features:
- Initial basic TUI functionality using `simple_term_menu`
- Core features:
- resizing
- renaming
@ -23,10 +31,6 @@ This project is a *port* of my earlier work, an collection of [bash script](http
- Exif management
- Add watermark
**Gif of program in action**
![my-gif](https://gitlab.com/python_projects3802849/optima-35/-/raw/main/media/v0.1.0-demo.gif?ref_type=heads)
## Dependencies
To run **OPTIMA-35**, the following Python libraries are required:
@ -34,29 +38,22 @@ To run **OPTIMA-35**, the following Python libraries are required:
- **pyyaml**: To handle YAML files for configuration and settings.
- **piexif**: To read, modify, and write EXIF metadata.
- **Pillow**: For image processing.
- **simple\_term\_menu**: For building the initial TUI interface.
- **pyside6**: GUI
### Installing Dependencies
You can install the dependencies using `pip`:
```bash
pip install textual pyyaml piexif pillow simple-term-menu
pip install pyyaml piexif pillow pyside6
```
Alternatively, you can use `conda` or its alternatives (`anaconda`, `mamba`, `micromamba`):
```bash
conda install -c conda-forge textual pyyaml piexif pillow simple-term-menu
conda install -c conda-forge pyyaml piexif pillow pyside6
```
## Development Approach
Compared to my previous project, [FTL Save Manager](https://gitlab.com/python_projects3802849/ftl-save-manager), this project emphasizes:
- **Enhanced Modularity**: Classes and components are organized into separate files, making the codebase more maintainable and scalable.
- **Improved Design Principles**: Focus on creating reusable and flexible code for future expansion.
- **Slower Code Pushes**: Updates and code releases will be less frequent but of higher quality, ensuring stability and adherence to best practices.
# Use of LLMs
In the interest of transparency, I disclose that Generative AI (GAI) large language models (LLMs), including OpenAIs ChatGPT and Ollama models (e.g., OpenCoder and Qwen2.5-coder), have been used to assist in this project.
@ -68,8 +65,9 @@ In the interest of transparency, I disclose that Generative AI (GAI) large langu
In cases where LLMs contribute directly to code or provide substantial optimizations, such contributions will be disclosed and documented in the relevant sections of the codebase.
mradermacher gguf Q4K-M Instruct version of infly/OpenCoder-1.5B
unsloth gguf Q4K_M Instruct version of both Qwen/QWEN2 1.5B and 3B
**Ollama**
- mradermacher gguf Q4K-M Instruct version of infly/OpenCoder-1.5B
- unsloth gguf Q4K_M Instruct version of both Qwen/QWEN2 1.5B and 3B
### References
1. **Huang, Siming, et al.**

View file

@ -1,13 +1,17 @@
# Example file. You can add and remove entries here.
make:
- Nikon
model:
- FG
- F50
lens:
- Nikon LENS SERIES E 50mm
- AF NIKKOR 35-70mm
iso: # Numeric values cause errors with simple_term_menu and must be quoted. This issue will be resolved with the future UI switch.
# Example file
artist:
- Mr. Finchum
- John Doe
copyright_info:
- All Rights Reserved
- CC BY-NC 4.0
- No Copyright
image_description:
- ILFORD DELTA 3200
- ILFORD ILFOCOLOR
- LomoChrome Turquoise
- Kodak 200
iso:
- "100"
- "200"
- "400"
@ -15,15 +19,17 @@ iso: # Numeric values cause errors with simple_term_menu and must be quoted. Th
- "1000"
- "1600"
- "3200"
image_description:
- ILFORD DELTA 3200
- ILFORD ILFOCOLOR
- LomoChrome Turquoise
lens:
- Nikon LENS SERIES E 50mm
- AF NIKKOR 35-70mm
- Canon FD 50mm f/1.4 S.S.C
make:
- Nikon
- Canon
model:
- FG
- F50
- AE-1
user_comment:
- Scanner.NORITSU-KOKI
- Scanner.NA
artist:
- Mr. Finchum
copyright_info:
- All Rights Reserved
- No Copyright

527
main.py
View file

@ -1,207 +1,285 @@
import sys
import os
import re
import time
import subprocess
from datetime import datetime
from utils.utility import Utilities
from utils.image_handler import ImageProcessor, ExifHandler
from ui.tui import SimpleTUI
class Optima35:
# The layout of class Optima35 was originally made by ChatGPT, but major adjustments have been made. To remain transparent, I disclose this.
def __init__(self, settings_file, exif_options_file):
from ui.main_window import Ui_MainWindow
from ui.exif_handler_window import ExifEditor
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, exif_file, version):
super(Optima35QT6, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.define_settings(exif_file, version)
self.setWindowTitle(f"{self.name} v{self.version}")
self.default_ui_layout()
self.define_gui_interaction()
# GUI
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)
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.restart_button.clicked.connect(self.restart_app)
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.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."""
# improved by chatGPT
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 handle_exif_file(self, do):
if do == "read":
self.exif_data = self.utilities.read_yaml(self.exif_file)
elif do == "write":
self.utilities.write_yaml(self.exif_file, self.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.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)
# Show relevant settings
if selected_format == "jpg":
self.ui.jpg_quality_spinBox.setVisible(True)
elif selected_format == "webp":
self.ui.jpg_quality_spinBox.setVisible(True)
elif selected_format == "png":
self.ui.png_quality_spinBox.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 handle_qprogressbar(self, current, total):
progress = int((100 / total) * current)
self.ui.progressBar.setValue(progress)
def check_options(self):
try:
self.settings["input_folder"] = self.ui.input_path.text()
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.exif_copy_checkBox.isChecked()
self.settings["own_exif"] = self.ui.exif_checkbox.isChecked()
self.settings["font_size"] = self.ui.font_size_comboBox.currentIndex() + 1
self.settings["optimize"] = self.ui.optimize_checkBox.isChecked()
self.settings["own_date"] = self.ui.add_date_checkBox.isChecked()
if self.ui.resize_checkbox.isChecked():
self.settings["resize_percentage"] = int(self.ui.resize_spinBox.text())
if self.ui.brightness_checkbox.isChecked():
self.settings["brightness_percentage"] = int(self.ui.brightness_spinBox.text())
if self.ui.contrast_checkbox.isChecked():
self.settings["contrast_percentage"] = int(self.ui.contrast_spinBox.text())
if self.ui.rename_checkbox.isChecked() and self.ui.filename.text() != "":
self.settings["new_file_names"] = self.ui.filename.text()
if self.ui.watermark_checkbox.isChecked() and self.ui.watermark_lineEdit.text() != "":
self.settings["watermark"] = self.ui.watermark_lineEdit.text()
if self.settings["own_exif"]:
self.selected_exif = self.collect_selected_exif()
if self.ui.add_date_checkBox.isChecked():
self.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"] = False
except Exception as e:
print(f"Whoops: {e}")
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"OPTIMA-35 {self.version}"
return user_data
def rebuild_ui(self):
# Define the bash script to execute
print("Rebuild function disabled")
return
bash_script = "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)
# core
def define_settings(self, exif_file, version):
self.name = "OPTIMA-35"
self.version = "0.2.1"
self.version = version
self.utilities = Utilities()
self.image_processor = ImageProcessor()
self.exif_handler = ExifHandler()
self.tui = SimpleTUI()
self.exif_file = exif_file
self.settings = {
"input_folder": None,
"output_folder": None,
"file_format": None,
"resize_percentage": None,
"copy_exif": None,
"contrast_percentage": None,
"brightness_percentage": None,
"new_file_names": None,
"resize_percentage": False,
"contrast_percentage": False,
"brightness_percentage": False,
"new_file_names": False,
"invert_image_order": False,
"watermark_text": None,
"modifications": [],
"copy_exif": False,
"own_exif": False,
"watermark": False,
"grayscale": False,
"jpg_quality": None,
"png_compression": None,
"font_size": None,
"optimize": False,
"gps": False
}
self.settings_to_save = [
"resize_percentage",
"jpg_quality",
"png_compression",
"web_optimize",
"contrast_percentage",
"brightness_percentage"
]
self.exif_choices = self.utilities.read_yaml(exif_options_file)
self.setting_file = settings_file
self.exif_data = None
def load_or_ask_settings(self):
"""Load settings from a YAML file or ask the user if not present or incomplete."""
# Partially ChatGPT
if self.read_settings(self.settings_to_save):
for item in self.settings_to_save:
print(f"{item}: {self.settings[item]}")
use_saved = self.tui.yes_no_menu("Use these settings?")
if use_saved:
return
else:
print("No settings found...")
print("Asking for new settings...\n")
self.settings["resize_percentage"] = self.take_input_and_validate(question = "Default resize percentage (below 100 downscale, above upscale): ", accepted_type = int, min_value = 1, max_value = 200)
self.settings["contrast_percentage"] = self.take_input_and_validate(question = "Default contrast percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100)
self.settings["brightness_percentage"] = self.take_input_and_validate(question = "Default brighness percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100)
self.settings["jpg_quality"] = self.take_input_and_validate(question = "JPEG quality (1-100, 80 default): ", accepted_type = int, min_value = 1, max_value = 100)
self.settings["png_compression"] = self.take_input_and_validate(question = "PNG compression level (0-9, 6 default): ", accepted_type = int, min_value = 0, max_value = 9)
self.settings["web_optimize"] = self.tui.yes_no_menu("Optimize images i.e. compressing?")
self.write_settings(self.settings_to_save)
def write_settings(self, keys_to_save):
""""Write self.setting, but only specific values"""
keys = keys_to_save
filtered_settings = {key: self.settings[key] for key in keys if key in self.settings}
self.utilities.write_yaml(self.setting_file, filtered_settings)
print("New settings saved successfully.")
def read_settings(self, keys_to_load):
"""
Read settings from the settings file and update self.settings
with the values for specific keys without overwriting existing values.
"""
# First draft by ChatGPT, adjusted to fit my needs.
keys = keys_to_load
if os.path.exists(self.setting_file):
loaded_settings = self.utilities.read_yaml(self.setting_file)
for key in keys:
if key in loaded_settings:
self.settings[key] = loaded_settings[key]
print("Settings loaded successfully.")
return True
else:
print("Settings file empty.")
return False
def collect_exif_data(self):
"""Collect EXIF data based on user input."""
user_data = {}
fields = [
"make", "model", "lens", "iso", "image_description",
"user_comment", "artist", "copyright_info"
]
for field in fields:
choise = self.tui.choose_menu(f"Enter {field.replace('_', ' ').title()}", self.exif_choices[field])
user_data[field] = choise.encode("utf-8")
user_data["software"] = f"OPTIMA-35 {self.version}".encode("utf-8")
new_date = self.get_date_input()
if new_date:
user_data["date_time_original"] = new_date
return user_data
def get_date_input(self):
# Partially chatGPT
while True:
date_input = input("Enter a date (yyyy-mm-dd): ")
if date_input == "":
return None # Skip if input is empty
def modify_timestamp_in_exif(self, exif_data, filename):
""""Takes exif data and adjust time to fit ending of filename."""
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
last_tree = filename[-3:len(filename)]
total_seconds = int(re.sub(r'\D+', '', last_tree))
minutes = total_seconds // 60
seconds = total_seconds % 60
time = datetime.strptime(exif_data["date_time_original"], "%Y:%m:%d %H:%M:%S") # change date time string back to an time object for modification
new_time = time.replace(hour=12, minute=minutes, second=seconds)
exif_data["date_time_original"] = new_time.strftime("%Y:%m:%d %H:%M:%S")
return exif_data
except ValueError:
print(f"Invalid input. Must be of type {accepted_type.__name__}.")
print("Modifying date went wrong, exiting...")
exit()
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.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
def process_images(self):
"""Process images based on user settings."""
input_folder = self.settings["input_folder"]
output_folder = self.settings["output_folder"]
image_files = [
f for f in os.listdir(input_folder) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp"))
]
if "Change EXIF" in self.settings["modifications"]:
selected_exif = self.collect_exif_data()
i = 1
for image_file in image_files:
input_path = os.path.join(input_folder, image_file)
if "Rename images" in self.settings["modifications"]:
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"])
else:
image_name = os.path.splitext(image_file)[0]
@ -210,25 +288,31 @@ class Optima35:
with self.image_processor.open_image(input_path) as img:
processed_img = img
for mod in self.settings["modifications"]:
if mod == "Resize image":
processed_img = self.image_processor.resize_image(
image = processed_img, percent = self.settings["resize_percentage"], resample = True
)
elif mod == "Change EXIF" and selected_exif:
if "date_time_original" in selected_exif:
selected_exif = self.modify_timestamp_in_exif(selected_exif, image_name)
exif_data = self.exif_handler.build_exif_dict(selected_exif, self.image_processor.get_image_size(processed_img))
elif mod == "Convert to grayscale":
processed_img = self.image_processor.grayscale(processed_img)
elif mod == "Change contrast":
processed_img = self.image_processor.change_contrast(processed_img, self.settings["contrast_percentage"])
elif mod == "Change brightness":
processed_img = self.image_processor.change_brightness(processed_img, self.settings["brightness_percentage"])
elif mod == "Add Watermark":
processed_img = self.image_processor.add_watermark(processed_img, self.settings["watermark_text"])
if self.settings["copy_exif"]:
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"], int(self.settings["font_size"]))
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["own_exif"] != False:
selected_exif = self.selected_exif
if "date_time_original" in self.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))
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)
elif self.settings["copy_exif"] == True:
# When copying exif from original, make sure to change Piexel X & Y Dimension to fit new size
try:
og_exif = self.exif_handler.get_exif_info(img)
@ -236,10 +320,11 @@ class Optima35:
exif_data = og_exif
except Exception:
# If an error happends it is because the picture does not have exif data
print("Copying EXIF data selected, but no EXIF data is available in the original image file.")
self.change_statusbar("Copying EXIF data selected, but no EXIF data is available in the original image file.")
exif_data = None
elif "Change EXIF" not in self.settings["modifications"]:
elif self.settings["copy_exif"] == False:
exif_data = None
self.change_statusbar(f"exif data: {exif_data}")
self.image_processor.save_image(
image = processed_img,
@ -248,45 +333,29 @@ class Optima35:
file_type = self.settings["file_format"],
jpg_quality = self.settings["jpg_quality"],
png_compressing = self.settings["png_compression"],
optimize = self.settings["web_optimize"]
optimize = self.settings["optimize"]
)
self.utilities.progress_bar(i, len(image_files))
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.progressBar.setValue(0)
def name_images(self, base_name, current_image, total_images, invert):
""""Returns name, combination of base_name and ending number."""
total_digits = len(str(total_images))
if invert:
ending_number = total_images - (current_image - 1)
else:
ending_number = current_image
ending = f"{ending_number:0{total_digits}}"
return f"{base_name}_{ending}"
def modify_timestamp_in_exif(self, exif_data, filename):
""""Takes exif data and adjust time to fit ending of filename."""
try:
last_tree = filename[-3:len(filename)]
total_seconds = int(re.sub(r'\D+', '', last_tree))
minutes = total_seconds // 60
seconds = total_seconds % 60
time = datetime.strptime(exif_data["date_time_original"], "%Y:%m:%d %H:%M:%S") # change date time string back to an time object for modification
new_time = time.replace(hour=12, minute=minutes, second=seconds)
exif_data["date_time_original"] = new_time.strftime("%Y:%m:%d %H:%M:%S")
return exif_data
except ValueError:
print("Modifying date went wrong, exiting...")
exit()
def run(self):
"""Run the main program."""
self.load_or_ask_settings()
self.get_user_settings()
self.process_images()
print("Done")
""""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__":
app = Optima35("config/settings.yaml", "config/exif_options.yaml")
app.run()
app = QtWidgets.QApplication(sys.argv)
window = Optima35QT6(exif_file = "local_files/exif.yaml", version = "0.3.4")
window.show()
app.exec()

292
main_tui.py Normal file
View file

@ -0,0 +1,292 @@
import os
import re
from datetime import datetime
from utils.utility import Utilities
from utils.image_handler import ImageProcessor, ExifHandler
from ui.tui import SimpleTUI
# legacy code, will be removed with then next minor version
class Optima35:
# The layout of class Optima35 was originally made by ChatGPT, but major adjustments have been made. To remain transparent, I disclose this.
def __init__(self, settings_file, exif_options_file):
self.name = "OPTIMA-35"
self.version = "0.2.1"
self.utilities = Utilities()
self.image_processor = ImageProcessor()
self.exif_handler = ExifHandler()
self.tui = SimpleTUI()
self.settings = {
"input_folder": None,
"output_folder": None,
"file_format": None,
"resize_percentage": None,
"copy_exif": None,
"contrast_percentage": None,
"brightness_percentage": None,
"new_file_names": None,
"invert_image_order": False,
"watermark_text": None,
"modifications": [],
}
self.settings_to_save = [
"resize_percentage",
"jpg_quality",
"png_compression",
"web_optimize",
"contrast_percentage",
"brightness_percentage"
]
self.exif_choices = self.utilities.read_yaml(exif_options_file)
self.setting_file = settings_file
def load_or_ask_settings(self):
"""Load settings from a YAML file or ask the user if not present or incomplete."""
# Partially ChatGPT
if self.read_settings(self.settings_to_save):
for item in self.settings_to_save:
print(f"{item}: {self.settings[item]}")
use_saved = self.tui.yes_no_menu("Use these settings?")
if use_saved:
return
else:
print("No settings found...")
print("Asking for new settings...\n")
self.settings["resize_percentage"] = self.take_input_and_validate(question = "Default resize percentage (below 100 downscale, above upscale): ", accepted_type = int, min_value = 1, max_value = 200)
self.settings["contrast_percentage"] = self.take_input_and_validate(question = "Default contrast percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100)
self.settings["brightness_percentage"] = self.take_input_and_validate(question = "Default brighness percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100)
self.settings["jpg_quality"] = self.take_input_and_validate(question = "JPEG quality (1-100, 80 default): ", accepted_type = int, min_value = 1, max_value = 100)
self.settings["png_compression"] = self.take_input_and_validate(question = "PNG compression level (0-9, 6 default): ", accepted_type = int, min_value = 0, max_value = 9)
self.settings["web_optimize"] = self.tui.yes_no_menu("Optimize images i.e. compressing?")
self.write_settings(self.settings_to_save)
def write_settings(self, keys_to_save):
""""Write self.setting, but only specific values"""
keys = keys_to_save
filtered_settings = {key: self.settings[key] for key in keys if key in self.settings}
self.utilities.write_yaml(self.setting_file, filtered_settings)
print("New settings saved successfully.")
def read_settings(self, keys_to_load):
"""
Read settings from the settings file and update self.settings
with the values for specific keys without overwriting existing values.
"""
# First draft by ChatGPT, adjusted to fit my needs.
keys = keys_to_load
if os.path.exists(self.setting_file):
loaded_settings = self.utilities.read_yaml(self.setting_file)
for key in keys:
if key in loaded_settings:
self.settings[key] = loaded_settings[key]
print("Settings loaded successfully.")
return True
else:
print("Settings file empty.")
return False
def collect_exif_data(self):
"""Collect EXIF data based on user input."""
user_data = {}
fields = [
"make", "model", "lens", "iso", "image_description",
"user_comment", "artist", "copyright_info"
]
for field in fields:
choise = self.tui.choose_menu(f"Enter {field.replace('_', ' ').title()}", self.exif_choices[field])
user_data[field] = choise.encode("utf-8")
user_data["software"] = f"OPTIMA-35 {self.version}".encode("utf-8")
new_date = self.get_date_input()
if new_date:
user_data["date_time_original"] = new_date
return user_data
def get_date_input(self):
# Partially chatGPT
while True:
date_input = input("Enter a date (yyyy-mm-dd): ")
if date_input == "":
return None # Skip if input is empty
try:
new_date = datetime.strptime(date_input, "%Y-%m-%d")
return new_date.strftime("%Y:%m:%d 00:00:00")
except ValueError:
print("Invalid date format. Please enter the date in yyyy-mm-dd format.")
def get_user_settings(self):
"""Get initial settings from the user."""
menu_options = [
"Resize image",
"Change EXIF",
"Convert to grayscale",
"Change contrast",
"Change brightness",
"Rename images",
"Invert image order",
"Add Watermark"
] # new option can be added here.
self.settings["input_folder"] = input("Enter path of input folder: ").strip() # Add: check if folder exists.
self.settings["output_folder"] = input("Enter path of output folder: ").strip()
self.settings["file_format"] = self.take_input_and_validate(question = "Enter export file format (jpg, png, webp): ", accepted_input = ["jpg", "png", "webp"], accepted_type = str)
self.settings["modifications"] = self.tui.multi_select_menu(
f"\n{self.name} v.{self.version} \nSelect what you want to do (esc or q to exit)",
menu_options
)
if "Change EXIF" not in self.settings["modifications"]:
self.settings["copy_exif"] = self.tui.yes_no_menu("Do you want to copy exif info from original file?")
if "Rename images" in self.settings["modifications"]:
self.settings["new_file_names"] = input("What should be the name for the new images? ") # Need
if "Invert image order" in self.settings["modifications"]:
self.settings["invert_image_order"] = True
if "Add Watermark" in self.settings["modifications"]:
self.settings["watermark_text"] = input("Enter text for watermark. ")
os.makedirs(self.settings["output_folder"], exist_ok = True)
def take_input_and_validate(self, question, accepted_input = None, accepted_type = str, min_value = None, max_value = None):
"""
Asks the user a question, validates the input, and ensures it matches the specified criteria.
Args:
question (str): The question to ask the user.
accepted_input (list): A list of acceptable inputs (optional for non-numeric types).
accepted_type (type): The expected type of input (e.g., str, int, float).
min_value (int/float): Minimum value for numeric inputs (optional).
max_value (int/float): Maximum value for numeric inputs (optional).
Returns:
The validated user input.
"""
# Main layout by chatGPT, but modified.
while True:
user_input = input(question).strip()
try:
# Convert input to the desired type
if accepted_type in [int, float]:
user_input = accepted_type(user_input)
# Validate range for numeric types
if (min_value is not None and user_input < min_value) or (max_value is not None and user_input > max_value):
print(f"Input must be between {min_value} and {max_value}.")
continue
elif accepted_type == str:
# No conversion needed for strings
user_input = str(user_input)
else:
raise ValueError(f"Unsupported type: {accepted_type}")
# Validate against accepted inputs if provided
if accepted_input is not None and user_input not in accepted_input:
print(f"Invalid input. Must be one of: {', '.join(map(str, accepted_input))}.")
continue
return user_input # Input is valid
except ValueError:
print(f"Invalid input. Must be of type {accepted_type.__name__}.")
def process_images(self):
"""Process images based on user settings."""
input_folder = self.settings["input_folder"]
output_folder = self.settings["output_folder"]
image_files = [
f for f in os.listdir(input_folder) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp"))
]
if "Change EXIF" in self.settings["modifications"]:
selected_exif = self.collect_exif_data()
i = 1
for image_file in image_files:
input_path = os.path.join(input_folder, image_file)
if "Rename images" in self.settings["modifications"]:
image_name = self.name_images(self.settings["new_file_names"], i, len(image_files), self.settings["invert_image_order"])
else:
image_name = os.path.splitext(image_file)[0]
output_path = os.path.join(output_folder, image_name)
with self.image_processor.open_image(input_path) as img:
processed_img = img
for mod in self.settings["modifications"]:
if mod == "Resize image":
processed_img = self.image_processor.resize_image(
image = processed_img, percent = self.settings["resize_percentage"], resample = True
)
elif mod == "Change EXIF" and selected_exif:
if "date_time_original" in selected_exif:
selected_exif = self.modify_timestamp_in_exif(selected_exif, image_name)
exif_data = self.exif_handler.build_exif_dict(selected_exif, self.image_processor.get_image_size(processed_img))
elif mod == "Convert to grayscale":
processed_img = self.image_processor.grayscale(processed_img)
elif mod == "Change contrast":
processed_img = self.image_processor.change_contrast(processed_img, self.settings["contrast_percentage"])
elif mod == "Change brightness":
processed_img = self.image_processor.change_brightness(processed_img, self.settings["brightness_percentage"])
elif mod == "Add Watermark":
processed_img = self.image_processor.add_watermark(processed_img, self.settings["watermark_text"])
if self.settings["copy_exif"]:
# When copying exif from original, make sure to change Piexel X & Y Dimension to fit new size
try:
og_exif = self.exif_handler.get_exif_info(img)
og_exif["Exif"][40962], og_exif["Exif"][40963] = self.image_processor.get_image_size(processed_img)
exif_data = og_exif
except Exception:
# If an error happends it is because the picture does not have exif data
print("Copying EXIF data selected, but no EXIF data is available in the original image file.")
exif_data = None
elif "Change EXIF" not in self.settings["modifications"]:
exif_data = None
self.image_processor.save_image(
image = processed_img,
path = output_path,
exif_data = exif_data,
file_type = self.settings["file_format"],
jpg_quality = self.settings["jpg_quality"],
png_compressing = self.settings["png_compression"],
optimize = self.settings["web_optimize"]
)
self.utilities.progress_bar(i, len(image_files))
i += 1
def name_images(self, base_name, current_image, total_images, invert):
""""Returns name, combination of base_name and ending number."""
total_digits = len(str(total_images))
if invert:
ending_number = total_images - (current_image - 1)
else:
ending_number = current_image
ending = f"{ending_number:0{total_digits}}"
return f"{base_name}_{ending}"
def modify_timestamp_in_exif(self, exif_data, filename):
""""Takes exif data and adjust time to fit ending of filename."""
try:
last_tree = filename[-3:len(filename)]
total_seconds = int(re.sub(r'\D+', '', last_tree))
minutes = total_seconds // 60
seconds = total_seconds % 60
time = datetime.strptime(exif_data["date_time_original"], "%Y:%m:%d %H:%M:%S") # change date time string back to an time object for modification
new_time = time.replace(hour=12, minute=minutes, second=seconds)
exif_data["date_time_original"] = new_time.strftime("%Y:%m:%d %H:%M:%S")
return exif_data
except ValueError:
print("Modifying date went wrong, exiting...")
exit()
def run(self):
"""Run the main program."""
self.load_or_ask_settings()
self.get_user_settings()
self.process_images()
print("Done")
if __name__ == "__main__":
app = Optima35("config/settings.yaml", "config/exif_options.yaml")
app.run()

BIN
media/exif_tab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
media/exifeditor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
media/main_tab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

87
ui/exif_handler_window.py Normal file
View file

@ -0,0 +1,87 @@
from PySide6.QtCore import Signal
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QComboBox, QListWidget,
QLineEdit, QHBoxLayout, QPushButton, QMessageBox
)
# By ChatGPT
class ExifEditor(QMainWindow):
# Signal to emit the updated EXIF data
exif_data_updated = Signal(dict)
def __init__(self, exif_data):
super().__init__()
self.exif_data = exif_data
self.current_key = None
self.setWindowTitle("EXIF Editor")
self.resize(400, 300)
# Main widget and layout
main_widget = QWidget()
main_layout = QVBoxLayout()
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
# ComboBox to select lists
self.combo_box = QComboBox()
self.combo_box.addItems(self.exif_data.keys())
self.combo_box.currentTextChanged.connect(self.load_list)
main_layout.addWidget(self.combo_box)
# List widget to display items
self.list_widget = QListWidget()
main_layout.addWidget(self.list_widget)
# Line edit for adding items
self.line_edit = QLineEdit()
self.line_edit.setPlaceholderText("Enter new item...")
main_layout.addWidget(self.line_edit)
# Buttons: Add, Delete, Cancel
button_layout = QHBoxLayout()
self.add_button = QPushButton("Add")
self.add_button.clicked.connect(self.add_item)
self.delete_button = QPushButton("Delete")
self.delete_button.clicked.connect(self.delete_item)
self.cancel_button = QPushButton("Close")
self.cancel_button.clicked.connect(self.close_editor)
button_layout.addWidget(self.add_button)
button_layout.addWidget(self.delete_button)
button_layout.addWidget(self.cancel_button)
main_layout.addLayout(button_layout)
# Load the first list by default
self.load_list(self.combo_box.currentText())
def load_list(self, key):
"""Load the selected list into the list widget."""
self.current_key = key
self.list_widget.clear()
if key in self.exif_data:
self.list_widget.addItems(self.exif_data[key])
def add_item(self):
"""Add a new item to the selected list."""
new_item = self.line_edit.text().strip()
if new_item:
self.exif_data[self.current_key].append(new_item)
self.list_widget.addItem(new_item)
self.line_edit.clear()
else:
QMessageBox.warning(self, "Warning", "Cannot add an empty item.")
def delete_item(self):
"""Delete the selected item from the list."""
selected_item = self.list_widget.currentItem()
if selected_item:
item_text = selected_item.text()
self.exif_data[self.current_key].remove(item_text)
self.list_widget.takeItem(self.list_widget.row(selected_item))
else:
QMessageBox.warning(self, "Warning", "No item selected to delete.")
def close_editor(self):
"""Emit the updated exif_data and close the editor."""
self.exif_data_updated.emit(self.exif_data)
self.close()

550
ui/main_window.py Normal file
View file

@ -0,0 +1,550 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'main_window.ui'
##
## Created by: Qt User Interface Compiler version 6.8.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QDateEdit,
QFrame, QGridLayout, QGroupBox, QHBoxLayout,
QLabel, QLineEdit, QMainWindow, QProgressBar,
QPushButton, QSizePolicy, QSpinBox, QStatusBar,
QTabWidget, QVBoxLayout, QWidget)
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(450, 708)
MainWindow.setMinimumSize(QSize(350, 677))
MainWindow.setMaximumSize(QSize(500, 1000))
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName(u"centralwidget")
self.gridLayout = QGridLayout(self.centralwidget)
self.gridLayout.setObjectName(u"gridLayout")
self.tabWidget = QTabWidget(self.centralwidget)
self.tabWidget.setObjectName(u"tabWidget")
self.tabWidget.setMaximumSize(QSize(500, 16777215))
self.tab_1 = QWidget()
self.tab_1.setObjectName(u"tab_1")
self.verticalLayout_10 = QVBoxLayout(self.tab_1)
self.verticalLayout_10.setObjectName(u"verticalLayout_10")
self.folder_group = QFrame(self.tab_1)
self.folder_group.setObjectName(u"folder_group")
self.folder_group.setMaximumSize(QSize(400, 16777215))
self.gridLayout_5 = QGridLayout(self.folder_group)
self.gridLayout_5.setObjectName(u"gridLayout_5")
self.input_path = QLineEdit(self.folder_group)
self.input_path.setObjectName(u"input_path")
self.gridLayout_5.addWidget(self.input_path, 0, 0, 1, 1)
self.output_path = QLineEdit(self.folder_group)
self.output_path.setObjectName(u"output_path")
self.gridLayout_5.addWidget(self.output_path, 0, 1, 1, 1)
self.input_folder_button = QPushButton(self.folder_group)
self.input_folder_button.setObjectName(u"input_folder_button")
self.gridLayout_5.addWidget(self.input_folder_button, 1, 0, 1, 1)
self.output_folder_button = QPushButton(self.folder_group)
self.output_folder_button.setObjectName(u"output_folder_button")
self.gridLayout_5.addWidget(self.output_folder_button, 1, 1, 1, 1)
self.verticalLayout_10.addWidget(self.folder_group)
self.groupBox = QGroupBox(self.tab_1)
self.groupBox.setObjectName(u"groupBox")
self.groupBox.setMaximumSize(QSize(400, 16777215))
self.gridLayout_4 = QGridLayout(self.groupBox)
self.gridLayout_4.setObjectName(u"gridLayout_4")
self.resize_checkbox = QCheckBox(self.groupBox)
self.resize_checkbox.setObjectName(u"resize_checkbox")
self.gridLayout_4.addWidget(self.resize_checkbox, 0, 0, 1, 1)
self.resize_spinBox = QSpinBox(self.groupBox)
self.resize_spinBox.setObjectName(u"resize_spinBox")
self.resize_spinBox.setEnabled(False)
self.resize_spinBox.setMinimum(1)
self.resize_spinBox.setMaximum(200)
self.resize_spinBox.setSingleStep(1)
self.resize_spinBox.setValue(80)
self.gridLayout_4.addWidget(self.resize_spinBox, 0, 1, 1, 1)
self.image_type = QComboBox(self.groupBox)
self.image_type.addItem(u"jpg")
self.image_type.addItem(u"png")
self.image_type.addItem(u"webp")
self.image_type.setObjectName(u"image_type")
self.gridLayout_4.addWidget(self.image_type, 1, 0, 1, 1)
self.jpg_quality_spinBox = QSpinBox(self.groupBox)
self.jpg_quality_spinBox.setObjectName(u"jpg_quality_spinBox")
self.jpg_quality_spinBox.setMinimum(1)
self.jpg_quality_spinBox.setMaximum(100)
self.jpg_quality_spinBox.setValue(80)
self.gridLayout_4.addWidget(self.jpg_quality_spinBox, 1, 1, 1, 1)
self.png_quality_spinBox = QSpinBox(self.groupBox)
self.png_quality_spinBox.setObjectName(u"png_quality_spinBox")
self.png_quality_spinBox.setEnabled(True)
self.png_quality_spinBox.setMinimum(1)
self.png_quality_spinBox.setMaximum(9)
self.png_quality_spinBox.setValue(6)
self.gridLayout_4.addWidget(self.png_quality_spinBox, 1, 2, 1, 1)
self.optimize_checkBox = QCheckBox(self.groupBox)
self.optimize_checkBox.setObjectName(u"optimize_checkBox")
self.gridLayout_4.addWidget(self.optimize_checkBox, 0, 2, 1, 1)
self.png_quality_spinBox.raise_()
self.resize_checkbox.raise_()
self.resize_spinBox.raise_()
self.image_type.raise_()
self.jpg_quality_spinBox.raise_()
self.optimize_checkBox.raise_()
self.verticalLayout_10.addWidget(self.groupBox)
self.groupBox_2 = QGroupBox(self.tab_1)
self.groupBox_2.setObjectName(u"groupBox_2")
self.groupBox_2.setMaximumSize(QSize(400, 16777215))
self.gridLayout_3 = QGridLayout(self.groupBox_2)
self.gridLayout_3.setObjectName(u"gridLayout_3")
self.watermark_lineEdit = QLineEdit(self.groupBox_2)
self.watermark_lineEdit.setObjectName(u"watermark_lineEdit")
self.watermark_lineEdit.setEnabled(False)
self.gridLayout_3.addWidget(self.watermark_lineEdit, 3, 0, 1, 3)
self.brightness_checkbox = QCheckBox(self.groupBox_2)
self.brightness_checkbox.setObjectName(u"brightness_checkbox")
self.gridLayout_3.addWidget(self.brightness_checkbox, 0, 0, 1, 1)
self.grayscale_checkBox = QCheckBox(self.groupBox_2)
self.grayscale_checkBox.setObjectName(u"grayscale_checkBox")
self.gridLayout_3.addWidget(self.grayscale_checkBox, 0, 2, 1, 1)
self.contrast_spinBox = QSpinBox(self.groupBox_2)
self.contrast_spinBox.setObjectName(u"contrast_spinBox")
self.contrast_spinBox.setEnabled(False)
self.contrast_spinBox.setMinimum(-100)
self.contrast_spinBox.setMaximum(100)
self.contrast_spinBox.setValue(10)
self.gridLayout_3.addWidget(self.contrast_spinBox, 1, 1, 1, 1)
self.watermark_checkbox = QCheckBox(self.groupBox_2)
self.watermark_checkbox.setObjectName(u"watermark_checkbox")
self.gridLayout_3.addWidget(self.watermark_checkbox, 2, 0, 1, 1)
self.brightness_spinBox = QSpinBox(self.groupBox_2)
self.brightness_spinBox.setObjectName(u"brightness_spinBox")
self.brightness_spinBox.setEnabled(False)
self.brightness_spinBox.setMinimum(-100)
self.brightness_spinBox.setMaximum(100)
self.brightness_spinBox.setValue(-10)
self.gridLayout_3.addWidget(self.brightness_spinBox, 0, 1, 1, 1)
self.contrast_checkbox = QCheckBox(self.groupBox_2)
self.contrast_checkbox.setObjectName(u"contrast_checkbox")
self.gridLayout_3.addWidget(self.contrast_checkbox, 1, 0, 1, 1)
self.font_size_comboBox = QComboBox(self.groupBox_2)
self.font_size_comboBox.addItem("")
self.font_size_comboBox.addItem("")
self.font_size_comboBox.addItem("")
self.font_size_comboBox.addItem("")
self.font_size_comboBox.addItem("")
self.font_size_comboBox.setObjectName(u"font_size_comboBox")
self.gridLayout_3.addWidget(self.font_size_comboBox, 2, 1, 1, 1)
self.verticalLayout_10.addWidget(self.groupBox_2)
self.rename_group = QGroupBox(self.tab_1)
self.rename_group.setObjectName(u"rename_group")
self.rename_group.setMaximumSize(QSize(400, 16777215))
self.gridLayout_6 = QGridLayout(self.rename_group)
self.gridLayout_6.setObjectName(u"gridLayout_6")
self.rename_checkbox = QCheckBox(self.rename_group)
self.rename_checkbox.setObjectName(u"rename_checkbox")
self.gridLayout_6.addWidget(self.rename_checkbox, 0, 0, 1, 1)
self.revert_checkbox = QCheckBox(self.rename_group)
self.revert_checkbox.setObjectName(u"revert_checkbox")
self.gridLayout_6.addWidget(self.revert_checkbox, 0, 1, 1, 1)
self.filename = QLineEdit(self.rename_group)
self.filename.setObjectName(u"filename")
self.filename.setEnabled(False)
self.gridLayout_6.addWidget(self.filename, 1, 0, 1, 2)
self.verticalLayout_10.addWidget(self.rename_group)
self.widget_9 = QWidget(self.tab_1)
self.widget_9.setObjectName(u"widget_9")
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.horizontalLayout_3.addWidget(self.restart_button)
self.progressBar = QProgressBar(self.widget_9)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setEnabled(True)
self.progressBar.setValue(0)
self.horizontalLayout_3.addWidget(self.progressBar)
self.start_button = QPushButton(self.widget_9)
self.start_button.setObjectName(u"start_button")
self.start_button.setEnabled(True)
self.horizontalLayout_3.addWidget(self.start_button)
self.verticalLayout_10.addWidget(self.widget_9)
self.tabWidget.addTab(self.tab_1, "")
self.tab_2 = QWidget()
self.tab_2.setObjectName(u"tab_2")
self.verticalLayout_9 = QVBoxLayout(self.tab_2)
self.verticalLayout_9.setObjectName(u"verticalLayout_9")
self.exif_group = QGroupBox(self.tab_2)
self.exif_group.setObjectName(u"exif_group")
self.horizontalLayout = QHBoxLayout(self.exif_group)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.exif_checkbox = QCheckBox(self.exif_group)
self.exif_checkbox.setObjectName(u"exif_checkbox")
self.exif_checkbox.setEnabled(True)
self.horizontalLayout.addWidget(self.exif_checkbox)
self.exif_copy_checkBox = QCheckBox(self.exif_group)
self.exif_copy_checkBox.setObjectName(u"exif_copy_checkBox")
self.horizontalLayout.addWidget(self.exif_copy_checkBox)
self.edit_exif_button = QPushButton(self.exif_group)
self.edit_exif_button.setObjectName(u"edit_exif_button")
self.edit_exif_button.setEnabled(False)
self.horizontalLayout.addWidget(self.edit_exif_button)
self.verticalLayout_9.addWidget(self.exif_group)
self.exif_options_group = QGroupBox(self.tab_2)
self.exif_options_group.setObjectName(u"exif_options_group")
self.exif_options_group.setEnabled(False)
self.gridLayout_7 = QGridLayout(self.exif_options_group)
self.gridLayout_7.setObjectName(u"gridLayout_7")
self.widget_7 = QWidget(self.exif_options_group)
self.widget_7.setObjectName(u"widget_7")
self.verticalLayout_7 = QVBoxLayout(self.widget_7)
self.verticalLayout_7.setObjectName(u"verticalLayout_7")
self.label_7 = QLabel(self.widget_7)
self.label_7.setObjectName(u"label_7")
self.verticalLayout_7.addWidget(self.label_7)
self.artist_comboBox = QComboBox(self.widget_7)
self.artist_comboBox.setObjectName(u"artist_comboBox")
self.verticalLayout_7.addWidget(self.artist_comboBox)
self.gridLayout_7.addWidget(self.widget_7, 3, 0, 1, 1)
self.widget_4 = QWidget(self.exif_options_group)
self.widget_4.setObjectName(u"widget_4")
self.verticalLayout_4 = QVBoxLayout(self.widget_4)
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
self.label_4 = QLabel(self.widget_4)
self.label_4.setObjectName(u"label_4")
self.verticalLayout_4.addWidget(self.label_4)
self.iso_comboBox = QComboBox(self.widget_4)
self.iso_comboBox.setObjectName(u"iso_comboBox")
self.verticalLayout_4.addWidget(self.iso_comboBox)
self.gridLayout_7.addWidget(self.widget_4, 1, 1, 1, 1)
self.widget_6 = QWidget(self.exif_options_group)
self.widget_6.setObjectName(u"widget_6")
self.verticalLayout_6 = QVBoxLayout(self.widget_6)
self.verticalLayout_6.setObjectName(u"verticalLayout_6")
self.label_6 = QLabel(self.widget_6)
self.label_6.setObjectName(u"label_6")
self.verticalLayout_6.addWidget(self.label_6)
self.user_comment_comboBox = QComboBox(self.widget_6)
self.user_comment_comboBox.setObjectName(u"user_comment_comboBox")
self.verticalLayout_6.addWidget(self.user_comment_comboBox)
self.gridLayout_7.addWidget(self.widget_6, 2, 1, 1, 1)
self.widget_2 = QWidget(self.exif_options_group)
self.widget_2.setObjectName(u"widget_2")
self.verticalLayout_2 = QVBoxLayout(self.widget_2)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.label_2 = QLabel(self.widget_2)
self.label_2.setObjectName(u"label_2")
self.verticalLayout_2.addWidget(self.label_2)
self.lens_comboBox = QComboBox(self.widget_2)
self.lens_comboBox.setObjectName(u"lens_comboBox")
self.verticalLayout_2.addWidget(self.lens_comboBox)
self.gridLayout_7.addWidget(self.widget_2, 1, 0, 1, 1)
self.widget_5 = QWidget(self.exif_options_group)
self.widget_5.setObjectName(u"widget_5")
self.verticalLayout_5 = QVBoxLayout(self.widget_5)
self.verticalLayout_5.setObjectName(u"verticalLayout_5")
self.label_5 = QLabel(self.widget_5)
self.label_5.setObjectName(u"label_5")
self.verticalLayout_5.addWidget(self.label_5)
self.image_description_comboBox = QComboBox(self.widget_5)
self.image_description_comboBox.setObjectName(u"image_description_comboBox")
self.verticalLayout_5.addWidget(self.image_description_comboBox)
self.gridLayout_7.addWidget(self.widget_5, 2, 0, 1, 1)
self.widget = QWidget(self.exif_options_group)
self.widget.setObjectName(u"widget")
self.verticalLayout = QVBoxLayout(self.widget)
self.verticalLayout.setObjectName(u"verticalLayout")
self.label = QLabel(self.widget)
self.label.setObjectName(u"label")
self.verticalLayout.addWidget(self.label)
self.make_comboBox = QComboBox(self.widget)
self.make_comboBox.setObjectName(u"make_comboBox")
self.verticalLayout.addWidget(self.make_comboBox)
self.gridLayout_7.addWidget(self.widget, 0, 0, 1, 1)
self.widget_3 = QWidget(self.exif_options_group)
self.widget_3.setObjectName(u"widget_3")
self.verticalLayout_3 = QVBoxLayout(self.widget_3)
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.label_3 = QLabel(self.widget_3)
self.label_3.setObjectName(u"label_3")
self.verticalLayout_3.addWidget(self.label_3)
self.model_comboBox = QComboBox(self.widget_3)
self.model_comboBox.setObjectName(u"model_comboBox")
self.verticalLayout_3.addWidget(self.model_comboBox)
self.gridLayout_7.addWidget(self.widget_3, 0, 1, 1, 1)
self.widget_8 = QWidget(self.exif_options_group)
self.widget_8.setObjectName(u"widget_8")
self.verticalLayout_8 = QVBoxLayout(self.widget_8)
self.verticalLayout_8.setObjectName(u"verticalLayout_8")
self.label_8 = QLabel(self.widget_8)
self.label_8.setObjectName(u"label_8")
self.verticalLayout_8.addWidget(self.label_8)
self.copyright_info_comboBox = QComboBox(self.widget_8)
self.copyright_info_comboBox.setObjectName(u"copyright_info_comboBox")
self.verticalLayout_8.addWidget(self.copyright_info_comboBox)
self.gridLayout_7.addWidget(self.widget_8, 3, 1, 1, 1)
self.verticalLayout_9.addWidget(self.exif_options_group)
self.gps_groupBox = QGroupBox(self.tab_2)
self.gps_groupBox.setObjectName(u"gps_groupBox")
self.gps_groupBox.setEnabled(False)
self.horizontalLayout_4 = QHBoxLayout(self.gps_groupBox)
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
self.gps_checkBox = QCheckBox(self.gps_groupBox)
self.gps_checkBox.setObjectName(u"gps_checkBox")
self.horizontalLayout_4.addWidget(self.gps_checkBox)
self.lat_lineEdit = QLineEdit(self.gps_groupBox)
self.lat_lineEdit.setObjectName(u"lat_lineEdit")
self.lat_lineEdit.setEnabled(False)
self.horizontalLayout_4.addWidget(self.lat_lineEdit)
self.long_lineEdit = QLineEdit(self.gps_groupBox)
self.long_lineEdit.setObjectName(u"long_lineEdit")
self.long_lineEdit.setEnabled(False)
self.horizontalLayout_4.addWidget(self.long_lineEdit)
self.verticalLayout_9.addWidget(self.gps_groupBox)
self.date_groupBox = QGroupBox(self.tab_2)
self.date_groupBox.setObjectName(u"date_groupBox")
self.date_groupBox.setEnabled(False)
self.horizontalLayout_2 = QHBoxLayout(self.date_groupBox)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.add_date_checkBox = QCheckBox(self.date_groupBox)
self.add_date_checkBox.setObjectName(u"add_date_checkBox")
self.horizontalLayout_2.addWidget(self.add_date_checkBox)
self.dateEdit = QDateEdit(self.date_groupBox)
self.dateEdit.setObjectName(u"dateEdit")
self.dateEdit.setEnabled(False)
self.horizontalLayout_2.addWidget(self.dateEdit)
self.verticalLayout_9.addWidget(self.date_groupBox)
self.tabWidget.addTab(self.tab_2, "")
self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.statusBar = QStatusBar(MainWindow)
self.statusBar.setObjectName(u"statusBar")
MainWindow.setStatusBar(self.statusBar)
self.retranslateUi(MainWindow)
self.resize_checkbox.toggled.connect(self.resize_spinBox.setEnabled)
self.brightness_checkbox.toggled.connect(self.brightness_spinBox.setEnabled)
self.contrast_checkbox.toggled.connect(self.contrast_spinBox.setEnabled)
self.watermark_checkbox.toggled.connect(self.watermark_lineEdit.setEnabled)
self.rename_checkbox.toggled.connect(self.filename.setEnabled)
self.exif_checkbox.toggled.connect(self.exif_options_group.setEnabled)
self.exif_checkbox.toggled.connect(self.exif_copy_checkBox.setDisabled)
self.exif_copy_checkBox.toggled.connect(self.exif_checkbox.setDisabled)
self.exif_checkbox.toggled.connect(self.edit_exif_button.setEnabled)
self.add_date_checkBox.toggled.connect(self.dateEdit.setEnabled)
self.exif_checkbox.toggled.connect(self.date_groupBox.setEnabled)
self.exif_checkbox.toggled.connect(self.gps_groupBox.setEnabled)
self.gps_checkBox.toggled.connect(self.lat_lineEdit.setEnabled)
self.gps_checkBox.toggled.connect(self.long_lineEdit.setEnabled)
self.tabWidget.setCurrentIndex(0)
self.font_size_comboBox.setCurrentIndex(2)
QMetaObject.connectSlotsByName(MainWindow)
# setupUi
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"OPTIMA-35", None))
self.input_path.setText(QCoreApplication.translate("MainWindow", u"local_files/img", None))
self.input_path.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Enter input folder", None))
self.output_path.setText(QCoreApplication.translate("MainWindow", u"local_files/out", None))
self.output_path.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Enter output folder", None))
self.input_folder_button.setText(QCoreApplication.translate("MainWindow", u"input", None))
self.output_folder_button.setText(QCoreApplication.translate("MainWindow", u"output", None))
self.groupBox.setTitle(QCoreApplication.translate("MainWindow", u"Essential group", None))
self.resize_checkbox.setText(QCoreApplication.translate("MainWindow", u"Resize", None))
self.optimize_checkBox.setText(QCoreApplication.translate("MainWindow", u"optimize", None))
self.groupBox_2.setTitle(QCoreApplication.translate("MainWindow", u"Extra stuff", None))
self.watermark_lineEdit.setText("")
self.watermark_lineEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Enter Watermark", None))
self.brightness_checkbox.setText(QCoreApplication.translate("MainWindow", u"Brightness", None))
self.grayscale_checkBox.setText(QCoreApplication.translate("MainWindow", u"Grayscale", None))
self.watermark_checkbox.setText(QCoreApplication.translate("MainWindow", u"Watermark", None))
self.contrast_checkbox.setText(QCoreApplication.translate("MainWindow", u"Contrast", None))
self.font_size_comboBox.setItemText(0, QCoreApplication.translate("MainWindow", u"Tiny", None))
self.font_size_comboBox.setItemText(1, QCoreApplication.translate("MainWindow", u"Small", None))
self.font_size_comboBox.setItemText(2, QCoreApplication.translate("MainWindow", u"Normal", None))
self.font_size_comboBox.setItemText(3, QCoreApplication.translate("MainWindow", u"Large", None))
self.font_size_comboBox.setItemText(4, QCoreApplication.translate("MainWindow", u"Huge", None))
self.font_size_comboBox.setCurrentText(QCoreApplication.translate("MainWindow", u"Normal", None))
self.rename_group.setTitle(QCoreApplication.translate("MainWindow", u"files", None))
self.rename_checkbox.setText(QCoreApplication.translate("MainWindow", u"Rename", None))
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))
self.exif_checkbox.setText(QCoreApplication.translate("MainWindow", u"own exif", None))
self.exif_copy_checkBox.setText(QCoreApplication.translate("MainWindow", u"copy exif", None))
self.edit_exif_button.setText(QCoreApplication.translate("MainWindow", u"edit exif", None))
self.exif_options_group.setTitle(QCoreApplication.translate("MainWindow", u"Must", None))
self.label_7.setText(QCoreApplication.translate("MainWindow", u"Artist", None))
self.label_4.setText(QCoreApplication.translate("MainWindow", u"ISO", None))
self.label_6.setText(QCoreApplication.translate("MainWindow", u"Scanner", None))
self.label_2.setText(QCoreApplication.translate("MainWindow", u"Lens", None))
self.label_5.setText(QCoreApplication.translate("MainWindow", u"Film", None))
self.label.setText(QCoreApplication.translate("MainWindow", u"Make", None))
self.make_comboBox.setCurrentText("")
self.make_comboBox.setPlaceholderText("")
self.label_3.setText(QCoreApplication.translate("MainWindow", u"Model", None))
self.label_8.setText(QCoreApplication.translate("MainWindow", u"Copyright", None))
self.gps_groupBox.setTitle(QCoreApplication.translate("MainWindow", u"GPS", None))
self.gps_checkBox.setText(QCoreApplication.translate("MainWindow", u"add gps", None))
self.lat_lineEdit.setText("")
self.lat_lineEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"latitude [S, N]", None))
self.long_lineEdit.setText("")
self.long_lineEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"longitude [W, E]", None))
self.date_groupBox.setTitle(QCoreApplication.translate("MainWindow", u"Optional", 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))
# retranslateUi

897
ui/main_window.ui Normal file
View file

@ -0,0 +1,897 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>450</width>
<height>708</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<height>677</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>1000</height>
</size>
</property>
<property name="windowTitle">
<string>OPTIMA-35</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="maximumSize">
<size>
<width>500</width>
<height>16777215</height>
</size>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_1">
<attribute name="title">
<string>Main</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_10">
<item>
<widget class="QFrame" name="folder_group">
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLineEdit" name="input_path">
<property name="text">
<string>local_files/img</string>
</property>
<property name="placeholderText">
<string>Enter input folder</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="output_path">
<property name="text">
<string>local_files/out</string>
</property>
<property name="placeholderText">
<string>Enter output folder</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="input_folder_button">
<property name="text">
<string>input</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="output_folder_button">
<property name="text">
<string>output</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<property name="title">
<string>Essential group</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QCheckBox" name="resize_checkbox">
<property name="text">
<string>Resize</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="resize_spinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>200</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="value">
<number>80</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QComboBox" name="image_type">
<item>
<property name="text">
<string notr="true">jpg</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">png</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">webp</string>
</property>
</item>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="jpg_quality_spinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>80</number>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QSpinBox" name="png_quality_spinBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>9</number>
</property>
<property name="value">
<number>6</number>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="optimize_checkBox">
<property name="text">
<string>optimize</string>
</property>
</widget>
</item>
</layout>
<zorder>png_quality_spinBox</zorder>
<zorder>resize_checkbox</zorder>
<zorder>resize_spinBox</zorder>
<zorder>image_type</zorder>
<zorder>jpg_quality_spinBox</zorder>
<zorder>optimize_checkBox</zorder>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<property name="title">
<string>Extra stuff</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="3" column="0" colspan="3">
<widget class="QLineEdit" name="watermark_lineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Enter Watermark</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="brightness_checkbox">
<property name="text">
<string>Brightness</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="grayscale_checkBox">
<property name="text">
<string>Grayscale</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="contrast_spinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimum">
<number>-100</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="watermark_checkbox">
<property name="text">
<string>Watermark</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="brightness_spinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimum">
<number>-100</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>-10</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="contrast_checkbox">
<property name="text">
<string>Contrast</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="font_size_comboBox">
<property name="currentText">
<string>Normal</string>
</property>
<property name="currentIndex">
<number>2</number>
</property>
<item>
<property name="text">
<string>Tiny</string>
</property>
</item>
<item>
<property name="text">
<string>Small</string>
</property>
</item>
<item>
<property name="text">
<string>Normal</string>
</property>
</item>
<item>
<property name="text">
<string>Large</string>
</property>
</item>
<item>
<property name="text">
<string>Huge</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="rename_group">
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<property name="title">
<string>files</string>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="QCheckBox" name="rename_checkbox">
<property name="text">
<string>Rename</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="revert_checkbox">
<property name="text">
<string>Revert order</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLineEdit" name="filename">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Enter file name</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_9" native="true">
<property name="maximumSize">
<size>
<width>400</width>
<height>50</height>
</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>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="enabled">
<bool>true</bool>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="start_button">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Convert</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>EXIF</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QGroupBox" name="exif_group">
<property name="title">
<string>EXIF EXPERIMENTAL</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="exif_checkbox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>own exif</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="exif_copy_checkBox">
<property name="text">
<string>copy exif</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="edit_exif_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>edit exif</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="exif_options_group">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Must</string>
</property>
<layout class="QGridLayout" name="gridLayout_7">
<item row="3" column="0">
<widget class="QWidget" name="widget_7" native="true">
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Artist</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="artist_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="1" column="1">
<widget class="QWidget" name="widget_4" native="true">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>ISO</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="iso_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="2" column="1">
<widget class="QWidget" name="widget_6" native="true">
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Scanner</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="user_comment_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QWidget" name="widget_2" native="true">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Lens</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="lens_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QWidget" name="widget_5" native="true">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Film</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="image_description_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="0">
<widget class="QWidget" name="widget" native="true">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Make</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="make_comboBox">
<property name="currentText">
<string/>
</property>
<property name="placeholderText">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QWidget" name="widget_3" native="true">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Model</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="model_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="3" column="1">
<widget class="QWidget" name="widget_8" native="true">
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Copyright</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="copyright_info_comboBox"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gps_groupBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>GPS</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QCheckBox" name="gps_checkBox">
<property name="text">
<string>add gps</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lat_lineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>latitude [S, N]</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="long_lineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>longitude [W, E]</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="date_groupBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Optional</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="add_date_checkBox">
<property name="text">
<string>add date</string>
</property>
</widget>
</item>
<item>
<widget class="QDateEdit" name="dateEdit">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusBar"/>
</widget>
<resources/>
<connections>
<connection>
<sender>resize_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>resize_spinBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>75</x>
<y>96</y>
</hint>
<hint type="destinationlabel">
<x>196</x>
<y>118</y>
</hint>
</hints>
</connection>
<connection>
<sender>brightness_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>brightness_spinBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>83</x>
<y>363</y>
</hint>
<hint type="destinationlabel">
<x>83</x>
<y>399</y>
</hint>
</hints>
</connection>
<connection>
<sender>contrast_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>contrast_spinBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>185</x>
<y>363</y>
</hint>
<hint type="destinationlabel">
<x>185</x>
<y>399</y>
</hint>
</hints>
</connection>
<connection>
<sender>watermark_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>watermark_lineEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>83</x>
<y>435</y>
</hint>
<hint type="destinationlabel">
<x>237</x>
<y>435</y>
</hint>
</hints>
</connection>
<connection>
<sender>rename_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>filename</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>105</x>
<y>522</y>
</hint>
<hint type="destinationlabel">
<x>182</x>
<y>560</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>exif_options_group</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>130</x>
<y>105</y>
</hint>
<hint type="destinationlabel">
<x>236</x>
<y>328</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>exif_copy_checkBox</receiver>
<slot>setDisabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>130</x>
<y>105</y>
</hint>
<hint type="destinationlabel">
<x>332</x>
<y>105</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_copy_checkBox</sender>
<signal>toggled(bool)</signal>
<receiver>exif_checkbox</receiver>
<slot>setDisabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>332</x>
<y>105</y>
</hint>
<hint type="destinationlabel">
<x>130</x>
<y>105</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>edit_exif_button</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>134</x>
<y>107</y>
</hint>
<hint type="destinationlabel">
<x>79</x>
<y>170</y>
</hint>
</hints>
</connection>
<connection>
<sender>add_date_checkBox</sender>
<signal>toggled(bool)</signal>
<receiver>dateEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>94</x>
<y>601</y>
</hint>
<hint type="destinationlabel">
<x>224</x>
<y>602</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>date_groupBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>126</x>
<y>103</y>
</hint>
<hint type="destinationlabel">
<x>224</x>
<y>589</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>gps_groupBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>94</x>
<y>103</y>
</hint>
<hint type="destinationlabel">
<x>224</x>
<y>535</y>
</hint>
</hints>
</connection>
<connection>
<sender>gps_checkBox</sender>
<signal>toggled(bool)</signal>
<receiver>lat_lineEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>72</x>
<y>547</y>
</hint>
<hint type="destinationlabel">
<x>192</x>
<y>547</y>
</hint>
</hints>
</connection>
<connection>
<sender>gps_checkBox</sender>
<signal>toggled(bool)</signal>
<receiver>long_lineEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>72</x>
<y>547</y>
</hint>
<hint type="destinationlabel">
<x>344</x>
<y>547</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -1,6 +1,7 @@
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
import piexif
import time
from fractions import Fraction
class ImageProcessor:
"""Functions using pillow are in here."""
@ -38,15 +39,17 @@ class ImageProcessor:
resized_image = image.resize((new_size),resample=Image.Resampling.NEAREST)
return resized_image
def add_watermark(self, image, text, font_size_scale = 70):
def add_watermark(self, image, text, font_size_percentage):
# Still not happy about this function..
drawer = ImageDraw.Draw(image)
imagewidth, imageheight = image.size
margin = (imageheight / 100 ) * 2 # margin dynamic, 2% of image size
font_size = imagewidth / font_size_scale # Scaling the font size
font_size = imagewidth * (font_size_percentage / 100)
try: # Try loading front, if notaviable return unmodified image
font = ImageFont.truetype("OpenDyslexic3-Regular.ttf", font_size)
except:
print("Error loading font for watermark, please ensure font is installed...\n")
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
@ -55,16 +58,16 @@ class ImageProcessor:
y = imageheight - textheight - margin
# thin border
drawer.text((x-1, y), text, font=font, fill=(64, 64, 64))
drawer.text((x+1, y), text, font=font, fill=(64, 64, 64))
drawer.text((x, y-1), text, font=font, fill=(64, 64, 64))
drawer.text((x, y+1), text, font=font, fill=(64, 64, 64))
drawer.text((x-1, y), text, font = font, fill = (64, 64, 64))
drawer.text((x+1, y), text, font = font, fill = (64, 64, 64))
drawer.text((x, y-1), text, font = font, fill = (64, 64, 64))
drawer.text((x, y+1), text, font = font, fill = (64, 64, 64))
# Adding text in the desired color
drawer.text((x, y), text, font = font, fill=(255, 255, 255))
drawer.text((x, y), text, font = font, fill = (255, 255, 255))
return image
def save_image(self, image, path, file_type, jpg_quality, png_compressing, optimize, exif_data = None):
def save_image(self, image, path, file_type, jpg_quality, png_compressing, optimize, exif_data):
# partly optimized by chatGPT
"""
Save an image to the specified path with optional EXIF data and optimization.
@ -72,7 +75,7 @@ class ImageProcessor:
file_type = file_type.lower()
save_params = {"optimize": optimize}
# Add file-specific parameters
if file_type == "jpg":
if file_type == "jpg" or "webp":
save_params["quality"] = jpg_quality
elif file_type == "png":
save_params["compress_level"] = png_compressing
@ -102,21 +105,97 @@ class ExifHandler:
"""Build a piexif-compatible EXIF dictionary from user data."""
# Mostly made by ChatGPT, some adjustment
zeroth_ifd = {
piexif.ImageIFD.Make: user_data["make"],
piexif.ImageIFD.Model: user_data["model"],
piexif.ImageIFD.Software: user_data["software"],
piexif.ImageIFD.Copyright: user_data["copyright_info"],
piexif.ImageIFD.Artist: user_data["artist"],
piexif.ImageIFD.ImageDescription: user_data["image_description"],
piexif.ImageIFD.Make: user_data["make"].encode("utf-8"),
piexif.ImageIFD.Model: user_data["model"].encode("utf-8"),
piexif.ImageIFD.Software: user_data["software"].encode("utf-8"),
piexif.ImageIFD.Copyright: user_data["copyright_info"].encode("utf-8"),
piexif.ImageIFD.Artist: user_data["artist"].encode("utf-8"),
piexif.ImageIFD.ImageDescription: user_data["image_description"].encode("utf-8"),
piexif.ImageIFD.XResolution: (72, 1),
piexif.ImageIFD.YResolution: (72, 1),
}
exif_ifd = {
piexif.ExifIFD.UserComment: user_data["user_comment"],
piexif.ExifIFD.ISOSpeedRatings: int(user_data["iso"]),
piexif.ExifIFD.UserComment: user_data["user_comment"].encode("utf-8"),
piexif.ExifIFD.ISOSpeedRatings: int(user_data["iso"].encode("utf-8")),
piexif.ExifIFD.PixelXDimension: imagesize[0],
piexif.ExifIFD.PixelYDimension: imagesize[1],
}
if "date_time_original" in user_data:
exif_ifd[piexif.ExifIFD.DateTimeOriginal] = user_data["date_time_original"].encode("utf-8")
return {"0th": zeroth_ifd, "Exif": exif_ifd}
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.
:param decimal_coordinate: the decimal coordinates, such as 34.0522
:param cardinal_directions: the locations of the decimal coordinate, such as ["S", "N"] or ["W", "E"]
:return: degrees, minutes, seconds and compass_direction
:rtype: int, int, float, string
"""
if decimal_coordinate < 0:
compass_direction = cardinal_directions[0]
elif decimal_coordinate > 0:
compass_direction = cardinal_directions[1]
else:
compass_direction = ""
degrees = int(abs(decimal_coordinate))
decimal_minutes = (abs(decimal_coordinate) - degrees) * 60
minutes = int(decimal_minutes)
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):
"""
This function converts DMS (degrees, minutes and seconds) to values that can
be used with the EXIF (Exchangeable Image File Format).
:param dms_degrees: int value for degrees
:param dms_minutes: int value for minutes
:param dms_seconds: fractions.Fraction value for seconds
:return: EXIF values for the provided DMS values
:rtype: nested tuple
"""
exif_format = (
(dms_degrees, 1),
(dms_minutes, 1),
(int(dms_seconds.limit_denominator(100).numerator), int(dms_seconds.limit_denominator(100).denominator))
)
return exif_format
def add_geolocation_to_exif(self, exif_data, latitude, longitude):
"""
https://stackoverflow.com/questions/77015464/adding-exif-gps-data-to-jpg-files-using-python-and-piexif
This function adds GPS values to an image using the EXIF format.
This fumction calls the functions deg_to_dms and dms_to_exif_format.
:param image_path: image to add the GPS data to
:param latitude: the northsouth position coordinate
: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"])
# 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])
try:
# https://exiftool.org/TagNames/GPS.html
# Create the GPS EXIF data
coordinates = {
piexif.GPSIFD.GPSVersionID: (2, 0, 0, 0),
piexif.GPSIFD.GPSLatitude: exif_latitude,
piexif.GPSIFD.GPSLatitudeRef: latitude_dms[3],
piexif.GPSIFD.GPSLongitude: exif_longitude,
piexif.GPSIFD.GPSLongitudeRef: longitude_dms[3]
}
# Update the EXIF data with the GPS information
exif_data["GPS"] = coordinates
return exif_data
except Exception as e:
print(f"Error: {str(e)}")

View file

@ -34,7 +34,6 @@ class Utilities:
def progress_bar(self, current, total, barsize = 50):
if current > total:
print("\033[91mThis bar has exceeded its limits!\033[0m Maybe the current value needs some restraint?")
print(f"{(current - total) * '\033[92mHonk, Honk!\033[0m '}")
return
progress = int((barsize / total) * current)
rest = barsize - progress