Merge branch 'chore/remove-core-optima35' into 'main'
Chore/remove core optima35 See merge request CodeByMrFinchum/optima-lab-35!1
This commit is contained in:
commit
5f738bdf14
13 changed files with 191 additions and 549 deletions
163
CHANGELOG.md
163
CHANGELOG.md
|
@ -1,160 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
## 0.5.x
|
||||
### **OPTIMA35 0.5.0: Code Cleaning and Preparation for Split**
|
||||
- Cleaned up the codebase, following **PEP8**, adding indication for only internal functions.
|
||||
- Refactored the project in preparation for splitting it into **OPTIMA35 (core functionality)** and **UI (graphical and text interfaces)**.
|
||||
- Moved `image_handler.py` into the `optima` folder/package to integrate it as an essential part of the OPTIMA35 package, rather than just a utility.
|
||||
|
||||
### **UI 0.1.0: GUI and TUI Updates**
|
||||
- Updated **GUI** and **TUI** to work seamlessly with the new **OPTIMA35** class.
|
||||
- Ensured compatibility with the newly organized codebase in the OPTIMA35 package.
|
||||
|
||||
## 0.4.x
|
||||
### 0.4.1: Finished GUI and TUI
|
||||
- Both **GUI** and **TUI** now fully utilize the `optima35` class for core functionality.
|
||||
- All planned features are operational and integrated into both interfaces.
|
||||
- **Next Step**: Bug hunting and optimization.
|
||||
- The fork `optima-35-tui` has been deleted, as **OPTIMA-35** now includes both **TUI** and **GUI** versions within the same project.
|
||||
|
||||
### 0.4.0: Splitting into Classes
|
||||
- **Code Organization:**
|
||||
- Core functionality for **Optima-35** is now refactored into `optima35.py` for better separation of logic and reusability.
|
||||
- The **GUI code** is moved to `gui.py` for a cleaner structure and maintainability.
|
||||
- The **TUI logic** will be moved into `tui.py`, making it modular and focused.
|
||||
- The original TUI fork will be deleted to streamline operations.
|
||||
|
||||
- **Main File Enhancements:**
|
||||
- `main.py` is now the entry point of the application and determines whether to start the GUI or TUI based on:
|
||||
- Operating system.
|
||||
- The presence of required dependencies (e.g., PySide for GUI).
|
||||
- Command-line arguments (`--tui` flag).
|
||||
|
||||
- **Benefits:**
|
||||
- Clear separation of concerns between GUI, TUI, and core functionalities.
|
||||
- Improved readability, maintainability, and scalability of the project.
|
||||
- Easier to test and debug individual components.
|
||||
|
||||
## 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.
|
||||
|
||||
### 0.2.0
|
||||
- **Cleaner folder structure**
|
||||
- Moving files with classes to different folder to keep project cleaner.
|
||||
|
||||
## 0.1.x
|
||||
### 0.1.1
|
||||
- **Add Original to add Timestamp to Images**
|
||||
- Introduced an option to add the original timestamp to images. Some programs use timestamps rather than file names to determine order, also enables a timeline-like organization for images.
|
||||
- **Improved Font Handling**
|
||||
- Instead of terminating the process when a font is not found, the program now skips the operation gracefully.
|
||||
- **Input Validation**
|
||||
- Added checks for input types, including strings, floats, and integers, to enhance robustness.
|
||||
- **Save Function Optimization**
|
||||
- Optimized the save function for cleaner code, partially utilizing ChatGPT-generated suggestions.
|
||||
- **Code Formatting**
|
||||
- Improved code structure and formatting for better readability and maintainability.
|
||||
|
||||
### 0.1.0: Core Features Added
|
||||
- **Images are modified through all selected options without saving, reducing quality degradation and saving local storage.**
|
||||
- **All core features are available:**
|
||||
- Simple TUI including options for selecting:
|
||||
- Resize
|
||||
- Change EXIF (with options from exif_options.yaml), copy original or leave empty
|
||||
- Convert to grayscale
|
||||
- Change contrast
|
||||
- Change brightness
|
||||
- Rename images (numbers are added automatically at the end)
|
||||
- Invert image order (so that the first image gets the last number and the last image gets the first; this is useful when the original numbering is reversed, which often happens when scanning 35mm film).
|
||||
- Add watermark (**Experimental**, requires the correct font to be installed)
|
||||
- **Settings via YAML:**
|
||||
- At the start of the program, the user is asked to save default values, such as JPG quality, resize options, and more. This way, the settings don't have to be entered at every start. Upon starting, the user is prompted to confirm whether they want to keep the current settings from the settings file.
|
||||
- Options for changing EXIF data are saved in exif_options.yaml. Here, you can enter all the models, lenses, etc., you would like to select within the program.
|
||||
|
||||
## 0.0.x
|
||||
### 0.0.3: Enhanced Functionality - now useable
|
||||
- **New Image Modification Functions:**
|
||||
- Added support for Grayscale conversion.
|
||||
- Introduced Brightness adjustment.
|
||||
- Enhanced with Contrast adjustment.
|
||||
- **New User Selection/Settings Features:**
|
||||
- Default values for settings can now be saved, such as:
|
||||
- JPEG quality.
|
||||
- PNG compression.
|
||||
- Additional optimization preferences.
|
||||
- Input folder, output folder, and file type are requested for every session.
|
||||
- **Progress Bar for Image Processing:**
|
||||
- Implemented a progress bar to visually track the processing of images.
|
||||
|
||||
### 0.0.2: Enhanced Functionality
|
||||
- **First Functional Features:**
|
||||
- Introduced the first operational functions, enabling the program to process images based on user input.
|
||||
- **User Interaction:**
|
||||
- Added functionality to prompt users for their desired operations (e.g., resizing and/or changing EXIF data) and gather necessary inputs.
|
||||
- **Image Processing Pipeline:**
|
||||
- The program now traverses a folder, opens images, applies modifications, and saves the processed files to the specified output location.
|
||||
- **EXIF Handling:**
|
||||
- If EXIF changes are not desired, the program offers an option to copy the original EXIF data while adjusting key fields (e.g., image dimensions).
|
||||
- **No Safety Checks Yet:**
|
||||
- Input validation (e.g., verifying folder existence, ensuring numeric input for percentages) is not yet implemented.
|
||||
- **Foundation for Future Features:**
|
||||
- The groundwork allows for seamless addition of new image processing functions, leveraging the main class and TUI structure.
|
||||
|
||||
### Version 0.0.1: Initial Setup
|
||||
- **Foundation Work:**
|
||||
- Established the groundwork for the project, focusing on testing and selecting the most effective tools. Transitioned from PyExifTool and Wand to Pillow and Piexif to minimize dependencies and streamline usability.
|
||||
- **Proof of Concept:**
|
||||
- Conducted extensive testing and developed proof-of-concept functions to evaluate how various libraries integrate and perform.
|
||||
- **TUI Development:**
|
||||
- Opted to use simple_term_menu instead of textual for the terminal-based user interface (TUI), leveraging prior experience to accelerate functional development over interface design.
|
||||
- **AI Exploration:**
|
||||
- Tested local generative AI tools such as OpenCoder and Qwen2-Coder, exploring their potential for future project integration.
|
||||
- **Project Status:**
|
||||
- The majority of work so far focuses on proof-of-concept implementation and experimentation.
|
||||
### 0.0.1 - Initial UI-Focused Release
|
||||
- Forked from OPTIMA35
|
||||
- Refactored the project to include only UI elements; all core OPTIMA35 files have been removed.
|
||||
- Updated the changelog to reflect the changes.
|
||||
- Integrated with OPTIMA35 from pip, ensuring TUI and GUI functionalities work seamlessly.
|
||||
- Made initial adjustments to the README for clarity and structure.
|
||||
|
|
32
README.md
32
README.md
|
@ -1,27 +1,22 @@
|
|||
# OPTIMA-35
|
||||
# OptimaLab35
|
||||
UI for [OPTIMA35](https://gitlab.com/CodeByMrFinchum/optima-35) package, WIP.
|
||||
|
||||
## Overview
|
||||
|
||||
**OPTIMA-35** (**Organizing, Processing, Tweaking Images, and Modifying scanned Analogs from 35mm Film**) is a Python-based project designed to streamline the management and editing of metadata and images from analog photography. While it was created with analog photography in mind, it is versatile enough to handle any type of images.
|
||||
|
||||
This project replaces my earlier [analogphotography](https://gitlab.com/sf-bashscripts/analogphotography) bash script collection, which has now been archived in favor of OPTIMA-35.
|
||||
|
||||
**OPTIMA-35** is a cross-platform program. The **GUI** works on Linux and Windows(1) and is expected to run on macOS. The **TUI** is currently Linux-only, as its dependency is exclusive to Linux.
|
||||
|
||||
(1): Windows' default image viewer has limitations in displaying some EXIF metadata. Use dedicated software for full EXIF data visibility.
|
||||
|
||||
## Current Status
|
||||
|
||||
### Development and Versioning Notes
|
||||
|
||||
**OPTIMA-35** is currently in an **alpha stage** and under active development. As a result:
|
||||
- The README may occasionally be outdated.
|
||||
- Users are encouraged to check for new branches and read the [**CHANGELOG**](https://gitlab.com/CodeByMrFinchum/optima-35/-/blob/main/CHANGELOG.md?ref_type=heads), which is consistently updated and well-documented.
|
||||
- Bugs or unforeseen behavior may occur.
|
||||
**OptimaLab35** is currently in an **alpha stage** and under active development. As a result:
|
||||
- The README may occasionally be outdated.
|
||||
- Users are encouraged to check for new branches and read the Changelog
|
||||
- Bugs or unforeseen behavior may occur.
|
||||
|
||||
While the project follows a semantic versioning structure (major.minor.patch), breaking changes—typically reserved for major version increments—may also occur in minor version updates during this development phase. Please review the changelog carefully before updating.
|
||||
|
||||
**OPTIMA-35** supports two modes: **GUI** and **TUI**.
|
||||
**OptimaLab35** supports two modes: **GUI** and **TUI**.
|
||||
- The **GUI** is loaded by default if **PySide6** is available.
|
||||
- The **TUI** serves as a fallback when **PySide6** is unavailable or can be started explicitly using the `--tui` option with `main.py`.
|
||||
|
||||
|
@ -76,24 +71,17 @@ While all features are implemented and functional, the designs of both the GUI a
|
|||
**OPTIMA-35** has two modes: **GUI** and **TUI**. Each mode has its own set of dependencies, so you don’t need to install TUI dependencies if you only plan to use the GUI (and vice versa).
|
||||
|
||||
**Required Dependencies:**
|
||||
- **pyyaml**: For handling YAML files (configuration and settings).
|
||||
- **piexif**: For reading, modifying, and writing EXIF metadata.
|
||||
- **pillow**: For image processing.
|
||||
- **optima35**
|
||||
- **pyside6**: For the GUI mode.
|
||||
- **simple_term_menu**: For the TUI mode.
|
||||
|
||||
### Installing Dependencies
|
||||
|
||||
You can install the dependencies using the respective requirements file for your desired mode (**TUI** or **GUI**).
|
||||
You can install the dependencies using the requirements file
|
||||
|
||||
Using `pip`:
|
||||
```bash
|
||||
pip install -r requirements_gui.txt
|
||||
```
|
||||
|
||||
Alternatively, if you use **conda** or its alternatives (**anaconda**, **mamba**, **micromamba**), run:
|
||||
```bash
|
||||
conda install -c conda-forge --file requirements_gui.txt
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
# Use of LLMs
|
||||
|
|
43
gui.py
43
gui.py
|
@ -2,10 +2,11 @@ import sys
|
|||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from optima.optima35 import OPTIMA35
|
||||
from optima35.core import OptimaManager
|
||||
from utils.utility import Utilities
|
||||
from ui.main_window import Ui_MainWindow
|
||||
from ui.exif_handler_window import ExifEditor
|
||||
from ui.simple_dialog import SimpleDialog # Import the SimpleDialog class
|
||||
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtWidgets import (
|
||||
|
@ -23,24 +24,25 @@ from PySide6.QtWidgets import (
|
|||
QSpinBox,
|
||||
)
|
||||
|
||||
class Optima35GUI(QMainWindow, Ui_MainWindow):
|
||||
def __init__(self, exif_file):
|
||||
super(Optima35GUI, self).__init__()
|
||||
self.name = "GUI35"
|
||||
self.version = "0.1.0"
|
||||
class OptimaLab35(QMainWindow, Ui_MainWindow):
|
||||
def __init__(self):
|
||||
super(OptimaLab35, self).__init__()
|
||||
self.name = "OptimaLab35"
|
||||
self.version = "0.0.1"
|
||||
self.ui = Ui_MainWindow()
|
||||
self.ui.setupUi(self)
|
||||
self.o = OPTIMA35()
|
||||
self.o = OptimaManager()
|
||||
self.u = Utilities()
|
||||
self.exif_file = exif_file
|
||||
self.u.program_configs()
|
||||
self.exif_file = os.path.expanduser("~/.config/OptimaLab35/exif.yaml")
|
||||
self.available_exif_data = None
|
||||
self.settings = {}
|
||||
self.setWindowTitle(f"{self.name} v{self.version} for {self.o.name} {self.o.version}")
|
||||
self.setWindowTitle(f"{self.name} v{self.version}")
|
||||
self._default_ui_layout()
|
||||
self._define_gui_interaction()
|
||||
|
||||
if exif_file == "config/exif_example.yaml":
|
||||
self._change_statusbar("Using example exif...", 10000)
|
||||
self.sd = SimpleDialog()
|
||||
self._change_statusbar(f"Using {self.o.name} v{self.o.version}", 5000)
|
||||
|
||||
def _default_ui_layout(self):
|
||||
self.ui.png_quality_spinBox.setVisible(False)
|
||||
|
@ -57,6 +59,11 @@ class Optima35GUI(QMainWindow, Ui_MainWindow):
|
|||
self.ui.tabWidget.currentChanged.connect(self._on_tab_changed)
|
||||
self.ui.edit_exif_button.clicked.connect(self._open_exif_editor)
|
||||
|
||||
self.ui.actionInfo.triggered.connect(self._info_window)
|
||||
|
||||
def _info_window(self):
|
||||
self.sd.show_dialog(f"{self.name} v{self.version}", f"{self.name} is a GUI for {self.o.name} (v{self.o.version})")
|
||||
|
||||
def _process(self):
|
||||
self.ui.start_button.setEnabled(False)
|
||||
self._update_settings() # Get all user selected data
|
||||
|
@ -265,19 +272,11 @@ class Optima35GUI(QMainWindow, Ui_MainWindow):
|
|||
user_data["software"] = f"{self.o.name} {self.o.version}"
|
||||
return user_data
|
||||
|
||||
def main(exif_file):
|
||||
def main():
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
window = Optima35GUI(exif_file=exif_file)
|
||||
window = OptimaLab35()
|
||||
window.show()
|
||||
app.exec()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.path.isfile("config/exif.yaml"):
|
||||
exif_file = "config/exif.yaml"
|
||||
print("Fall back to exif example file...")
|
||||
elif os.path.isfile("config/exif_example.yaml"):
|
||||
exif_file = "config/exif_example.yaml"
|
||||
else:
|
||||
print("Exif file missing, please ensure an exif file exist in config folder (exif.yaml, or exif_example_yaml)\nExiting...")
|
||||
exit()
|
||||
main(exif_file)
|
||||
main()
|
||||
|
|
13
main.py
13
main.py
|
@ -11,11 +11,11 @@ def check_pyside_installed():
|
|||
|
||||
def start_gui():
|
||||
import gui
|
||||
gui.main(exif_file)
|
||||
gui.main()
|
||||
|
||||
def start_tui():
|
||||
import tui
|
||||
tui.main(exif_file, tui_settings_file)
|
||||
tui.main()
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(description="Start the Optima35 application.")
|
||||
|
@ -41,13 +41,4 @@ def main():
|
|||
start_tui()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.path.isfile("config/exif.yaml"):
|
||||
exif_file = "config/exif.yaml"
|
||||
elif os.path.isfile("config/exif_example.yaml"):
|
||||
exif_file = "config/exif_example.yaml"
|
||||
print("Fall back to exif example file...")
|
||||
else:
|
||||
print("Exif file missing, please ensure an exif file exist in config folder (exif.yaml, or exif_example_yaml)\nExiting...")
|
||||
exit()
|
||||
tui_settings_file = "config/tui_settings.yaml"
|
||||
main()
|
||||
|
|
|
@ -1,199 +0,0 @@
|
|||
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
|
||||
import piexif
|
||||
from fractions import Fraction
|
||||
|
||||
class ImageProcessor:
|
||||
"""Functions using pillow are in here."""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def open_image(self, path):
|
||||
"""Open an image from path, returns image object."""
|
||||
return Image.open(path)
|
||||
|
||||
def get_image_size(self, image):
|
||||
"""Simply get image size."""
|
||||
return image.size
|
||||
|
||||
def grayscale(self, image):
|
||||
"""Change to grayscale"""
|
||||
return image.convert("L")
|
||||
|
||||
def change_contrast(self, image, change):
|
||||
"""Change contrast by percent."""
|
||||
enhancer = ImageEnhance.Contrast(image)
|
||||
new_img = enhancer.enhance(1 + (change/100))
|
||||
return new_img
|
||||
|
||||
def change_brightness(self, image, change):
|
||||
"""Changes brightness by percent"""
|
||||
enhancer = ImageEnhance.Brightness(image)
|
||||
new_img = enhancer.enhance(1 + (change/100))
|
||||
return new_img
|
||||
|
||||
def resize_image(self, image, percent, resample = True):
|
||||
"""Resize an image by giving a percent."""
|
||||
new_size = tuple(int(x * (percent / 100)) for x in image.size)
|
||||
if resample:
|
||||
resized_image = image.resize(new_size)
|
||||
else:
|
||||
resized_image = image.resize((new_size),resample=Image.Resampling.NEAREST)
|
||||
return resized_image
|
||||
|
||||
def add_watermark(self, image, text, font_size_percentage):
|
||||
"""Addes a watermark to the image using default os font."""
|
||||
drawer = ImageDraw.Draw(image)
|
||||
imagewidth, imageheight = image.size
|
||||
margin = (imageheight / 100 ) * 2 # margin dynamic, 2% of image size
|
||||
font_size = imagewidth * (font_size_percentage / 100)
|
||||
|
||||
try: # Try loading front, if notaviable return unmodified image
|
||||
font = ImageFont.load_default(font_size)
|
||||
except Exception as e:
|
||||
print(f"Error {e}\nloading font for watermark, please ensure font is installed...\n")
|
||||
return image
|
||||
|
||||
c, w, textwidth, textheight, = drawer.textbbox(xy = (0, 0), text = text, font = font) # Getting text size, only need the last two values
|
||||
x = imagewidth - textwidth - margin
|
||||
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))
|
||||
# Adding text in the desired color
|
||||
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, piexif_exif_data):
|
||||
# partly optimized by chatGPT
|
||||
"""
|
||||
Save an image to the specified path with optional EXIF data.
|
||||
"""
|
||||
save_params = {"optimize": optimize}
|
||||
# Add file-specific parameters
|
||||
if file_type == "jpg" or "webp":
|
||||
save_params["quality"] = jpg_quality
|
||||
elif file_type == "png":
|
||||
save_params["compress_level"] = png_compressing
|
||||
elif file_type not in ["webp", "jpg", "png"]:
|
||||
input(f"Type: {file_type} is not supported. Press Enter to continue...")
|
||||
return
|
||||
# Add EXIF data if available
|
||||
if piexif_exif_data is not None:
|
||||
save_params["exif"] = piexif.dump(piexif_exif_data)
|
||||
if file_type == "webp":
|
||||
print("File format webp does not support all exif features, some information might get lost...\n")
|
||||
try:
|
||||
image.save(f"{path}.{file_type}", **save_params)
|
||||
except Exception as e:
|
||||
print(f"Failed to save image: {e}")
|
||||
|
||||
class ExifHandler:
|
||||
"""Function using piexif are here."""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_exif_info(self, image):
|
||||
return(piexif.load(image.info['exif']))
|
||||
|
||||
def build_exif_dict(self, user_data, imagesize):
|
||||
"""Build a piexif-compatible EXIF dictionary from a dicts."""
|
||||
# Mostly made by ChatGPT, some adjustment
|
||||
zeroth_ifd = {
|
||||
piexif.ImageIFD.Make: user_data["make"].encode("utf-8"),
|
||||
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"].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)}")
|
|
@ -1,108 +0,0 @@
|
|||
import re
|
||||
import os
|
||||
from datetime import datetime
|
||||
from optima.image_handler import ImageProcessor, ExifHandler
|
||||
|
||||
class OPTIMA35:
|
||||
def __init__(self):
|
||||
self.name = "OPTIMA-35"
|
||||
self.version = "0.5.0"
|
||||
self.image_processor = ImageProcessor()
|
||||
self.exif_handler = ExifHandler()
|
||||
|
||||
def modify_timestamp_in_exif(self, data_for_exif: dict, filename: str):
|
||||
""""Takes a dict formated for exif use by piexif and adjusts the date_time_original, changing the minutes and seconds to fit the number of the filname."""
|
||||
last_tree = filename[-3:len(filename)]
|
||||
total_seconds = int(re.sub(r'\D+', '', last_tree))
|
||||
minutes = total_seconds // 60
|
||||
seconds = total_seconds % 60
|
||||
time = datetime.strptime(data_for_exif["date_time_original"], "%Y:%m:%d %H:%M:%S") # change date time string back to an time object for modification
|
||||
new_time = time.replace(hour=12, minute=minutes, second=seconds)
|
||||
data_for_exif["date_time_original"] = new_time.strftime("%Y:%m:%d %H:%M:%S")
|
||||
return data_for_exif
|
||||
|
||||
def process_image(self,
|
||||
image_input_file,
|
||||
image_output_file,
|
||||
file_type,
|
||||
quality,
|
||||
compressing,
|
||||
optimize,
|
||||
resize = None,
|
||||
watermark = None,
|
||||
font_size = 2,
|
||||
grayscale = False,
|
||||
brightness = None,
|
||||
contrast = None,
|
||||
dict_for_exif = None,
|
||||
gps = None,
|
||||
copy_exif = False):
|
||||
# Partly optimized by ChatGPT
|
||||
# Open the image file
|
||||
with self.image_processor.open_image(image_input_file) as img:
|
||||
processed_img = img
|
||||
image_name = os.path.basename(image_output_file) # for date adjustment
|
||||
|
||||
# Resize
|
||||
if resize is not None:
|
||||
processed_img = self.image_processor.resize_image(
|
||||
image=processed_img, percent=resize
|
||||
)
|
||||
|
||||
# Watermark
|
||||
if watermark is not None:
|
||||
processed_img = self.image_processor.add_watermark(
|
||||
processed_img, watermark, int(font_size)
|
||||
)
|
||||
|
||||
# Grayscale
|
||||
if grayscale:
|
||||
processed_img = self.image_processor.grayscale(processed_img)
|
||||
|
||||
# Brightness
|
||||
if brightness is not None:
|
||||
processed_img = self.image_processor.change_brightness(
|
||||
processed_img, brightness
|
||||
)
|
||||
|
||||
# Contrast
|
||||
if contrast is not None:
|
||||
processed_img = self.image_processor.change_contrast(
|
||||
processed_img, contrast
|
||||
)
|
||||
|
||||
# EXIF data handling
|
||||
exif_piexif_format = None
|
||||
if dict_for_exif: # todo: maybe move to ui and only accept complete exif dicts..
|
||||
selected_exif = dict_for_exif
|
||||
if "date_time_original" in dict_for_exif:
|
||||
selected_exif = self.modify_timestamp_in_exif(selected_exif, image_name)
|
||||
exif_piexif_format = self.exif_handler.build_exif_dict(
|
||||
selected_exif, self.image_processor.get_image_size(processed_img)
|
||||
)
|
||||
|
||||
# GPS data
|
||||
if gps is not None:
|
||||
latitude = float(gps[0])
|
||||
longitude = float(gps[1])
|
||||
exif_piexif_format = self.exif_handler.add_geolocation_to_exif(exif_piexif_format, latitude, longitude)
|
||||
|
||||
# Copy EXIF data if selected, and ensure size is correct in exif data
|
||||
elif copy_exif:
|
||||
try:
|
||||
og_exif = self.exif_handler.get_exif_info(img)
|
||||
og_exif["Exif"][40962], og_exif["Exif"][40963] = self.image_processor.get_image_size(processed_img)
|
||||
exif_piexif_format = og_exif
|
||||
except Exception:
|
||||
print("Copying EXIF data selected, but no EXIF data is available in the original image file.")
|
||||
|
||||
# Save the processed image
|
||||
self.image_processor.save_image(
|
||||
image = processed_img,
|
||||
path = image_output_file,
|
||||
piexif_exif_data = exif_piexif_format,
|
||||
file_type = file_type,
|
||||
jpg_quality = quality,
|
||||
png_compressing = compressing,
|
||||
optimize = optimize
|
||||
)
|
|
@ -1,3 +1,4 @@
|
|||
pyyaml
|
||||
piexif
|
||||
pillow
|
||||
optima35
|
||||
pyside6
|
||||
simple_term_menu
|
||||
|
|
35
tui.py
35
tui.py
|
@ -1,20 +1,21 @@
|
|||
import os
|
||||
from datetime import datetime
|
||||
# my packages
|
||||
from optima.optima35 import OPTIMA35
|
||||
from optima35.core import OptimaManager
|
||||
from utils.utility import Utilities
|
||||
from ui.simple_tui import SimpleTUI
|
||||
|
||||
class Optima35TUI():
|
||||
def __init__(self, exif_file, settings_file):
|
||||
self.name = "TUI35"
|
||||
self.version = "0.1.0"
|
||||
self.o = OPTIMA35()
|
||||
def __init__(self):
|
||||
self.name = "OptimaLab35-lite"
|
||||
self.version = "0.0.1"
|
||||
self.o = OptimaManager()
|
||||
self.u = Utilities()
|
||||
self.tui = SimpleTUI()
|
||||
self.exif_file = exif_file
|
||||
self.available_exif_data = self.u.read_yaml(exif_file)
|
||||
self.setting_file = settings_file
|
||||
self.u.program_configs()
|
||||
self.exif_file = os.path.expanduser("~/.config/OptimaLab35/exif.yaml")
|
||||
self.available_exif_data = self.u.read_yaml(self.exif_file)
|
||||
self.setting_file = os.path.expanduser("~/.config/OptimaLab35/tui_settings.yaml")
|
||||
self.settings = {
|
||||
"input_folder": None,
|
||||
"output_folder": None,
|
||||
|
@ -153,6 +154,7 @@ class Optima35TUI():
|
|||
|
||||
def _ask_for_settings(self):
|
||||
print("Asking for new settings...\n")
|
||||
print(f"Settings for {self.name} v{self.version} will be saved {self.setting_file}...")
|
||||
self.settings["resize"] = self.take_input_and_validate(question = "Default resize percentage (below 100 downscale, above upscale): ", accepted_type = int, min_value = 10, max_value = 200)
|
||||
self.settings["contrast"] = self.take_input_and_validate(question = "Default contrast percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100)
|
||||
self.settings["brightness"] = self.take_input_and_validate(question = "Default brighness percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100)
|
||||
|
@ -189,6 +191,7 @@ class Optima35TUI():
|
|||
|
||||
def _collect_exif_data(self):
|
||||
"""Collect EXIF data based on user input."""
|
||||
print(f"Exif file can be found {self.exif_file}...")
|
||||
user_data = {}
|
||||
fields = [
|
||||
"make", "model", "lens", "iso", "image_description",
|
||||
|
@ -318,19 +321,9 @@ class Optima35TUI():
|
|||
self._process()
|
||||
print("Done")
|
||||
|
||||
|
||||
|
||||
def main(exif_file, config_file):
|
||||
app = Optima35TUI(exif_file, config_file)
|
||||
def main():
|
||||
app = Optima35TUI()
|
||||
app.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.path.isfile("config/exif.yaml"):
|
||||
exif_file = "config/exif.yaml"
|
||||
elif os.path.isfile("config/exif_example.yaml"):
|
||||
exif_file = "config/exif_example.yaml"
|
||||
print("Fall back to exif example file...")
|
||||
else:
|
||||
print("Exif file missing, please ensure an exif file exist in config folder (exif.yaml, or exif_example_yaml)\nExiting...")
|
||||
exit()
|
||||
main(exif_file, "config/tui_settings.yaml")
|
||||
main()
|
||||
|
|
|
@ -11,15 +11,17 @@
|
|||
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.QtGui import (QAction, 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)
|
||||
QLabel, QLineEdit, QMainWindow, QMenu,
|
||||
QMenuBar, QProgressBar, QPushButton, QSizePolicy,
|
||||
QSpinBox, QStatusBar, QTabWidget, QVBoxLayout,
|
||||
QWidget)
|
||||
|
||||
class Ui_MainWindow(object):
|
||||
def setupUi(self, MainWindow):
|
||||
|
@ -28,6 +30,8 @@ class Ui_MainWindow(object):
|
|||
MainWindow.resize(450, 708)
|
||||
MainWindow.setMinimumSize(QSize(350, 677))
|
||||
MainWindow.setMaximumSize(QSize(500, 1000))
|
||||
self.actionInfo = QAction(MainWindow)
|
||||
self.actionInfo.setObjectName(u"actionInfo")
|
||||
self.centralwidget = QWidget(MainWindow)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.gridLayout = QGridLayout(self.centralwidget)
|
||||
|
@ -459,6 +463,15 @@ class Ui_MainWindow(object):
|
|||
self.statusBar = QStatusBar(MainWindow)
|
||||
self.statusBar.setObjectName(u"statusBar")
|
||||
MainWindow.setStatusBar(self.statusBar)
|
||||
self.menuBar = QMenuBar(MainWindow)
|
||||
self.menuBar.setObjectName(u"menuBar")
|
||||
self.menuBar.setGeometry(QRect(0, 0, 450, 27))
|
||||
self.menuInfo = QMenu(self.menuBar)
|
||||
self.menuInfo.setObjectName(u"menuInfo")
|
||||
MainWindow.setMenuBar(self.menuBar)
|
||||
|
||||
self.menuBar.addAction(self.menuInfo.menuAction())
|
||||
self.menuInfo.addAction(self.actionInfo)
|
||||
|
||||
self.retranslateUi(MainWindow)
|
||||
self.resize_checkbox.toggled.connect(self.resize_spinBox.setEnabled)
|
||||
|
@ -485,6 +498,7 @@ class Ui_MainWindow(object):
|
|||
|
||||
def retranslateUi(self, MainWindow):
|
||||
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"OPTIMA-35", None))
|
||||
self.actionInfo.setText(QCoreApplication.translate("MainWindow", u"Info", None))
|
||||
self.input_path.setText("")
|
||||
self.input_path.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Enter input folder", None))
|
||||
self.output_path.setText("")
|
||||
|
@ -540,5 +554,6 @@ class Ui_MainWindow(object):
|
|||
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))
|
||||
self.menuInfo.setTitle(QCoreApplication.translate("MainWindow", u"Info", None))
|
||||
# retranslateUi
|
||||
|
||||
|
|
|
@ -666,6 +666,28 @@
|
|||
</layout>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusBar"/>
|
||||
<widget class="QMenuBar" name="menuBar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>450</width>
|
||||
<height>27</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuInfo">
|
||||
<property name="title">
|
||||
<string>Info</string>
|
||||
</property>
|
||||
<addaction name="actionInfo"/>
|
||||
</widget>
|
||||
<addaction name="menuInfo"/>
|
||||
</widget>
|
||||
<action name="actionInfo">
|
||||
<property name="text">
|
||||
<string>Info</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
|
|
30
ui/simple_dialog.py
Normal file
30
ui/simple_dialog.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from PySide6.QtWidgets import QApplication, QDialog, QVBoxLayout, QLineEdit, QPushButton, QLabel
|
||||
# ChatGPT
|
||||
class SimpleDialog(QDialog):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Set default properties
|
||||
self.setGeometry(100, 100, 300, 100)
|
||||
|
||||
# Create the layout
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Create the label for the message
|
||||
self.message_label = QLabel(self)
|
||||
|
||||
# Create the close button
|
||||
close_button = QPushButton("Close", self)
|
||||
close_button.clicked.connect(self.close)
|
||||
|
||||
# Add widgets to layout
|
||||
layout.addWidget(self.message_label)
|
||||
layout.addWidget(close_button)
|
||||
|
||||
# Set layout for the dialog
|
||||
self.setLayout(layout)
|
||||
|
||||
def show_dialog(self, title: str, message: str):
|
||||
self.setWindowTitle(title) # Set the window title
|
||||
self.message_label.setText(message) # Set the message text
|
||||
self.exec() # Open the dialog as a modal window
|
|
@ -1,4 +1,5 @@
|
|||
import yaml
|
||||
import os
|
||||
|
||||
class Utilities:
|
||||
def __init__(self):
|
||||
|
@ -20,6 +21,66 @@ class Utilities:
|
|||
except PermissionError as e:
|
||||
print(f"Error saving setings: {e}")
|
||||
|
||||
def program_configs(self):
|
||||
"""Prepear folder for config and generate default exif if non aviable"""
|
||||
program_folder = self._ensure_program_folder_exists()
|
||||
if not os.path.isfile(f"{program_folder}/exif.yaml"):
|
||||
self._default_exif(f"{program_folder}/exif.yaml")
|
||||
|
||||
def _ensure_program_folder_exists(self):
|
||||
program_folder = os.path.expanduser("~/.config/OptimaLab35")
|
||||
print(program_folder)
|
||||
if not os.path.exists(program_folder):
|
||||
print("in not, make folder")
|
||||
os.makedirs(program_folder)
|
||||
return program_folder
|
||||
|
||||
def _default_exif(self, file):
|
||||
"""Makes a default exif file."""
|
||||
print("Making default")
|
||||
def_exif = {
|
||||
"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": [
|
||||
"200",
|
||||
"400",
|
||||
"1600",
|
||||
"3200"
|
||||
],
|
||||
"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"
|
||||
]
|
||||
}
|
||||
self.write_yaml(file, def_exif)
|
||||
|
||||
def append_number_to_name(self, base_name: str, current_image: int, total_images: int, invert: bool):
|
||||
""""Returns name, combination of base_name and ending number."""
|
||||
total_digits = len(str(total_images))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue