All core features aviable
This commit is contained in:
parent
7965b55302
commit
bc69b077b0
9 changed files with 579 additions and 14 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
local_files/
|
||||
debug.*
|
||||
.ropeproject/
|
||||
__pycache__/
|
||||
|
|
60
CHANGELOG.md
Normal file
60
CHANGELOG.md
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Changelog
|
||||
|
||||
## 0.1.x
|
||||
### 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.
|
90
README.md
90
README.md
|
@ -1,18 +1,66 @@
|
|||
# README
|
||||
OPTIMA-35 stands for Organizing, Processing, Tweaking Images, and Modifying Analogs (35mm film scans).
|
||||
# OPTIMA-35
|
||||
|
||||
This project is a soft port of my previous [Bash script collection](https://gitlab.com/sf-bashscripts/analogphotography/-/tree/main), designed to process images (specifically 35mm film scans). It includes functionalities such as renaming, EXIF metadata editing, color adjustment, and more.
|
||||
## Overview
|
||||
|
||||
By transitioning to Python, I aim to transform the script collection into a more cohesive program with enhanced functionality and structure. Additionally, this project serves as a means to strengthen my coding skills and explore Python’s capabilities.
|
||||
# Dependencies
|
||||
- textual
|
||||
- pyyaml
|
||||
- piexif
|
||||
- pillow
|
||||
**OPTIMA-35** (**Organizing, Processing, Tweaking Images and Modifying scanned Analogs from 35mm Film**) is a Python-based project designed to provide a streamlined way to manage and edit metadata and images from analog photography. But can be used for any images.
|
||||
|
||||
Textual will be used for TUI, PyExifTool is used to acces exiftool and Wand is used to interact with ImageMagick.
|
||||
This project is a *port* of my earlier work, an collection of [bash script](https://gitlab.com/sf-bashscripts/analogphotography), transitioning functionality to a more modular and maintainable design.
|
||||
|
||||
Python packages can be installed with pip, exiftool and ImageMagick has to be installed depending on your OS.
|
||||
The primary focus is on building a terminal-based user interface (TUI). Initially, the interface will utilize `simple_term_menu`, with plans to expand to `textual` for a more dynamic TUI experience in the future.
|
||||
|
||||
**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.
|
||||
|
||||
## Key Features
|
||||
|
||||
- Intuitive TUI for organizing and editing metadata and image properties.
|
||||
- Improved modularity with classes split into separate files for flexibility and maintainability.
|
||||
- Supports essential tasks like reading, editing, and saving EXIF data, as well as resizing and processing images.
|
||||
|
||||
**Gif of program in action**
|
||||
|
||||
|
||||

|
||||
|
||||
## Dependencies
|
||||
|
||||
To run **OPTIMA-35**, the following Python libraries are required:
|
||||
|
||||
- **textual**: For building TUI (planned future updates).
|
||||
- **pyyaml**: To handle YAML files for configuration and settings.
|
||||
- **piexif**: To read, modify, and write EXIF metadata.
|
||||
- **Pillow**: For image processing.
|
||||
- **simple\_term\_menu**: For building the initial TUI interface.
|
||||
|
||||
### Installing Dependencies
|
||||
|
||||
You can install the dependencies using `pip`:
|
||||
|
||||
```bash
|
||||
pip install textual pyyaml piexif pillow simple-term-menu
|
||||
```
|
||||
|
||||
Alternatively, you can use `conda` or its alternatives (`anaconda`, `mamba`, `micromamba`):
|
||||
|
||||
```bash
|
||||
conda install -c conda-forge textual pyyaml piexif pillow simple-term-menu
|
||||
```
|
||||
|
||||
## Development Approach
|
||||
|
||||
Compared to my previous project, [FTL Save Manager](https://gitlab.com/python_projects3802849/ftl-save-manager), this project emphasizes:
|
||||
|
||||
- **Enhanced Modularity**: Classes and components are organized into separate files, making the codebase more maintainable and scalable.
|
||||
- **Improved Design Principles**: Focus on creating reusable and flexible code for future expansion.
|
||||
- **Slower Code Pushes**: Updates and code releases will be less frequent but of higher quality, ensuring stability and adherence to best practices.
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
@ -25,10 +73,26 @@ In the interest of transparency, I disclose that Generative AI (GAI) large langu
|
|||
|
||||
In cases where LLMs contribute directly to code or provide substantial optimizations, such contributions will be disclosed and documented in the relevant sections of the codebase.
|
||||
|
||||
Formating for readme not finished yet.
|
||||
mradermacher gguf Q4K-M Instruct version of infly/OpenCoder-1.5B
|
||||
unsloth gguf Q4K_M Instruct version of both Qwen/QWEN2 1.5B and 3B
|
||||
|
||||
### References
|
||||
|
||||
1. **Huang, Siming, et al.**
|
||||
*OpenCoder: The Open Cookbook for Top-Tier Code Large Language Models.*
|
||||
2024. [PDF](https://arxiv.org/pdf/2411.04905)
|
||||
|
||||
|
||||
2. **Hui, Binyuan, et al.**
|
||||
*Qwen2.5-Coder Technical Report.*
|
||||
*arXiv preprint arXiv:2409.12186*, 2024. [arXiv](https://arxiv.org/abs/2409.12186)
|
||||
|
||||
3. **Yang, An, et al.**
|
||||
*Qwen2 Technical Report.*
|
||||
*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},
|
||||
|
@ -37,7 +101,6 @@ unsloth gguf Q4K_M Instruct version of both Qwen/QWEN2 1.5B and 3B
|
|||
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},
|
||||
|
@ -45,7 +108,6 @@ unsloth gguf Q4K_M Instruct version of both Qwen/QWEN2 1.5B and 3B
|
|||
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},
|
||||
|
|
BIN
demo.gif
Normal file
BIN
demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 306 KiB |
27
exif_options.yaml
Normal file
27
exif_options.yaml
Normal file
|
@ -0,0 +1,27 @@
|
|||
make:
|
||||
- Nikon
|
||||
model:
|
||||
- FG
|
||||
- F50
|
||||
lens:
|
||||
- Nikon LENS SERIES E 50mm
|
||||
- AF NIKKOR 35-70mm
|
||||
iso:
|
||||
- "100"
|
||||
- "200"
|
||||
- "400"
|
||||
- "800"
|
||||
- "1000"
|
||||
- "1600"
|
||||
image_description:
|
||||
- ILFORD DELTA 3200
|
||||
- ILFORD ILFOCOLOR
|
||||
- LomoChrome Turquoise
|
||||
user_comment:
|
||||
- "Scanner.NORITSU-KOKI"
|
||||
- "Scanner.NA"
|
||||
artist:
|
||||
- Mr. Finchum
|
||||
copyright_info:
|
||||
- All Rights Reserved
|
||||
- No Copyright
|
105
image_handler.py
Normal file
105
image_handler.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
|
||||
import piexif
|
||||
|
||||
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):
|
||||
enhancer = ImageEnhance.Contrast(image)
|
||||
new_img = enhancer.enhance(1 + (change/100))
|
||||
return new_img
|
||||
|
||||
def change_brightness(self, image, change):
|
||||
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_scale = 70):
|
||||
drawer = ImageDraw.Draw(image)
|
||||
imagewidth, imageheight = image.size
|
||||
margin = (imageheight / 100 ) * 2 # margin dynamic, 2% of image size
|
||||
|
||||
font_size = imagewidth / font_size_scale # Scaling the font size
|
||||
try:
|
||||
font = ImageFont.truetype("OpenDyslexic3-Regular.ttf", font_size)
|
||||
except:
|
||||
print("Error loading font for watermark, exiting...")
|
||||
exit()
|
||||
|
||||
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
|
||||
drawer.text((x, y), text, font = font)
|
||||
|
||||
return image
|
||||
|
||||
def save_image(self, image, path, file_type, jpg_quality, png_compressing, _optimize, exif_data = None):
|
||||
"""Saving images. Needs improvments."""
|
||||
if exif_data != None:
|
||||
if file_type == "jpg":
|
||||
image.save(f"{path}.{file_type.lower()}", quality = jpg_quality, optimize = _optimize, exif = piexif.dump(exif_data))
|
||||
elif file_type == "png":
|
||||
image.save(f"{path}.{file_type.lower()}", compress_level = png_compressing, optimize = _optimize, exif = piexif.dump(exif_data))
|
||||
else:
|
||||
input(f"Type: {file_type} not yet aviable, enter to continue...")
|
||||
else:
|
||||
if file_type == "jpg":
|
||||
image.save(f"{path}.{file_type.lower()}", quality = jpg_quality, optimize = _optimize)
|
||||
elif file_type == "png":
|
||||
image.save(f"{path}.{file_type.lower()}", compress_level = png_compressing, optimize = _optimize)
|
||||
else:
|
||||
input(f"Type: {file_type} not yet aviable, enter to continue...")
|
||||
|
||||
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 user data."""
|
||||
# Mostly made by ChatGPT, some adjustment by Mr Finchum
|
||||
zeroth_ifd = {
|
||||
piexif.ImageIFD.Make: user_data["make"],
|
||||
piexif.ImageIFD.Model: user_data["model"],
|
||||
piexif.ImageIFD.Software: user_data["software"],
|
||||
piexif.ImageIFD.Copyright: user_data["copyright_info"],
|
||||
piexif.ImageIFD.Artist: user_data["artist"],
|
||||
piexif.ImageIFD.ImageDescription: user_data["image_description"],
|
||||
piexif.ImageIFD.XResolution: (72, 1),
|
||||
piexif.ImageIFD.YResolution: (72, 1),
|
||||
}
|
||||
exif_ifd = {
|
||||
piexif.ExifIFD.DateTimeOriginal: user_data["date_time_original"],
|
||||
piexif.ExifIFD.UserComment: user_data["user_comment"],
|
||||
piexif.ExifIFD.ISOSpeedRatings: int(user_data["iso"]),
|
||||
piexif.ExifIFD.PixelXDimension: imagesize[0],
|
||||
piexif.ExifIFD.PixelYDimension: imagesize[1],
|
||||
}
|
||||
return {"0th": zeroth_ifd, "Exif": exif_ifd}
|
201
main.py
Normal file
201
main.py
Normal file
|
@ -0,0 +1,201 @@
|
|||
import os
|
||||
from datetime import datetime
|
||||
#from debug import my_debugging_tools # Removed for main push
|
||||
from utility import Utilities
|
||||
from image_handler import ImageProcessor, ExifHandler
|
||||
from tui import SimpleTUI
|
||||
|
||||
class Optima35:
|
||||
# The layout of class Optima35 was originally made by ChatGPT, but major adjustments have been made. To remain transparent, I disclose this.
|
||||
def __init__(self, settings_file, exif_options_file):
|
||||
self.version = "0.1.0"
|
||||
#self.debugger = my_debugging_tools() # Removed for main push
|
||||
self.utilities = Utilities()
|
||||
self.image_processor = ImageProcessor()
|
||||
self.exif_handler = ExifHandler()
|
||||
self.tui = SimpleTUI()
|
||||
self.settings = {
|
||||
"input_folder": None,
|
||||
"output_folder": None,
|
||||
"file_format": None,
|
||||
"resize_percentage": None,
|
||||
"copy_exif": None,
|
||||
"contrast_percentage": None,
|
||||
"brightness_percentage": None,
|
||||
"new_file_names": None,
|
||||
"invert_image_order": False,
|
||||
"watermark_text": None,
|
||||
"modifications": [],
|
||||
}
|
||||
self.settings_to_save = ["resize_percentage", "jpg_quality", "png_compression", "web_optimize", "contrast_percentage", "brightness_percentage"]
|
||||
self.exif_choices = self.utilities.read_yaml(exif_options_file)
|
||||
self.setting_file = settings_file
|
||||
|
||||
def load_or_ask_settings(self):
|
||||
"""Load settings from a YAML file or ask the user if not present or incomplete."""
|
||||
# Partly ChatGPT
|
||||
if self.read_settings(self.settings_to_save):
|
||||
for item in self.settings_to_save:
|
||||
print(f"{item}: {self.settings[item]}")
|
||||
use_saved = self.tui.yes_no_menu("Use these settings?")
|
||||
if use_saved:
|
||||
return
|
||||
else:
|
||||
print("No settings found...")
|
||||
|
||||
print("Asking for new settings...\n")
|
||||
self.settings["resize_percentage"] = int(input("Default resize percentage: ").strip())
|
||||
self.settings["contrast_percentage"] = int(input("Default contrast percentage: ").strip())
|
||||
self.settings["brightness_percentage"] = int(input("Default brightness percentage: ").strip())
|
||||
self.settings["jpg_quality"] = int(input("JPEG quality (1-100): ").strip())
|
||||
self.settings["png_compression"] = int(input("PNG compression level (0-9): ").strip())
|
||||
self.settings["web_optimize"] = self.tui.yes_no_menu("Optimize images for web?")
|
||||
|
||||
self.write_settings(self.settings_to_save)
|
||||
|
||||
def write_settings(self, keys_to_save):
|
||||
""""Write self.setting, but only specific values"""
|
||||
keys = keys_to_save
|
||||
filtered_settings = {key: self.settings[key] for key in keys if key in self.settings}
|
||||
|
||||
self.utilities.write_yaml(self.setting_file, filtered_settings)
|
||||
print("New settings saved successfully.")
|
||||
|
||||
def read_settings(self, keys_to_load):
|
||||
"""
|
||||
Read settings from the settings file and update self.settings
|
||||
with the values for specific keys without overwriting existing values.
|
||||
"""
|
||||
# First draft by ChatGPT, adjusted to fit my needs.
|
||||
keys = keys_to_load
|
||||
if os.path.exists(self.setting_file):
|
||||
loaded_settings = self.utilities.read_yaml(self.setting_file)
|
||||
|
||||
for key in keys:
|
||||
if key in loaded_settings:
|
||||
self.settings[key] = loaded_settings[key]
|
||||
print("Settings loaded successfully.")
|
||||
return True
|
||||
else:
|
||||
print("Settings file empty.")
|
||||
return False
|
||||
|
||||
def collect_exif_data(self):
|
||||
"""Collect EXIF data based on user input."""
|
||||
user_data = {}
|
||||
fields = [
|
||||
"make", "model", "lens", "iso", "image_description",
|
||||
"user_comment", "artist", "copyright_info"
|
||||
]
|
||||
for field in fields:
|
||||
choise = self.tui.choose_menu(f"Enter {field.replace('_', ' ').title()}", self.exif_choices[field])
|
||||
user_data[field] = choise.encode("utf-8")
|
||||
|
||||
user_data["software"] = f"OPTIMA-35 {self.version}".encode("utf-8")
|
||||
user_data["date_time_original"] = datetime.now().strftime("%Y:%m:%d %H:%M:%S").encode("utf-8")
|
||||
return user_data
|
||||
|
||||
def get_user_settings(self):
|
||||
"""Get initial settings from the user."""
|
||||
menu_options = ["Resize image", "Change EXIF", "Convert to grayscale", "Change contrast", "Change brightness", "Rename images", "Invert image order", "Add Watermark"] # new option can be added here.
|
||||
self.settings["input_folder"] = input("Enter path of input folder: ").strip() # Add: check if folder exists
|
||||
self.settings["output_folder"] = input("Enter path of output folder: ").strip()
|
||||
self.settings["file_format"] = 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["modifications"] = self.tui.multi_select_menu(
|
||||
"Select what you want to do", menu_options
|
||||
)
|
||||
if "Change EXIF" not in self.settings["modifications"]:
|
||||
self.settings["copy_exif"] = self.tui.yes_no_menu("Do you want to copy exif info from original file?")
|
||||
if "Rename images" in self.settings["modifications"]:
|
||||
self.settings["new_file_names"] = input("What should be the name for the new images? ")
|
||||
if "Invert image order" in self.settings["modifications"]:
|
||||
self.settings["invert_image_order"] = True
|
||||
if "Add Watermark" in self.settings["modifications"]:
|
||||
self.settings["watermark_text"] = input("Enter text for watermark. ")
|
||||
|
||||
os.makedirs(self.settings["output_folder"], exist_ok = True)
|
||||
|
||||
def process_images(self):
|
||||
"""Process images based on user settings."""
|
||||
input_folder = self.settings["input_folder"]
|
||||
output_folder = self.settings["output_folder"]
|
||||
|
||||
image_files = [
|
||||
f for f in os.listdir(input_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg'))
|
||||
]
|
||||
|
||||
if "Change EXIF" in self.settings["modifications"]:
|
||||
selected_exif = self.collect_exif_data()
|
||||
i = 1
|
||||
for image_file in image_files:
|
||||
|
||||
input_path = os.path.join(input_folder, image_file)
|
||||
|
||||
if "Rename images" in self.settings["modifications"]:
|
||||
image_name = self.name_images(self.settings["new_file_names"], i, len(image_files), self.settings["invert_image_order"])
|
||||
else:
|
||||
image_name = os.path.splitext(image_file)[0]
|
||||
|
||||
output_path = os.path.join(output_folder, image_name)
|
||||
|
||||
with self.image_processor.open_image(input_path) as img:
|
||||
processed_img = img
|
||||
for mod in self.settings["modifications"]:
|
||||
if mod == "Resize image":
|
||||
processed_img = self.image_processor.resize_image(
|
||||
image = processed_img, percent = self.settings["resize_percentage"], resample = True
|
||||
)
|
||||
elif mod == "Change EXIF" and selected_exif:
|
||||
exif_data = self.exif_handler.build_exif_dict(selected_exif, self.image_processor.get_image_size(processed_img))
|
||||
elif mod == "Convert to grayscale":
|
||||
processed_img = self.image_processor.grayscale(processed_img)
|
||||
elif mod == "Change contrast":
|
||||
processed_img = self.image_processor.change_contrast(processed_img, self.settings["contrast_percentage"])
|
||||
elif mod == "Change brightness":
|
||||
processed_img = self.image_processor.change_brightness(processed_img, self.settings["brightness_percentage"])
|
||||
elif mod == "Add Watermark":
|
||||
processed_img = self.image_processor.add_watermark(processed_img, self.settings["watermark_text"])
|
||||
|
||||
if self.settings["copy_exif"]:
|
||||
# When copying exif from original, make sure to change Piexel X & Y Dimension to fit new size
|
||||
try:
|
||||
og_exif = self.exif_handler.get_exif_info(img)
|
||||
og_exif["Exif"][40962], og_exif["Exif"][40963] = self.image_processor.get_image_size(processed_img)
|
||||
exif_data = og_exif
|
||||
except Exception:
|
||||
# If an error happends it is because the picture does not have exif data
|
||||
print("Copying EXIF data selected, but no EXIF data is available in the original image file.")
|
||||
exif_data = None
|
||||
else:
|
||||
exif_data = None
|
||||
|
||||
self.image_processor.save_image(
|
||||
image = processed_img, path = output_path, exif_data = exif_data,
|
||||
file_type = self.settings["file_format"], jpg_quality = self.settings["jpg_quality"],
|
||||
png_compressing = self.settings["png_compression"], _optimize = self.settings["web_optimize"]
|
||||
)
|
||||
self.utilities.progress_bar(i, len(image_files))
|
||||
i += 1
|
||||
|
||||
def name_images(self, base_name, current_image, total_images, invert):
|
||||
""""Returns name, combination of base_name and ending number."""
|
||||
total_digits = len(str(total_images))
|
||||
|
||||
if invert:
|
||||
ending_number = total_images - (current_image - 1)
|
||||
else:
|
||||
ending_number = current_image
|
||||
|
||||
ending = f"{ending_number:0{total_digits}}"
|
||||
return f"{base_name}_{ending}"
|
||||
|
||||
def run(self):
|
||||
"""Run the main program."""
|
||||
self.load_or_ask_settings()
|
||||
self.get_user_settings()
|
||||
self.process_images()
|
||||
print("Done")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = Optima35("local_files/settings.yaml", "exif_options.yaml")
|
||||
app.run()
|
58
tui.py
Normal file
58
tui.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
from simple_term_menu import TerminalMenu
|
||||
|
||||
class SimpleTUI:
|
||||
"""TUI parts using library simple_term_menu"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def choose_menu(self, menu_title, choices):
|
||||
""" Dynamic function to display content of a list and returnes which was selected."""
|
||||
menu_options = choices
|
||||
menu = TerminalMenu(
|
||||
menu_entries = menu_options,
|
||||
title = menu_title,
|
||||
menu_cursor = "> ",
|
||||
menu_cursor_style = ("fg_gray", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = True,
|
||||
clear_screen = False
|
||||
)
|
||||
menu.show()
|
||||
return menu.chosen_menu_entry
|
||||
|
||||
def multi_select_menu(self, menu_title, choices):
|
||||
""" Dynamic function to display content of a list and returnes which was selected."""
|
||||
menu_options = choices
|
||||
menu = TerminalMenu(
|
||||
menu_entries = menu_options,
|
||||
title = menu_title,
|
||||
multi_select=True,
|
||||
show_multi_select_hint=True,
|
||||
menu_cursor_style = ("fg_gray", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = True,
|
||||
clear_screen = False
|
||||
)
|
||||
menu.show()
|
||||
choisen_values = menu.chosen_menu_entries
|
||||
|
||||
if choisen_values == None:
|
||||
print("Exiting...")
|
||||
exit()
|
||||
else:
|
||||
return menu.chosen_menu_entries
|
||||
|
||||
def yes_no_menu(self, message): # oh
|
||||
menu_options = ["[y] yes", "[n] no"]
|
||||
menu = TerminalMenu(menu_entries = menu_options,
|
||||
title = f"{message}",
|
||||
menu_cursor = "> ",
|
||||
menu_cursor_style = ("fg_red", "bold"),
|
||||
menu_highlight_style = ("bg_gray", "fg_black"),
|
||||
cycle_cursor = True,
|
||||
clear_screen = False)
|
||||
menu_entry_index = menu.show()
|
||||
if menu_entry_index == 0:
|
||||
return True
|
||||
elif menu_entry_index == 1:
|
||||
return False
|
50
utility.py
Normal file
50
utility.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
import yaml
|
||||
|
||||
class Utilities:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def read_yaml(self, yaml_file):
|
||||
try:
|
||||
with open(yaml_file, "r") as file:
|
||||
data = yaml.safe_load(file)
|
||||
return data
|
||||
|
||||
except (FileNotFoundError, PermissionError) as e:
|
||||
print(f"Error loading settings file: {e}")
|
||||
return
|
||||
|
||||
def write_yaml(self, yaml_file, data):
|
||||
try:
|
||||
with open(yaml_file, "w") as file:
|
||||
yaml.dump(data, file)
|
||||
|
||||
except PermissionError as e:
|
||||
print(f"Error saving setings: {e}")
|
||||
|
||||
def yes_no(self, str):
|
||||
"""Ask user y/n question"""
|
||||
while True:
|
||||
choice = input(f"{str} (y/n): ")
|
||||
if choice == "y":
|
||||
return True
|
||||
elif choice == "n":
|
||||
return False
|
||||
else:
|
||||
print("Not a valid option, try again.")
|
||||
|
||||
def progress_bar(self, current, total, barsize = 50):
|
||||
if current > total:
|
||||
print("\033[91mThis bar has exceeded its limits!\033[0m Maybe the current value needs some restraint?")
|
||||
print(f"{(current - total) * '\033[92mHonk, Honk!\033[0m '}")
|
||||
return
|
||||
progress = int((barsize / total) * current)
|
||||
rest = barsize - progress
|
||||
if rest <= 2: rest = 0
|
||||
|
||||
# Determine the number of digits in total
|
||||
total_digits = len(str(total))
|
||||
# Format current with leading zeros
|
||||
current_formatted = f"{current:0{total_digits}}"
|
||||
print(f"{current_formatted}|{progress * '-'}>{rest * ' '}|{total}", end="\r")
|
||||
if current == total: print("")
|
Loading…
Add table
Add a link
Reference in a new issue