Merge branch 'feature/enhance-functions' into 'main'
Enhancing core features See merge request python_projects3802849/optima-35!2
This commit is contained in:
commit
033a021db7
8 changed files with 185 additions and 102 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
local_files/
|
local_files/
|
||||||
debug.*
|
debug.*
|
||||||
|
debug_log/
|
||||||
.ropeproject/
|
.ropeproject/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,6 +1,18 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.1.x
|
## 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
|
### 0.1.0: Core Features Added
|
||||||
- **Images are modified through all selected options without saving, reducing quality degradation and saving local storage.**
|
- **Images are modified through all selected options without saving, reducing quality degradation and saving local storage.**
|
||||||
- **All core features are available:**
|
- **All core features are available:**
|
||||||
|
@ -17,7 +29,6 @@
|
||||||
- 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.
|
- 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.
|
- 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.x
|
||||||
### 0.0.3: Enhanced Functionality - now useable
|
### 0.0.3: Enhanced Functionality - now useable
|
||||||
- **New Image Modification Functions:**
|
- **New Image Modification Functions:**
|
||||||
|
|
44
README.md
44
README.md
|
@ -10,6 +10,14 @@ The primary focus is on building a terminal-based user interface (TUI). Initiall
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
## **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.
|
||||||
|
|
||||||
|
### Available Features:
|
||||||
|
- Initial basic TUI functionality using `simple_term_menu` (planned to switch to a different interface later).
|
||||||
|
- Core features, including image resizing, metadata management, and YAML configuration.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- Intuitive TUI for organizing and editing metadata and image properties.
|
- Intuitive TUI for organizing and editing metadata and image properties.
|
||||||
|
@ -46,22 +54,12 @@ conda install -c conda-forge textual pyyaml piexif pillow simple-term-menu
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Approach
|
## Development Approach
|
||||||
|
|
||||||
Compared to my previous project, [FTL Save Manager](https://gitlab.com/python_projects3802849/ftl-save-manager), this project emphasizes:
|
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.
|
- **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.
|
- **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.
|
- **Slower Code Pushes**: Updates and code releases will be less frequent but of higher quality, ensuring stability and adherence to best practices.
|
||||||
|
|
||||||
## Current Status
|
|
||||||
|
|
||||||
The project is in its early stages, and initial releases will focus on:
|
|
||||||
|
|
||||||
- Basic TUI functionality using `simple_term_menu`.
|
|
||||||
- Core features like image resizing, metadata management, and YAML configuration.
|
|
||||||
|
|
||||||
Stay tuned for updates and more features as development progresses!
|
|
||||||
|
|
||||||
# Use of LLMs
|
# 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.
|
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.
|
||||||
|
|
||||||
|
@ -77,12 +75,10 @@ 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
|
unsloth gguf Q4K_M Instruct version of both Qwen/QWEN2 1.5B and 3B
|
||||||
|
|
||||||
### References
|
### References
|
||||||
|
|
||||||
1. **Huang, Siming, et al.**
|
1. **Huang, Siming, et al.**
|
||||||
*OpenCoder: The Open Cookbook for Top-Tier Code Large Language Models.*
|
*OpenCoder: The Open Cookbook for Top-Tier Code Large Language Models.*
|
||||||
2024. [PDF](https://arxiv.org/pdf/2411.04905)
|
2024. [PDF](https://arxiv.org/pdf/2411.04905)
|
||||||
|
|
||||||
|
|
||||||
2. **Hui, Binyuan, et al.**
|
2. **Hui, Binyuan, et al.**
|
||||||
*Qwen2.5-Coder Technical Report.*
|
*Qwen2.5-Coder Technical Report.*
|
||||||
*arXiv preprint arXiv:2409.12186*, 2024. [arXiv](https://arxiv.org/abs/2409.12186)
|
*arXiv preprint arXiv:2409.12186*, 2024. [arXiv](https://arxiv.org/abs/2409.12186)
|
||||||
|
@ -90,27 +86,3 @@ unsloth gguf Q4K_M Instruct version of both Qwen/QWEN2 1.5B and 3B
|
||||||
3. **Yang, An, et al.**
|
3. **Yang, An, et al.**
|
||||||
*Qwen2 Technical Report.*
|
*Qwen2 Technical Report.*
|
||||||
*arXiv preprint arXiv:2407.10671*, 2024. [arXiv](https://arxiv.org/abs/2407.10671)
|
*arXiv preprint arXiv:2407.10671*, 2024. [arXiv](https://arxiv.org/abs/2407.10671)
|
||||||
|
|
||||||
|
|
||||||
#### Orignal latext cites:
|
|
||||||
|
|
||||||
@inproceedings{Huang2024OpenCoderTO,
|
|
||||||
title={OpenCoder: The Open Cookbook for Top-Tier Code Large Language Models},
|
|
||||||
author={Siming Huang and Tianhao Cheng and Jason Klein Liu and Jiaran Hao and Liuyihan Song and Yang Xu and J. Yang and J. H. Liu and Chenchen Zhang and Linzheng Chai and Ruifeng Yuan and Zhaoxiang Zhang and Jie Fu and Qian Liu and Ge Zhang and Zili Wang and Yuan Qi and Yinghui Xu and Wei Chu},
|
|
||||||
year={2024},
|
|
||||||
url={https://arxiv.org/pdf/2411.04905}
|
|
||||||
}
|
|
||||||
|
|
||||||
@article{hui2024qwen2,
|
|
||||||
title={Qwen2. 5-Coder Technical Report},
|
|
||||||
author={Hui, Binyuan and Yang, Jian and Cui, Zeyu and Yang, Jiaxi and Liu, Dayiheng and Zhang, Lei and Liu, Tianyu and Zhang, Jiajun and Yu, Bowen and Dang, Kai and others},
|
|
||||||
journal={arXiv preprint arXiv:2409.12186},
|
|
||||||
year={2024}
|
|
||||||
}
|
|
||||||
|
|
||||||
@article{qwen2,
|
|
||||||
title={Qwen2 Technical Report},
|
|
||||||
author={An Yang and Baosong Yang and Binyuan Hui and Bo Zheng and Bowen Yu and Chang Zhou and Chengpeng Li and Chengyuan Li and Dayiheng Liu and Fei Huang and Guanting Dong and Haoran Wei and Huan Lin and Jialong Tang and Jialin Wang and Jian Yang and Jianhong Tu and Jianwei Zhang and Jianxin Ma and Jin Xu and Jingren Zhou and Jinze Bai and Jinzheng He and Junyang Lin and Kai Dang and Keming Lu and Keqin Chen and Kexin Yang and Mei Li and Mingfeng Xue and Na Ni and Pei Zhang and Peng Wang and Ru Peng and Rui Men and Ruize Gao and Runji Lin and Shijie Wang and Shuai Bai and Sinan Tan and Tianhang Zhu and Tianhao Li and Tianyu Liu and Wenbin Ge and Xiaodong Deng and Xiaohuan Zhou and Xingzhang Ren and Xinyu Zhang and Xipin Wei and Xuancheng Ren and Yang Fan and Yang Yao and Yichang Zhang and Yu Wan and Yunfei Chu and Yuqiong Liu and Zeyu Cui and Zhenru Zhang and Zhihao Fan},
|
|
||||||
journal={arXiv preprint arXiv:2407.10671},
|
|
||||||
year={2024}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,20 +6,21 @@ model:
|
||||||
lens:
|
lens:
|
||||||
- Nikon LENS SERIES E 50mm
|
- Nikon LENS SERIES E 50mm
|
||||||
- AF NIKKOR 35-70mm
|
- AF NIKKOR 35-70mm
|
||||||
iso:
|
iso: # Numeric values cause errors with simple_term_menu and must be quoted. This issue will be resolved with the future UI switch.
|
||||||
- "100"
|
- "100"
|
||||||
- "200"
|
- "200"
|
||||||
- "400"
|
- "400"
|
||||||
- "800"
|
- "800"
|
||||||
- "1000"
|
- "1000"
|
||||||
- "1600"
|
- "1600"
|
||||||
|
- "3200"
|
||||||
image_description:
|
image_description:
|
||||||
- ILFORD DELTA 3200
|
- ILFORD DELTA 3200
|
||||||
- ILFORD ILFOCOLOR
|
- ILFORD ILFOCOLOR
|
||||||
- LomoChrome Turquoise
|
- LomoChrome Turquoise
|
||||||
user_comment:
|
user_comment:
|
||||||
- "Scanner.NORITSU-KOKI"
|
- Scanner.NORITSU-KOKI
|
||||||
- "Scanner.NA"
|
- Scanner.NA
|
||||||
artist:
|
artist:
|
||||||
- Mr. Finchum
|
- Mr. Finchum
|
||||||
copyright_info:
|
copyright_info:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
|
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
|
||||||
import piexif
|
import piexif
|
||||||
|
import time
|
||||||
|
|
||||||
class ImageProcessor:
|
class ImageProcessor:
|
||||||
"""Functions using pillow are in here."""
|
"""Functions using pillow are in here."""
|
||||||
|
@ -41,13 +42,13 @@ class ImageProcessor:
|
||||||
drawer = ImageDraw.Draw(image)
|
drawer = ImageDraw.Draw(image)
|
||||||
imagewidth, imageheight = image.size
|
imagewidth, imageheight = image.size
|
||||||
margin = (imageheight / 100 ) * 2 # margin dynamic, 2% of 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_scale # Scaling the font size
|
||||||
try:
|
try: # Try loading front, if notaviable return unmodified image
|
||||||
font = ImageFont.truetype("OpenDyslexic3-Regular.ttf", font_size)
|
font = ImageFont.truetype("OpenDyslexic3-Regular.ttf", font_size)
|
||||||
except:
|
except:
|
||||||
print("Error loading font for watermark, exiting...")
|
print("Error loading font for watermark, please ensure font is installed...\n")
|
||||||
exit()
|
time.sleep(0.3)
|
||||||
|
return image
|
||||||
|
|
||||||
c, w, textwidth, textheight, = drawer.textbbox(xy = (0, 0), text = text, font = font) # Getting text size, only need the last two values
|
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
|
x = imagewidth - textwidth - margin
|
||||||
|
@ -56,26 +57,34 @@ class ImageProcessor:
|
||||||
|
|
||||||
return image
|
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 = None):
|
||||||
"""Saving images. Needs improvments."""
|
# partly optimized by chatGPT
|
||||||
if exif_data != None:
|
"""
|
||||||
if file_type == "jpg":
|
Save an image to the specified path with optional EXIF data and optimization.
|
||||||
image.save(f"{path}.{file_type.lower()}", quality = jpg_quality, optimize = _optimize, exif = piexif.dump(exif_data))
|
"""
|
||||||
elif file_type == "png":
|
file_type = file_type.lower()
|
||||||
image.save(f"{path}.{file_type.lower()}", compress_level = png_compressing, optimize = _optimize, exif = piexif.dump(exif_data))
|
save_params = {"optimize": optimize}
|
||||||
else:
|
# Add file-specific parameters
|
||||||
input(f"Type: {file_type} not yet aviable, enter to continue...")
|
if file_type == "jpg":
|
||||||
else:
|
save_params["quality"] = jpg_quality
|
||||||
if file_type == "jpg":
|
elif file_type == "png":
|
||||||
image.save(f"{path}.{file_type.lower()}", quality = jpg_quality, optimize = _optimize)
|
save_params["compress_level"] = png_compressing
|
||||||
elif file_type == "png":
|
elif file_type not in ["webp", "jpg", "png"]:
|
||||||
image.save(f"{path}.{file_type.lower()}", compress_level = png_compressing, optimize = _optimize)
|
input(f"Type: {file_type} is not supported. Press Enter to continue...")
|
||||||
else:
|
return
|
||||||
input(f"Type: {file_type} not yet aviable, enter to continue...")
|
# Add EXIF data if available
|
||||||
|
if exif_data is not None:
|
||||||
|
save_params["exif"] = piexif.dump(exif_data)
|
||||||
|
if file_type == "webp":
|
||||||
|
print("File format webp does not support all exif features, some information might get lost...\n")
|
||||||
|
time.sleep(0.1)
|
||||||
|
try:
|
||||||
|
image.save(f"{path}.{file_type}", **save_params)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to save image: {e}")
|
||||||
|
|
||||||
class ExifHandler:
|
class ExifHandler:
|
||||||
"""Function using piexif are here."""
|
"""Function using piexif are here."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -84,7 +93,7 @@ class ExifHandler:
|
||||||
|
|
||||||
def build_exif_dict(self, user_data, imagesize):
|
def build_exif_dict(self, user_data, imagesize):
|
||||||
"""Build a piexif-compatible EXIF dictionary from user data."""
|
"""Build a piexif-compatible EXIF dictionary from user data."""
|
||||||
# Mostly made by ChatGPT, some adjustment by Mr Finchum
|
# Mostly made by ChatGPT, some adjustment
|
||||||
zeroth_ifd = {
|
zeroth_ifd = {
|
||||||
piexif.ImageIFD.Make: user_data["make"],
|
piexif.ImageIFD.Make: user_data["make"],
|
||||||
piexif.ImageIFD.Model: user_data["model"],
|
piexif.ImageIFD.Model: user_data["model"],
|
||||||
|
@ -96,10 +105,11 @@ class ExifHandler:
|
||||||
piexif.ImageIFD.YResolution: (72, 1),
|
piexif.ImageIFD.YResolution: (72, 1),
|
||||||
}
|
}
|
||||||
exif_ifd = {
|
exif_ifd = {
|
||||||
piexif.ExifIFD.DateTimeOriginal: user_data["date_time_original"],
|
|
||||||
piexif.ExifIFD.UserComment: user_data["user_comment"],
|
piexif.ExifIFD.UserComment: user_data["user_comment"],
|
||||||
piexif.ExifIFD.ISOSpeedRatings: int(user_data["iso"]),
|
piexif.ExifIFD.ISOSpeedRatings: int(user_data["iso"]),
|
||||||
piexif.ExifIFD.PixelXDimension: imagesize[0],
|
piexif.ExifIFD.PixelXDimension: imagesize[0],
|
||||||
piexif.ExifIFD.PixelYDimension: imagesize[1],
|
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}
|
return {"0th": zeroth_ifd, "Exif": exif_ifd}
|
||||||
|
|
147
main.py
147
main.py
|
@ -1,6 +1,6 @@
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
#from debug import my_debugging_tools # Removed for main push
|
|
||||||
from utility import Utilities
|
from utility import Utilities
|
||||||
from image_handler import ImageProcessor, ExifHandler
|
from image_handler import ImageProcessor, ExifHandler
|
||||||
from tui import SimpleTUI
|
from tui import SimpleTUI
|
||||||
|
@ -8,8 +8,7 @@ from tui import SimpleTUI
|
||||||
class Optima35:
|
class Optima35:
|
||||||
# The layout of class Optima35 was originally made by ChatGPT, but major adjustments have been made. To remain transparent, I disclose this.
|
# 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):
|
def __init__(self, settings_file, exif_options_file):
|
||||||
self.version = "0.1.0"
|
self.version = "0.1.1"
|
||||||
#self.debugger = my_debugging_tools() # Removed for main push
|
|
||||||
self.utilities = Utilities()
|
self.utilities = Utilities()
|
||||||
self.image_processor = ImageProcessor()
|
self.image_processor = ImageProcessor()
|
||||||
self.exif_handler = ExifHandler()
|
self.exif_handler = ExifHandler()
|
||||||
|
@ -27,13 +26,20 @@ class Optima35:
|
||||||
"watermark_text": None,
|
"watermark_text": None,
|
||||||
"modifications": [],
|
"modifications": [],
|
||||||
}
|
}
|
||||||
self.settings_to_save = ["resize_percentage", "jpg_quality", "png_compression", "web_optimize", "contrast_percentage", "brightness_percentage"]
|
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.exif_choices = self.utilities.read_yaml(exif_options_file)
|
||||||
self.setting_file = settings_file
|
self.setting_file = settings_file
|
||||||
|
|
||||||
def load_or_ask_settings(self):
|
def load_or_ask_settings(self):
|
||||||
"""Load settings from a YAML file or ask the user if not present or incomplete."""
|
"""Load settings from a YAML file or ask the user if not present or incomplete."""
|
||||||
# Partly ChatGPT
|
# Partially ChatGPT
|
||||||
if self.read_settings(self.settings_to_save):
|
if self.read_settings(self.settings_to_save):
|
||||||
for item in self.settings_to_save:
|
for item in self.settings_to_save:
|
||||||
print(f"{item}: {self.settings[item]}")
|
print(f"{item}: {self.settings[item]}")
|
||||||
|
@ -44,12 +50,12 @@ class Optima35:
|
||||||
print("No settings found...")
|
print("No settings found...")
|
||||||
|
|
||||||
print("Asking for new settings...\n")
|
print("Asking for new settings...\n")
|
||||||
self.settings["resize_percentage"] = int(input("Default resize percentage: ").strip())
|
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"] = int(input("Default contrast percentage: ").strip())
|
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"] = int(input("Default brightness percentage: ").strip())
|
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"] = int(input("JPEG quality (1-100): ").strip())
|
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"] = int(input("PNG compression level (0-9): ").strip())
|
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 for web?")
|
self.settings["web_optimize"] = self.tui.yes_no_menu("Optimize images i.e. compressing?")
|
||||||
|
|
||||||
self.write_settings(self.settings_to_save)
|
self.write_settings(self.settings_to_save)
|
||||||
|
|
||||||
|
@ -57,7 +63,6 @@ class Optima35:
|
||||||
""""Write self.setting, but only specific values"""
|
""""Write self.setting, but only specific values"""
|
||||||
keys = keys_to_save
|
keys = keys_to_save
|
||||||
filtered_settings = {key: self.settings[key] for key in keys if key in self.settings}
|
filtered_settings = {key: self.settings[key] for key in keys if key in self.settings}
|
||||||
|
|
||||||
self.utilities.write_yaml(self.setting_file, filtered_settings)
|
self.utilities.write_yaml(self.setting_file, filtered_settings)
|
||||||
print("New settings saved successfully.")
|
print("New settings saved successfully.")
|
||||||
|
|
||||||
|
@ -70,7 +75,6 @@ class Optima35:
|
||||||
keys = keys_to_load
|
keys = keys_to_load
|
||||||
if os.path.exists(self.setting_file):
|
if os.path.exists(self.setting_file):
|
||||||
loaded_settings = self.utilities.read_yaml(self.setting_file)
|
loaded_settings = self.utilities.read_yaml(self.setting_file)
|
||||||
|
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if key in loaded_settings:
|
if key in loaded_settings:
|
||||||
self.settings[key] = loaded_settings[key]
|
self.settings[key] = loaded_settings[key]
|
||||||
|
@ -92,43 +96,107 @@ class Optima35:
|
||||||
user_data[field] = choise.encode("utf-8")
|
user_data[field] = choise.encode("utf-8")
|
||||||
|
|
||||||
user_data["software"] = f"OPTIMA-35 {self.version}".encode("utf-8")
|
user_data["software"] = f"OPTIMA-35 {self.version}".encode("utf-8")
|
||||||
user_data["date_time_original"] = datetime.now().strftime("%Y:%m:%d %H:%M:%S").encode("utf-8")
|
new_date = self.get_date_input()
|
||||||
|
|
||||||
|
if new_date:
|
||||||
|
user_data["date_time_original"] = new_date
|
||||||
|
|
||||||
return user_data
|
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):
|
def get_user_settings(self):
|
||||||
"""Get initial settings from the user."""
|
"""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.
|
menu_options = [
|
||||||
self.settings["input_folder"] = input("Enter path of input folder: ").strip() # Add: check if folder exists
|
"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["output_folder"] = input("Enter path of output folder: ").strip()
|
||||||
self.settings["file_format"] = input("Enter export file format (e.g., jpg, png): ").strip() # Add: specific question depending on export file type, like jpg quality and png compression
|
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(
|
self.settings["modifications"] = self.tui.multi_select_menu(
|
||||||
"Select what you want to do", menu_options
|
"Select what you want to do", menu_options
|
||||||
)
|
)
|
||||||
if "Change EXIF" not in self.settings["modifications"]:
|
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?")
|
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"]:
|
if "Rename images" in self.settings["modifications"]:
|
||||||
self.settings["new_file_names"] = input("What should be the name for the new images? ")
|
self.settings["new_file_names"] = input("What should be the name for the new images? ") # Need
|
||||||
if "Invert image order" in self.settings["modifications"]:
|
if "Invert image order" in self.settings["modifications"]:
|
||||||
self.settings["invert_image_order"] = True
|
self.settings["invert_image_order"] = True
|
||||||
if "Add Watermark" in self.settings["modifications"]:
|
if "Add Watermark" in self.settings["modifications"]:
|
||||||
self.settings["watermark_text"] = input("Enter text for watermark. ")
|
self.settings["watermark_text"] = input("Enter text for watermark. ")
|
||||||
|
|
||||||
os.makedirs(self.settings["output_folder"], exist_ok = True)
|
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):
|
def process_images(self):
|
||||||
"""Process images based on user settings."""
|
"""Process images based on user settings."""
|
||||||
input_folder = self.settings["input_folder"]
|
input_folder = self.settings["input_folder"]
|
||||||
output_folder = self.settings["output_folder"]
|
output_folder = self.settings["output_folder"]
|
||||||
|
|
||||||
image_files = [
|
image_files = [
|
||||||
f for f in os.listdir(input_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg'))
|
f for f in os.listdir(input_folder) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp"))
|
||||||
]
|
]
|
||||||
|
|
||||||
if "Change EXIF" in self.settings["modifications"]:
|
if "Change EXIF" in self.settings["modifications"]:
|
||||||
selected_exif = self.collect_exif_data()
|
selected_exif = self.collect_exif_data()
|
||||||
i = 1
|
i = 1
|
||||||
for image_file in image_files:
|
for image_file in image_files:
|
||||||
|
|
||||||
input_path = os.path.join(input_folder, image_file)
|
input_path = os.path.join(input_folder, image_file)
|
||||||
|
|
||||||
if "Rename images" in self.settings["modifications"]:
|
if "Rename images" in self.settings["modifications"]:
|
||||||
|
@ -146,6 +214,8 @@ class Optima35:
|
||||||
image = processed_img, percent = self.settings["resize_percentage"], resample = True
|
image = processed_img, percent = self.settings["resize_percentage"], resample = True
|
||||||
)
|
)
|
||||||
elif mod == "Change EXIF" and selected_exif:
|
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))
|
exif_data = self.exif_handler.build_exif_dict(selected_exif, self.image_processor.get_image_size(processed_img))
|
||||||
elif mod == "Convert to grayscale":
|
elif mod == "Convert to grayscale":
|
||||||
processed_img = self.image_processor.grayscale(processed_img)
|
processed_img = self.image_processor.grayscale(processed_img)
|
||||||
|
@ -166,13 +236,17 @@ class Optima35:
|
||||||
# If an error happends it is because the picture does not have exif data
|
# 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.")
|
print("Copying EXIF data selected, but no EXIF data is available in the original image file.")
|
||||||
exif_data = None
|
exif_data = None
|
||||||
else:
|
elif "Change EXIF" not in self.settings["modifications"]:
|
||||||
exif_data = None
|
exif_data = None
|
||||||
|
|
||||||
self.image_processor.save_image(
|
self.image_processor.save_image(
|
||||||
image = processed_img, path = output_path, exif_data = exif_data,
|
image = processed_img,
|
||||||
file_type = self.settings["file_format"], jpg_quality = self.settings["jpg_quality"],
|
path = output_path,
|
||||||
png_compressing = self.settings["png_compression"], _optimize = self.settings["web_optimize"]
|
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))
|
self.utilities.progress_bar(i, len(image_files))
|
||||||
i += 1
|
i += 1
|
||||||
|
@ -180,15 +254,30 @@ class Optima35:
|
||||||
def name_images(self, base_name, current_image, total_images, invert):
|
def name_images(self, base_name, current_image, total_images, invert):
|
||||||
""""Returns name, combination of base_name and ending number."""
|
""""Returns name, combination of base_name and ending number."""
|
||||||
total_digits = len(str(total_images))
|
total_digits = len(str(total_images))
|
||||||
|
|
||||||
if invert:
|
if invert:
|
||||||
ending_number = total_images - (current_image - 1)
|
ending_number = total_images - (current_image - 1)
|
||||||
else:
|
else:
|
||||||
ending_number = current_image
|
ending_number = current_image
|
||||||
|
|
||||||
ending = f"{ending_number:0{total_digits}}"
|
ending = f"{ending_number:0{total_digits}}"
|
||||||
return f"{base_name}_{ending}"
|
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):
|
def run(self):
|
||||||
"""Run the main program."""
|
"""Run the main program."""
|
||||||
self.load_or_ask_settings()
|
self.load_or_ask_settings()
|
||||||
|
@ -197,5 +286,5 @@ class Optima35:
|
||||||
print("Done")
|
print("Done")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = Optima35("local_files/settings.yaml", "exif_options.yaml")
|
app = Optima35("settings.yaml", "exif_options.yaml")
|
||||||
app.run()
|
app.run()
|
||||||
|
|
16
tui.py
16
tui.py
|
@ -44,13 +44,15 @@ class SimpleTUI:
|
||||||
|
|
||||||
def yes_no_menu(self, message): # oh
|
def yes_no_menu(self, message): # oh
|
||||||
menu_options = ["[y] yes", "[n] no"]
|
menu_options = ["[y] yes", "[n] no"]
|
||||||
menu = TerminalMenu(menu_entries = menu_options,
|
menu = TerminalMenu(
|
||||||
title = f"{message}",
|
menu_entries = menu_options,
|
||||||
menu_cursor = "> ",
|
title = f"{message}",
|
||||||
menu_cursor_style = ("fg_red", "bold"),
|
menu_cursor = "> ",
|
||||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
menu_cursor_style = ("fg_red", "bold"),
|
||||||
cycle_cursor = True,
|
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||||
clear_screen = False)
|
cycle_cursor = True,
|
||||||
|
clear_screen = False
|
||||||
|
)
|
||||||
menu_entry_index = menu.show()
|
menu_entry_index = menu.show()
|
||||||
if menu_entry_index == 0:
|
if menu_entry_index == 0:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -9,7 +9,6 @@ class Utilities:
|
||||||
with open(yaml_file, "r") as file:
|
with open(yaml_file, "r") as file:
|
||||||
data = yaml.safe_load(file)
|
data = yaml.safe_load(file)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except (FileNotFoundError, PermissionError) as e:
|
except (FileNotFoundError, PermissionError) as e:
|
||||||
print(f"Error loading settings file: {e}")
|
print(f"Error loading settings file: {e}")
|
||||||
return
|
return
|
||||||
|
@ -18,7 +17,6 @@ class Utilities:
|
||||||
try:
|
try:
|
||||||
with open(yaml_file, "w") as file:
|
with open(yaml_file, "w") as file:
|
||||||
yaml.dump(data, file)
|
yaml.dump(data, file)
|
||||||
|
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
print(f"Error saving setings: {e}")
|
print(f"Error saving setings: {e}")
|
||||||
|
|
||||||
|
@ -41,7 +39,6 @@ class Utilities:
|
||||||
progress = int((barsize / total) * current)
|
progress = int((barsize / total) * current)
|
||||||
rest = barsize - progress
|
rest = barsize - progress
|
||||||
if rest <= 2: rest = 0
|
if rest <= 2: rest = 0
|
||||||
|
|
||||||
# Determine the number of digits in total
|
# Determine the number of digits in total
|
||||||
total_digits = len(str(total))
|
total_digits = len(str(total))
|
||||||
# Format current with leading zeros
|
# Format current with leading zeros
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue