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:
Mr Finchum 2025-01-03 09:57:00 +00:00
commit 5f738bdf14
13 changed files with 191 additions and 549 deletions

View file

@ -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.

View file

@ -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 dont 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
View file

@ -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
View file

@ -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()

View file

View file

@ -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 northsouth position coordinate
:param longitude: the eastwest position coordinate
"""
# converts the latitude and longitude coordinates to DMS
latitude_dms = self._deg_to_dms(latitude, ["S", "N"])
longitude_dms = self._deg_to_dms(longitude, ["W", "E"])
# convert the DMS values to EXIF values
exif_latitude = self._dms_to_exif_format(latitude_dms[0], latitude_dms[1], latitude_dms[2])
exif_longitude = self._dms_to_exif_format(longitude_dms[0], longitude_dms[1], longitude_dms[2])
try:
# https://exiftool.org/TagNames/GPS.html
# Create the GPS EXIF data
coordinates = {
piexif.GPSIFD.GPSVersionID: (2, 0, 0, 0),
piexif.GPSIFD.GPSLatitude: exif_latitude,
piexif.GPSIFD.GPSLatitudeRef: latitude_dms[3],
piexif.GPSIFD.GPSLongitude: exif_longitude,
piexif.GPSIFD.GPSLongitudeRef: longitude_dms[3]
}
# Update the EXIF data with the GPS information
exif_data["GPS"] = coordinates
return exif_data
except Exception as e:
print(f"Error: {str(e)}")

View file

@ -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
)

View file

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

35
tui.py
View file

@ -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()

View file

@ -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

View file

@ -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
View 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

View file

@ -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))