Feature/gui
This commit is contained in:
parent
c36ab9b733
commit
1512e8efeb
15 changed files with 2328 additions and 299 deletions
47
CHANGELOG.md
47
CHANGELOG.md
|
@ -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.
|
||||
|
|
27
LICENSE.md
27
LICENSE.md
|
@ -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.
|
||||
|
|
36
README.md
36
README.md
|
@ -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.*
|
||||
|
||||
|
||||
{width=40%}
|
||||
{width=40%}
|
||||
{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**
|
||||
|
||||

|
||||
|
||||
## 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 OpenAI’s 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.**
|
||||
|
|
|
@ -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
527
main.py
|
@ -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
292
main_tui.py
Normal 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
BIN
media/exif_tab.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 188 KiB |
BIN
media/exifeditor.png
Normal file
BIN
media/exifeditor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
BIN
media/main_tab.png
Normal file
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
87
ui/exif_handler_window.py
Normal 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
550
ui/main_window.py
Normal 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
897
ui/main_window.ui
Normal 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>
|
|
@ -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 north–south position coordinate
|
||||
:param longitude: the east–west 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)}")
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue