Compare commits

..

49 commits

Author SHA1 Message Date
daba110805
patch: adjusting pip readme.
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 13:17:54 +02:00
9869a9e419
patch: added missing entry.
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 13:09:48 +02:00
9535b869af
fix: fixes version insection
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 13:03:08 +02:00
41d81c4f7c
ci: switching from gitlab-ci to woodpecker.
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 12:51:26 +02:00
e2cef24b65 patch: adjusted pyproject.toml 2025-03-24 15:24:14 +01:00
3233b92fba patch: Added classifiers for pypi 2025-02-09 13:21:29 +01:00
4f7fc53c92 patch: Adding contribution section to readme 2025-01-28 15:29:02 +00:00
dc32740309 refactor!: Stable release for optima35 2025-01-28 15:06:29 +00:00
8f539d4097 fix: Fixed missing lens and incorrect user comment bug. 2025-01-23 14:56:13 +01:00
8064db6aec fix: fixes changelog
Added jump from version in the changelog.
2025-01-21 19:45:24 +00:00
a4ca23145e chore: more pipeline 2025-01-21 19:37:14 +00:00
9328a59cd5 chore: fixes pipeline automatic versioning
Last automatic versioning mistake should had been due to wrong merge titles.
2025-01-21 19:29:26 +00:00
350940a328 chore: fixes version numbering from pipeline
Adjusted settings of repo that versioning can be determed automaticly correctly.
2025-01-21 19:17:37 +00:00
46d79bf542 Merge branch 'feature/enhancing' into 'main'
feat: BREAKING GPS now must be float

See merge request CodeByMrFinchum/optima35!15
2025-01-21 19:05:48 +00:00
37831e1c02 feat: BREAKING GPS now must be float 2025-01-21 19:05:48 +00:00
8e56565b29 Merge branch 'fix/pipline' into 'main'
fix: fixes pipline

See merge request CodeByMrFinchum/optima35!14
2025-01-21 18:49:13 +00:00
1a9f05998b fix: fixes pipline 2025-01-21 18:49:13 +00:00
5070a82e51 Merge branch 'feature/release_pipline' into 'main'
feat: Adds pipline automation

See merge request CodeByMrFinchum/optima35!13
2025-01-21 18:42:22 +00:00
f0e357e940 feat: Adds pipline automation
Adds pipline for versioning, buildign and publishing to pypi.
2025-01-21 18:42:22 +00:00
a737d09e7c Merge branch 'main' into 'main'
ci: adds version calculation via gitversion and autotagging on defaultbranch

See merge request CodeByMrFinchum/optima35!12
2025-01-14 19:30:55 +00:00
Mr. Finch
ea1812f0f1 ci: adds version calculation via gitversion and autotagging on defaultbranch 2025-01-14 19:30:55 +00:00
4f66c686d1 Merge branch 'preserve/current-version' into 'main'
Making a version known to work with OptimaLab35 v0.1.0

See merge request CodeByMrFinchum/optima35!11
2025-01-03 16:27:23 +00:00
c2f4cfa9b7 Making a version known to work with OptimaLab35 v0.1.0 2025-01-03 17:18:20 +01:00
a19d3d8ed4 Merge branch 'refactor/package-structure' into 'main'
Refactor/package structure

See merge request CodeByMrFinchum/optima35!10
2025-01-03 12:59:41 +00:00
ca79e7c944 fixed formation 2025-01-03 13:51:18 +01:00
33e8334566 __main__ which is started if users starts optima35 without ui. 2025-01-03 13:48:15 +01:00
d85c796dcc v0.6.3-a2, added __version__, version in pypi is now dynamic. 2025-01-03 13:47:28 +01:00
f22ca680ca Oops forgot to update the version nr in core... 2025-01-03 12:49:04 +01:00
cf256374ef Adjusted README 2025-01-03 12:45:09 +01:00
798169234c v0.6.3-a1 2025-01-03 12:44:53 +01:00
8f1e8b8f9e Moved all code to src folder 2025-01-03 12:42:41 +01:00
b9b3c8f3a9 Adjust for new folder stucture. 2025-01-03 12:37:54 +01:00
a0483f491a Merge branch 'refactor/standalone-package' into 'main'
Refactor/standalone package

See merge request CodeByMrFinchum/optima-35!9
2025-01-03 10:00:38 +00:00
f5e7d52b26 Now optima35 is on pip. 2025-01-03 10:59:53 +01:00
cefc4a19a8 Adjusted requirements now that the ui is seperated. 2025-01-02 18:53:48 +01:00
484e0eb198 v0.6.0 2025-01-02 17:08:04 +01:00
53286817d5 Stripping alot of content, everything regarding UI. 2025-01-02 17:07:47 +01:00
9930488581 Changing naming scene to fit with package building 2025-01-02 17:07:20 +01:00
eb71883761 Removing all files not needed to have a optima35 packge 2025-01-02 17:06:36 +01:00
d74c6a2176 Removing all UI elements 2025-01-02 17:02:08 +01:00
7af3191585 Merge branch 'chore/optimizing' into 'main'
Chore/optimizing

See merge request CodeByMrFinchum/optima-35!8
2025-01-02 14:20:44 +00:00
94a008a26b v0.5.0, prepearing to split project. 2025-01-02 15:11:42 +01:00
46ab11847c Moved a function to this class, more fitting. 2025-01-02 15:11:31 +01:00
ad62200bb5 Now works with optima35 0.5.0 2025-01-02 15:11:15 +01:00
1bc135e8c3 Made easier to use for other. Giving parameters instead of using local dict. 2025-01-02 15:10:53 +01:00
7ef1960ced Moved image handler to optima folder. 2025-01-02 15:10:06 +01:00
5ae4c8f9ef Changes because used in program.. 2025-01-02 15:09:35 +01:00
fc084b2cad Removed restart button which was used for debugging. 2025-01-02 15:09:04 +01:00
5182bd6705 Prepearing to split UI from optima35 repo. 2025-01-02 15:08:42 +01:00
32 changed files with 530 additions and 2958 deletions

7
.gitignore vendored
View file

@ -1,7 +1,4 @@
local_files/ test/
debug.* dist/
debug_log/
.ropeproject/ .ropeproject/
__pycache__/ __pycache__/
config/tui_settings.yaml
config/exif.yaml

View file

@ -0,0 +1,92 @@
steps:
- name: gitversion
depends_on: [] # nothing start emititly
when:
event: push
branch: main
image: mcr.microsoft.com/dotnet/sdk:9.0
environment:
CI_TOKEN:
from_secret: CI_TOKEN
commands:
- git remote set-url origin https://CodeByMrFinchum:$CI_TOKEN@code.boxyfoxy.net/$CI_REPO.git
- git fetch --unshallow --tags
- apt-get update && apt-get install -y jq
- dotnet tool install --global GitVersion.Tool --version 5.*
- export PATH="$PATH:/root/.dotnet/tools"
- dotnet-gitversion -output json > version.json
- ls
- cat version.json
- |
echo "GitVersion_SemVer=$(jq -r '.SemVer' version.json)" >> gitversion.env
echo "GitVersion_LegacySemVer=$(jq -r '.LegacySemVer' version.json)" >> gitversion.env
echo "GitVersion_FullSemVer=$(jq -r '.FullSemVer' version.json)" >> gitversion.env
echo "GitVersion_Major=$(jq -r '.Major' version.json)" >> gitversion.env
echo "GitVersion_Minor=$(jq -r '.Minor' version.json)" >> gitversion.env
echo "GitVersion_Patch=$(jq -r '.Patch' version.json)" >> gitversion.env
echo "GitVersion_MajorMinorPatch=$(jq -r '.MajorMinorPatch' version.json)" >> gitversion.env
echo "GitVersion_BuildMetaData=$(jq -r '.BuildMetaData' version.json)" >> gitversion.env
- name: tagging
depends_on: [gitversion]
when:
event: push
branch: main
image: alpine/git
environment:
CI_TOKEN:
from_secret: CI_TOKEN
commands:
- ls
- cat gitversion.env
- git config --global user.email "ci@noreply.boxyfoxy.net"
- git config --global user.name "CI Bot"
- git remote set-url origin https://CodeByMrFinchum:$${CI_TOKEN}@code.boxyfoxy.net/$${CI_REPO}.git
- . gitversion.env
- git tag $GitVersion_SemVer
- git push origin tag $GitVersion_SemVer
- name: build
depends_on: [gitversion, tagging]
when:
event: push
branch: main
image: python:3.9.21
commands:
- ls
- cat gitversion.env
- export $(cat gitversion.env | xargs)
- sed -i "s/1.0.0/$GitVersion_SemVer/" src/optima35/__init__.py
- cat src/optima35/__init__.py
- python3 -m pip install build
- python3 -m build
- name: publish_pypi
depends_on: [gitversion, tagging, build]
when:
event: push
branch: main
image: python:3.9.21
environment:
TWINE_PASSWORD:
from_secret: TWINE_API
TWINE_USERNAME: "__token__"
commands:
- ls
- python3 -m pip install twine
- python3 -m twine upload dist/*
- name: publish_forgejo
depends_on: [gitversion, tagging, build]
when:
event: push
branch: main
image: python:3.9.21
environment:
TWINE_PASSWORD:
from_secret: PKG_TOKEN
TWINE_USERNAME: "CodeByMrFinchum"
commands:
- ls
- python3 -m pip install twine
- python3 -m twine upload --repository-url https://code.boxyfoxy.net/api/packages/CodeByMrFinchum/pypi dist/*

View file

@ -1,5 +1,91 @@
# Changelog # Changelog
## 1.1.x: Migration (25.04.11)
- Migrated from GitLab to my forgejo instance for development.
---
## 1.0.x
### 1.0.3 Patch: Adjusted pyproject.toml (25.03.24)
- Added <4.0 python version
### 1.0.2: Added Classifier (25.02.09)
- Added classifiers in `pyproject.toml` for PyPI.
### 1.0.1: Contribution (25.01.28)
- Added a contribution section in the README for Mr. Finch.
### **1.0.0** Refactoring (25.01.28)
- Added function descriptions for better clarity and maintainability.
- Introduced guidelines for each function, defining objectives and expected behavior.
---
## 0.12.x
### 0.12.2: Bug fixes
- Fixed missing lens in meta data
- Fixed incorrect User Comment, aka, Scanner name
### 0.12.1
- Error in GitVersion.yml file resulted in jump from 6 to 12.
### 0.12.0
- Versioning from pipeline.
---
## 0.7.x
### 0.7.0
- **BREAKING CHANGE:** GPS location must now be provided as a float instead of a string.
- Repo only: Pipline
---
## 0.6.x
### 0.6.8
- Repo only: Pipline
### 0.6.6
- Added function to insert exif data into image file (i.e. without modifying image)
### 0.6.5 / -a
- No breaking changes to backward compatibility yet.
- Updated the `process` function: an image can now be returned in a modified form without saving. It is returned as a Qt image, which is required for the new UI functionality.
- No change from alpha to *stable*
### 0.6.4
- Released a stable-ish version to ensure compatibility with the current GUI in OptimaLab35 (v0.1.0).
- This version serves as a baseline before potential breaking changes in future updates.
### 0.6.3-a2
- Adding __version__ to `__init__.py` so version is automaticly updated in program as well as pypi.
### 0.6.3-a1
- Adding postfix a to indicate alpha version, making it clear that it is in an early state
### 0.6.2
- Version on pypi.
- .1 and .2 have no change, but had to republish the file, which required to increase the version number.
### 0.6.0
- Working on to Publish on pypi
- Renaming of files and classes
---
## 0.5.x
### 0.5.0
### **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.x
### 0.4.1: Finished GUI and TUI ### 0.4.1: Finished GUI and TUI
- Both **GUI** and **TUI** now fully utilize the `optima35` class for core functionality. - Both **GUI** and **TUI** now fully utilize the `optima35` class for core functionality.
@ -25,6 +111,8 @@
- Improved readability, maintainability, and scalability of the project. - Improved readability, maintainability, and scalability of the project.
- Easier to test and debug individual components. - Easier to test and debug individual components.
---
## 0.3.x ## 0.3.x
### 0.3.4: Features Finalized ### 0.3.4: Features Finalized
- Core Features Completed: - Core Features Completed:
@ -71,6 +159,8 @@
- Watermark is still in testing / alpha - 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. - 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.x
### 0.2.1: Merge from TUI fork ### 0.2.1: Merge from TUI fork
- Ensure watermark is white with black borders. - Ensure watermark is white with black borders.
@ -79,6 +169,8 @@
- **Cleaner folder structure** - **Cleaner folder structure**
- Moving files with classes to different folder to keep project cleaner. - Moving files with classes to different folder to keep project cleaner.
---
## 0.1.x ## 0.1.x
### 0.1.1 ### 0.1.1
- **Add Original to add Timestamp to Images** - **Add Original to add Timestamp to Images**
@ -108,6 +200,8 @@
- 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:**

5
GitVersion.yml Normal file
View file

@ -0,0 +1,5 @@
---
mode: MainLine
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|patch|refactor|revert|style|test)(\\([\\w\\s-,/\\\\]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)" # noqa yaml[line-length]
minor-version-bump-message: "^(build|chore|ci|docs|feat|refactor|revert|style|test)(\\([\\w\\s-,/\\\\]*\\))?:"
patch-version-bump-message: "^(fix|patch)(\\([\\w\\s-,/\\\\]*\\))?:"

130
README.md
View file

@ -1,105 +1,65 @@
# OPTIMA-35 # **OPTIMA35**
Developed on my [forgejo instance](https://code.boxyfoxy.net/CodeByMrFinchum), [GitLab](https://gitlab.com/CodeByMrFinchum) is used as backup.
## Overview **OPTIMA35** stands for **Organizing, Processing, Tweaking Images, and Modifying Analogs from 35mm Film**. It is a Python package designed to simplify image editing and metadata management, providing an interface/API for handling image and EXIF data seamlessly. While OPTIMA35 was created with analog photography in mind—where scanned images often lack proper EXIF data or retain only scanner metadata—it is equally useful for digital images. Adding or managing EXIF data is invaluable for organizing private photo collections, making your photo library more structured and searchable.
**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. OPTIMA35 is a core package that acts as an interface for libraries like Pillow and piexif, simplifying image manipulation tasks. While it modifies images one at a time, it requires a dedicated program for flexible and batch processing. For a user-friendly graphical experience, consider using [OptimaLab35](https://gitlab.com/CodeByMrFinchum/OptimaLab35), a GUI designed specifically for OPTIMA35, also developed by me.
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. Currently, there are no plans to create a formal API documentation. The code includes annotations and detailed function descriptions to explain its functionality. As this is a private hobby project, dedicating time to writing comprehensive documentation would take away from my limited free time.
**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. ## **Features**
## Current Status ### **Image Processing**
- Resize images (upscale or downscale)
- Convert images to grayscale
- Adjust brightness and contrast
- Add customizable text-based watermarks
### Development and Versioning Notes ### **EXIF Management**
- Add EXIF data using a simple dictionary
- Copy EXIF data from the original image
- Remove EXIF metadata completely
- Add timestamps (e.g., original photo timestamp)
- Automatically adjust EXIF timestamps based on image file names
- Add GPS coordinates to images
**OPTIMA-35** is currently in an **alpha stage** and under active development. As a result: ### **Streamlined Integration**
- The README may occasionally be outdated. - Handles all required EXIF byte conversions behind the scenes
- 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. - Provides an intuitive API for frequently needed operations
- 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**. ## **Installation**
- The **GUI** is loaded by default if **PySide6** is available. Install the GUI (dependencies are installed automatically)
- The **TUI** serves as a fallback when **PySide6** is unavailable or can be started explicitly using the `--tui` option with `main.py`.
While all features are implemented and functional, the designs of both the GUI and TUI are not yet finalized. Some safety checks are still under development.
### Available Features:
**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 a date to EXIF
- Remove EXIF
- **Watermarking**
### Preview GUI
**GUI for OPTIMA-35** with KvArcDark theme on Linux
**Main windows**
![main_tab](https://gitlab.com/CodeByMrFinchum/optima-35/-/raw/main/media/main_tab.png){width=40%}
**Exif tab when opened, settings disabled by default**
![exif_tab_disabled](https://gitlab.com/CodeByMrFinchum/optima-35/-/raw/main/media/exif_tab_disabled.png){width=40%}
**Exif tab after enabeling own exif data**
![exif_tab_enabled](https://gitlab.com/CodeByMrFinchum/optima-35/-/raw/main/media/exif_tab_enabled.png){width=40%}
**Dialog window to modify exif file without need to open yaml file**
![exifeditor](https://gitlab.com/CodeByMrFinchum/optima-35/-/raw/main/media/exifeditor.png){width=40%}
### Preview TUI
**asciinema recording for TUI**
![demo_gif](https://gitlab.com/CodeByMrFinchum/optima-35/-/raw/main/media/demo_v041.gif)
## Dependencies
**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.
- **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**).
Using `pip`:
```bash ```bash
pip install -r requirements_gui.txt pip install OptimaLab35
``` ```
Alternatively, if you use **conda** or its alternatives (**anaconda**, **mamba**, **micromamba**), run: Or in case you only want optima35 (dependencies are installed automatically):
```bash ```bash
conda install -c conda-forge --file requirements_gui.txt pip install optima35
``` ```
# Use of LLMs ---
## **Current Status**
**Stable Release (v1.0)**
- The program follows semantic versioning (**major.minor.patch**).
- The current release is stable, and all changes within the same major version will remain backward compatible.
- Breaking changes, if any, will result in a new major version.
- Future development will primarily focus on the graphical user interface (OptimaLab35), with only minor updates or patches for OPTIMA35 as needed.
---
# Contribution
Thanks to developer [Mr Finch](https://gitlab.com/MrFinchMkV) for contributing to this project, and for initiating and helping setting up the CI/CD pipeline.
## Use of LLMs
In the interest of transparency, I disclose that Generative AI (GAI) large language models (LLMs), including OpenAIs 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 OpenAIs ChatGPT and Ollama models (e.g., OpenCoder and Qwen2.5-coder), have been used to assist in this project.
## Areas of Assistance: ### Areas of Assistance:
- Project discussions and planning - Project discussions and planning
- Spelling and grammar corrections - Spelling and grammar corrections
- Suggestions for suitable packages and libraries - Suggestions for suitable packages and libraries
@ -111,7 +71,7 @@ In cases where LLMs contribute directly to code or provide substantial optimizat
- mradermacher gguf Q4K-M Instruct version of infly/OpenCoder-1.5B - 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)

View file

@ -1,34 +0,0 @@
artist:
- Mr. Finchum
- John Doe
copyright_info:
- All Rights Reserved
- CC BY-NC 4.0
- No Copyright
image_description:
- ILFORD DELTA 3200
- ILFORD ILFOCOLOR
- LomoChrome Turquoise
- Kodak 200
iso:
- '100'
- '200'
- '400'
- '800'
- '1000'
- '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

275
gui.py
View file

@ -1,275 +0,0 @@
import sys
import os
from datetime import datetime
from optima.optima35 import OPTIMA35
from utils.utility import Utilities
from ui.main_window import Ui_MainWindow
from ui.exif_handler_window import ExifEditor
from PySide6 import QtWidgets
from PySide6.QtWidgets import (
QMessageBox,
QApplication,
QMainWindow,
QWidget,
QVBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QCheckBox,
QFileDialog,
QHBoxLayout,
QSpinBox,
)
class Optima35GUI(QMainWindow, Ui_MainWindow):
def __init__(self, exif_file):
super(Optima35GUI, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.o = OPTIMA35()
self.u = Utilities()
self.exif_file = exif_file
self.exif_data = None
self.setWindowTitle(f"{self.o.name} {self.o.version}")
self.default_ui_layout()
self.define_gui_interaction()
if exif_file == "config/exif_example.yaml":
self.change_statusbar("Using example exif...", 10000)
def default_ui_layout(self):
self.ui.png_quality_spinBox.setVisible(False)
def define_gui_interaction(self):
self.ui.input_folder_button.clicked.connect(self.browse_input_folder)
self.ui.output_folder_button.clicked.connect(self.browse_output_folder)
self.ui.start_button.clicked.connect(self.process)
self.ui.image_type.currentIndexChanged.connect(self.update_quality_options)
self.ui.exif_checkbox.stateChanged.connect(
lambda state: self.handle_checkbox_state(state, 2, self.populate_exif)
)
self.ui.tabWidget.currentChanged.connect(self.on_tab_changed)
self.ui.edit_exif_button.clicked.connect(self.open_exif_editor)
self.ui.restart_button.clicked.connect(self.restart_app)
def process(self):
#self.ui.start_button.setEnabled(False)
#self.ui.restart_button.setEnabled(False)
self.check_options() # Get all user selected data
input_folder_valid = os.path.exists(self.o.settings["input_folder"])
output_folder_valid = os.path.exists(self.o.settings["output_folder"])
if not input_folder_valid or not output_folder_valid:
QMessageBox.warning(self, "Warning", f"Input location {input_folder_valid}\nOutput folder {output_folder_valid}...")
return
input_folder = self.o.settings["input_folder"]
output_folder = self.o.settings["output_folder"]
image_files = [
f for f in os.listdir(input_folder) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp"))
]
i = 1
for image_file in image_files:
input_path = os.path.join(input_folder, image_file)
if self.o.settings["new_file_names"] != False:
image_name = self.o.name_images(self.o.settings["new_file_names"], i, len(image_files), self.o.settings["invert_image_order"])
else:
image_name = os.path.splitext(image_file)[0]
output_path = os.path.join(output_folder, image_name)
self.o.process(input_path, output_path)
self.handle_qprogressbar(i, len(image_files))
i += 1
QMessageBox.information(self, "Information", "Finished")
#self.ui.start_button.setEnabled(True)
#self.ui.restart_button.setEnabled(True)
self.ui.progressBar.setValue(0)
def open_exif_editor(self):
"""Open the EXIF Editor."""
self.exif_editor = ExifEditor(self.exif_data)
self.exif_editor.exif_data_updated.connect(self.update_exif_data)
self.exif_editor.show()
def update_exif_data(self, updated_exif_data):
"""Update the EXIF data."""
self.exif_data = updated_exif_data
self.populate_exif()
def handle_checkbox_state(self, state, desired_state, action):
"""Perform an action based on the checkbox state and a desired state. Have to use lambda when calling."""
if state == desired_state:
action()
def on_tab_changed(self, index):
"""Handle tab changes."""
# chatgpt
if index == 1: # EXIF Tab
self.handle_exif_file("read")
elif index == 0: # Main Tab
self.handle_exif_file("write")
def handle_exif_file(self, do):
if do == "read":
self.exif_data = self.u.read_yaml(self.exif_file)
elif do == "write":
self.u.write_yaml(self.exif_file, self.exif_data)
def populate_exif(self):
# partly chatGPT
# Mapping of EXIF fields to comboboxes in the UI
combo_mapping = {
"make": self.ui.make_comboBox,
"model": self.ui.model_comboBox,
"lens": self.ui.lens_comboBox,
"iso": self.ui.iso_comboBox,
"image_description": self.ui.image_description_comboBox,
"user_comment": self.ui.user_comment_comboBox,
"artist": self.ui.artist_comboBox,
"copyright_info": self.ui.copyright_info_comboBox,
}
self.populate_comboboxes(combo_mapping)
def populate_comboboxes(self, combo_mapping):
"""Populate comboboxes with EXIF data."""
# ChatGPT
for field, comboBox in combo_mapping.items():
comboBox.clear() # Clear existing items
comboBox.addItems(map(str, self.exif_data.get(field, [])))
def update_quality_options(self):
"""Update visibility of quality settings based on selected format."""
# ChatGPT
selected_format = self.ui.image_type.currentText()
# Hide all quality settings
self.ui.png_quality_spinBox.setVisible(False)
self.ui.jpg_quality_spinBox.setVisible(False)
# Show relevant settings
if selected_format == "jpg":
self.ui.jpg_quality_spinBox.setVisible(True)
elif selected_format == "webp":
self.ui.jpg_quality_spinBox.setVisible(True)
elif selected_format == "png":
self.ui.png_quality_spinBox.setVisible(True)
def browse_input_folder(self):
folder = QFileDialog.getExistingDirectory(self, "Select Input Folder")
if folder:
self.ui.input_path.setText(folder)
def browse_output_folder(self):
folder = QFileDialog.getExistingDirectory(self, "Select Output Folder")
if folder:
self.ui.output_path.setText(folder)
def change_statusbar(self, msg, timeout = 500):
self.ui.statusBar.showMessage(msg, timeout)
def handle_qprogressbar(self, current, total):
progress = int((100 / total) * current)
self.ui.progressBar.setValue(progress)
def check_options(self):
try:
self.o.settings["input_folder"] = self.ui.input_path.text()
self.o.settings["output_folder"] = self.ui.output_path.text()
self.o.settings["file_format"] = self.ui.image_type.currentText()
self.o.settings["jpg_quality"] = int(self.ui.jpg_quality_spinBox.text())
self.o.settings["png_compression"] = int(self.ui.png_quality_spinBox.text())
self.o.settings["invert_image_order"] = self.ui.revert_checkbox.isChecked()
self.o.settings["grayscale"] = self.ui.grayscale_checkBox.isChecked()
self.o.settings["copy_exif"] = self.ui.exif_copy_checkBox.isChecked()
self.o.settings["own_exif"] = self.ui.exif_checkbox.isChecked()
self.o.settings["font_size"] = self.ui.font_size_comboBox.currentIndex() + 1
self.o.settings["optimize"] = self.ui.optimize_checkBox.isChecked()
self.o.settings["own_date"] = self.ui.add_date_checkBox.isChecked()
if self.ui.resize_checkbox.isChecked():
self.o.settings["resize"] = int(self.ui.resize_spinBox.text())
if self.ui.brightness_checkbox.isChecked():
self.o.settings["brightness"] = int(self.ui.brightness_spinBox.text())
if self.ui.contrast_checkbox.isChecked():
self.o.settings["contrast"] = int(self.ui.contrast_spinBox.text())
if self.ui.rename_checkbox.isChecked():
if self.ui.filename.text() != "":
self.o.settings["new_file_names"] = self.ui.filename.text()
else:
self.o.settings["new_file_names"] = False
else:
self.o.settings["new_file_names"] = False
if self.ui.watermark_checkbox.isChecked():
if self.ui.watermark_lineEdit.text() != "":
self.o.settings["watermark"] = self.ui.watermark_lineEdit.text()
else:
self.o.settings["watermark"] = False
else:
self.o.settings["watermark"] = False
if self.o.settings["own_exif"]:
self.o.selected_exif = self.collect_selected_exif()
if self.ui.add_date_checkBox.isChecked():
self.o.selected_exif["date_time_original"] = self.get_date()
if self.ui.gps_checkBox.isChecked():
self.o.settings["gps"] = [self.ui.lat_lineEdit.text(), self.ui.long_lineEdit.text()]
else:
self.o.settings["gps"] = False
except Exception as e:
print(f"Whoops: {e}")
def get_date(self):
date_input = self.ui.dateEdit.date().toString("yyyy-MM-dd")
new_date = datetime.strptime(date_input, "%Y-%m-%d")
return new_date.strftime("%Y:%m:%d 00:00:00")
def collect_selected_exif(self):
user_data = {}
user_data["make"] = self.ui.make_comboBox.currentText()
user_data["model"] = self.ui.model_comboBox.currentText()
user_data["lens"] = self.ui.lens_comboBox.currentText()
user_data["iso"] = self.ui.iso_comboBox.currentText()
user_data["image_description"] = self.ui.image_description_comboBox.currentText()
user_data["user_comment"] = self.ui.user_comment_comboBox.currentText()
user_data["artist"] = self.ui.artist_comboBox.currentText()
user_data["copyright_info"] = self.ui.copyright_info_comboBox.currentText()
user_data["software"] = f"{self.o.name} {self.o.version}"
return user_data
def rebuild_ui(self):
# Define the bash script to execute
bash_script = "/home/sam/git/gitlab_public/optima-35/rebuild_ui.sh"
os.system(bash_script)
def restart_app(self):
"""Restarts the application."""
self.rebuild_ui()
# chatGPT
python = sys.executable # Path to the Python interpreter
os.execv(python, [python] + sys.argv)
def main(exif_file):
app = QtWidgets.QApplication(sys.argv)
window = Optima35GUI(exif_file=exif_file)
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)

53
main.py
View file

@ -1,53 +0,0 @@
import os
from argparse import ArgumentParser
# Mainly from ChatGPT
def check_pyside_installed():
try:
import PySide6 # Replace with PySide2 if using that version
return True
except ImportError:
return False
def start_gui():
import gui
gui.main(exif_file)
def start_tui():
import tui
tui.main(exif_file, tui_settings_file)
def main():
parser = ArgumentParser(description="Start the Optima35 application.")
parser.add_argument("--tui", action="store_true", help="Start in terminal UI mode.")
args = parser.parse_args()
if args.tui:
print("Starting TUI...")
start_tui()
return
# Check OS and start GUI if on Windows
if os.name == "nt":
print("Detected Windows. Starting GUI...")
start_gui()
else:
# Non-Windows: Check if PySide is installed
if check_pyside_installed():
print("PySide detected. Starting GUI...")
start_gui()
else:
print("PySide is not installed. Falling back to TUI...")
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

@ -1,372 +0,0 @@
{"version": 2, "width": 80, "height": 24, "timestamp": 1735643611, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
[0.677329, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[0.678152, "o", "\u001b]2;sam@potatohead:~/git/gitlab_public/optima-35\u0007\u001b]1;..lic/optima-35\u0007"]
[0.680479, "o", "\u001b]7;file://potatohead/home/sam/git/gitlab_public/optima-35\u0007"]
[0.743392, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[47m\u001b[38;5;30m \u001b[0m\u001b[38;5;30m\u001b[47m\u001b[47m\u001b[38;5;30m \u001b[0m\u001b[38;5;30m\u001b[47m\u001b[48;5;30m\u001b[37m\u001b[0m\u001b[37m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[48;5;30m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m/\u001b[38;5;250mg\u001b[0m\u001b[38;5;250m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[48;5;30m\u001b[38;5;255moptima-35\u001b[0m\u001b[38;5;255m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[48;5;30m\u001b[43m\u001b[38;5;30m\u001b[0m\u001b[38;5;30m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m feature/enhance-modularity \u001b[30m!5 \u001b[30m?2\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b[K\u001b[1C\u001b[0m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[40m\u001b[32m \u001b[0m\u001b[32m\u001b[40m\u001b[40m\u001b[32m✔\u001b[0m\u001b[32m\u001b[40m\u001b[40m\u001b[32m \u001b[0m\u001b[32m\u001b[40m\u001b[40m\u001b[32m\u001b[34m\u001b[0m\u001b[34m\u001b[40m\u001b[44m\u001b[30m base\u001b[0m\u001b[30m\u001b[44m\u001b[44m\u001b[30m \u001b[0m\u001b[30m\u001b[44m\u001b[44m\u001b[30m \u001b[0m\u001b[30m\u001b[44m\u001b[49m\u001b[39m\u001b[14D"]
[0.744052, "o", "\u001b[?2004h"]
[1.448961, "o", "m\u001b[K"]
[1.45588, "o", "\b\u001b[4mm\u001b[24m"]
[1.456369, "o", "\b\u001b[4mm\u001b[24m\u001b[38;5;244micromamba acti\u001b[38;5;244mv\u001b[38;5;244mate optima35\u001b[39m\u001b[K\u001b[A\u001b[53C"]
[1.524001, "o", "\b\u001b[4mm\u001b[39m\u001b[4mi\u001b[24m"]
[1.538941, "o", "\b\b\u001b[24m\u001b[1m\u001b[31mm\u001b[24m\u001b[1m\u001b[31mi\u001b[0m\u001b[39m"]
[1.64567, "o", "\b\b\u001b[1m\u001b[31mm\u001b[1m\u001b[31mi\u001b[1m\u001b[31mc\u001b[0m\u001b[39m"]
[1.823061, "o", "\b\u001b[1m\u001b[31mc\u001b[1m\u001b[31mr\u001b[0m\u001b[39m"]
[1.936561, "o", "\b\u001b[1m\u001b[31mr\u001b[1m\u001b[31mo\u001b[0m\u001b[39m"]
[1.941248, "o", "\b\b\b\b\b\u001b[0m\u001b[32mm\u001b[0m\u001b[32mi\u001b[0m\u001b[32mc\u001b[0m\u001b[32mr\u001b[0m\u001b[32mo\u001b[39m"]
[2.275468, "o", "\u001b[39mm\u001b[39ma\u001b[39mm\u001b[39mb\u001b[39ma\u001b[39m \u001b[39ma\u001b[39mc\u001b[39mt\u001b[39miv\u001b[39ma\u001b[39mt\u001b[39me\u001b[39m \u001b[39mo\u001b[39mp\u001b[39mt\u001b[39mi\u001b[39mm\u001b[39ma\u001b[39m3\u001b[39m5"]
[2.283536, "o", "\u001b[A\u001b[56C\u001b[32mo\u001b[32mm\u001b[32ma\u001b[32mm\u001b[32mb\u001b[32ma\u001b[39m\u001b[1B\u001b[62D"]
[2.684344, "o", "\u001b[?2004l\r\r\n"]
[2.686472, "o", "\u001b]2;micromamba activate optima35\u0007\u001b]1;\u0007"]
[2.719122, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[2.719308, "o", "\u001b]2;sam@potatohead:~/git/gitlab_public/optima-35\u0007\u001b]1;..lic/optima-35\u0007"]
[2.722384, "o", "\u001b]7;file://potatohead/home/sam/git/gitlab_public/optima-35\u0007"]
[2.8234, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[47m\u001b[38;5;30m \u001b[0m\u001b[38;5;30m\u001b[47m\u001b[47m\u001b[38;5;30m \u001b[0m\u001b[38;5;30m\u001b[47m\u001b[48;5;30m\u001b[37m\u001b[0m\u001b[37m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[48;5;30m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m/\u001b[38;5;250mg\u001b[0m\u001b[38;5;250m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[48;5;30m\u001b[38;5;255moptima-35\u001b[0m\u001b[38;5;255m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[48;5;30m\u001b[43m\u001b[38;5;30m\u001b[0m\u001b[38;5;30m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m feature/enhance-modularity \u001b[30m!5 \u001b[30m?2\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b[K"]
[2.824436, "o", "\u001b[?2004h"]
[3.546197, "o", "p"]
[3.552859, "o", "\b\u001b[1m\u001b[31mp\u001b[0m\u001b[39m"]
[3.553263, "o", "\b\u001b[1m\u001b[31mp\u001b[0m\u001b[39m\u001b[38;5;244myside6-uic ui/\u001b[38;5;244mm\u001b[38;5;244main_window.ui -o ui/main_window.py \u001b[39m\u001b[K\u001b[A\u001b[30C"]
[3.775027, "o", "\b\u001b[1m\u001b[31mp\u001b[1m\u001b[31my\u001b[0m\u001b[39m"]
[3.978262, "o", "\b\b\u001b[1m\u001b[31mp\u001b[1m\u001b[31my\u001b[1m\u001b[31mt\u001b[0m\u001b[39m\u001b[K\u001b[1B\r\u001b[K\u001b[A\u001b[68C"]
[3.996697, "o", "\u001b[38;5;244mhon main.py\u001b[39m\u001b[11D"]
[4.104095, "o", "\b\u001b[1m\u001b[31mt\u001b[1m\u001b[31mh\u001b[0m\u001b[39m"]
[4.51609, "o", "\b\u001b[1m\u001b[31mh\u001b[1m\u001b[31mo\u001b[0m\u001b[39m"]
[4.898228, "o", "\u001b[39mn\u001b[39m \u001b[39mm\u001b[39ma\u001b[39mi\u001b[39mn\u001b[39m.\u001b[39mp\u001b[39my"]
[4.916302, "o", "\u001b[14D\u001b[0m\u001b[32mp\u001b[0m\u001b[32my\u001b[0m\u001b[32mt\u001b[0m\u001b[32mh\u001b[0m\u001b[32mo\u001b[32mn\u001b[39m \u001b[4mm\u001b[4ma\u001b[4mi\u001b[4mn\u001b[4m.\u001b[4mp\u001b[4my\u001b[24m"]
[5.403027, "o", "\b\u001b[4my\u001b[4m \u001b[24m \r\u001b[K"]
[5.409587, "o", "\u001b[A\u001b[78C\u001b[4my\u001b[24m\u001b[24m \u001b[K\r"]
[5.40996, "o", "\u001b[38;5;244m-\u001b[38;5;244m-\u001b[38;5;244mt\u001b[38;5;244mu\u001b[38;5;244mi\u001b[39m\r"]
[5.601688, "o", "\u001b[39m-"]
[5.722974, "o", "\r-\u001b[39m-"]
[6.026401, "o", "\u001b[39mt"]
[6.161384, "o", "\u001b[39mu"]
[6.217467, "o", "\u001b[39mi"]
[6.589968, "o", "\u001b[?2004l\r\r\n"]
[6.59058, "o", "\u001b]2;python main.py --tui\u0007\u001b]1;\u0007"]
[6.617562, "o", "Fall back to exif example file...\r\n"]
[6.622191, "o", "Starting TUI...\r\n"]
[6.716642, "o", "Settings file empty.\r\nNo settings found...\r\nAsking for new settings...\r\n\r\nDefault resize percentage (below 100 downscale, above upscale): "]
[9.164618, "o", "8"]
[9.248071, "o", "0"]
[9.743554, "o", "\r\n"]
[9.743735, "o", "Default contrast percentage (negative = decrease, positive = increase): "]
[10.78209, "o", "1"]
[10.867213, "o", "0"]
[11.206582, "o", "\r\n"]
[11.206816, "o", "Default brighness percentage (negative = decrease, positive = increase): "]
[11.714352, "o", "-"]
[12.073169, "o", "1"]
[12.167016, "o", "0"]
[12.597502, "o", "\r\n"]
[12.597744, "o", "JPEG quality (1-100, 80 default): "]
[13.650373, "o", "8"]
[13.712797, "o", "0"]
[13.941506, "o", "\r\n"]
[13.941706, "o", "PNG compression level (0-9, 6 default): "]
[14.539469, "o", "6"]
[14.796779, "o", "\r\n"]
[14.938909, "o", "\u001b[?1h\u001b=\u001b[?25l\r\n"]
[14.942831, "o", "\u001b[A\rOptimize images i.e. compressing? \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b[34my\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \u001b(B\u001b[m\u001b[47m\u001b[30myes\u001b(B\u001b[m \r\n"]
[14.943016, "o", "\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b[34mn\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m no \r\u001b[A"]
[14.946002, "o", "\u001b(B\u001b[m\u001b[31m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\u001b[A"]
[15.919041, "o", "\u001b[A\u001b[M\u001b[M\u001b[M\u001b[?12l\u001b[?25h\u001b[?1l\u001b>"]
[15.922308, "o", "New settings saved successfully.\r\nEnter path of input folder: "]
[17.083508, "o", "o"]
[17.413375, "o", "\b \b"]
[17.597213, "o", "l"]
[17.763659, "o", "o"]
[17.829166, "o", "c"]
[17.907582, "o", "a"]
[18.018695, "o", "l"]
[18.219882, "o", "_"]
[18.597998, "o", "f"]
[18.684139, "o", "i"]
[18.854542, "o", "l"]
[18.910718, "o", "e"]
[19.116207, "o", "s"]
[19.29677, "o", "/"]
[19.606841, "o", "i"]
[19.67462, "o", "m"]
[19.7635, "o", "g"]
[19.92206, "o", "\r\n"]
[19.922244, "o", "Enter path of output folder: "]
[20.173606, "o", "l"]
[20.321372, "o", "o"]
[20.383433, "o", "c"]
[20.463172, "o", "a"]
[20.555845, "o", "l"]
[20.752791, "o", "_"]
[20.975923, "o", "f"]
[21.051336, "o", "i"]
[21.212242, "o", "l"]
[21.277781, "o", "e"]
[21.473246, "o", "s"]
[21.804064, "o", "/"]
[22.106301, "o", "o"]
[22.201691, "o", "u"]
[22.269246, "o", "t"]
[22.601648, "o", "\r\n"]
[22.601885, "o", "Enter export file format (jpg, png, webp): "]
[24.083726, "o", "j"]
[24.127736, "o", "p"]
[24.264589, "o", "g"]
[24.57916, "o", "\r\n"]
[24.598678, "o", "\u001b[?1h\u001b=\u001b[?25l\r\n\r\n\r\n"]
[24.607614, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mResize image\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n"]
[24.607794, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n"]
[24.608348, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[24.616611, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r"]
[24.616839, "o", "\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r"]
[24.617428, "o", "\r\n \r\r\n \r\r\n \r"]
[24.619314, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C"]
[24.619478, "o", "\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[26.660369, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mChange EXIF\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n"]
[26.660559, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n"]
[26.66109, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[26.6667, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r"]
[26.666875, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[26.789333, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mConvert to grayscale\u001b(B\u001b[m \r\n"]
[26.789529, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n"]
[26.789664, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[26.797578, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r"]
[26.798309, "o", "\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r"]
[26.798751, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[26.929884, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n"]
[26.930073, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mChange contrast\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n"]
[26.930199, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[26.937997, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[26.938647, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r"]
[26.939112, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[27.212356, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n"]
[27.212564, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mChange brightness\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[27.22141, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[27.221766, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r"]
[27.221985, "o", "\r\n \r\r\n \r"]
[27.222796, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[27.344641, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n"]
[27.345282, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mRename images\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n"]
[27.345758, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[27.354429, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r"]
[27.35514, "o", "\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[27.616497, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n"]
[27.616697, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mInvert image order\u001b(B\u001b[m \r\n"]
[27.617352, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[27.623229, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r"]
[27.623463, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[27.778389, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n"]
[27.778648, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n"]
[27.779389, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mAdd Watermark\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[27.787995, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[27.788617, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r"]
[27.788866, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n"]
[27.789448, "o", "\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[28.089642, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n"]
[28.089921, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n"]
[28.090473, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n"]
[28.09084, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mAdd Watermark\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[28.096658, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[28.096769, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r"]
[28.096929, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[28.244958, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n"]
[28.245236, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n"]
[28.245441, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n"]
[28.246239, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mInvert image order\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[28.254857, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[28.255412, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n"]
[28.255651, "o", " \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n"]
[28.255861, "o", "\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n"]
[28.256065, "o", "\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[28.538276, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n"]
[28.53847, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mRename images\u001b(B\u001b[m \r\n"]
[28.539087, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[28.547915, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[28.54851, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r"]
[28.549125, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C"]
[28.549609, "o", "\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[28.635635, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n"]
[28.636112, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mRename images\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[28.645078, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[28.645679, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r"]
[28.646141, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C"]
[28.646396, "o", "\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.060214, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n"]
[29.060392, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n"]
[29.060994, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mInvert image order\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.066936, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r"]
[29.06701, "o", "\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n"]
[29.06705, "o", " \r"]
[29.067299, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.305548, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n"]
[29.305754, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mInvert image order\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.314491, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r"]
[29.314678, "o", "\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r"]
[29.315449, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.476124, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n"]
[29.476376, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n"]
[29.476945, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n"]
[29.477451, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mRename images\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.485916, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[29.486211, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n"]
[29.48687, "o", " \r"]
[29.487248, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.645362, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n"]
[29.645617, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n"]
[29.646303, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mChange brightness\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.655419, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[29.655923, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r"]
[29.656398, "o", "\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r"]
[29.656619, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n"]
[29.656796, "o", "\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.832918, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n"]
[29.83311, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mChange brightness\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.842077, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[29.842286, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r"]
[29.842422, "o", "\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n"]
[29.842576, "o", " \r\r\n \r\r\n \r"]
[29.843329, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n"]
[29.843799, "o", "\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.976699, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n"]
[29.976948, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mChange contrast\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[29.985854, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r"]
[29.98636, "o", "\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n"]
[29.986765, "o", "\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.109772, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n"]
[30.109978, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mChange contrast\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.118884, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r"]
[30.119119, "o", "\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r"]
[30.119814, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.230951, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n"]
[30.231215, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mConvert to grayscale\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n"]
[30.231425, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n"]
[30.231688, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.240842, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"]
[30.241119, "o", "\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r"]
[30.241323, "o", "\r\n \r\r\n \r"]
[30.242531, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.360245, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n"]
[30.360539, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mConvert to grayscale\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n"]
[30.360826, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.36533, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n \r\r\n"]
[30.365472, "o", "\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r"]
[30.365943, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.50115, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n"]
[30.501599, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mChange EXIF\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n"]
[30.502022, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.510559, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r"]
[30.510805, "o", "\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n"]
[30.510971, "o", " \r\r\n \r\r\n \r\r\n \r\r\n \r"]
[30.511911, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.685517, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CResize image \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mChange EXIF\u001b(B\u001b[m \r\n"]
[30.685758, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n"]
[30.685952, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.694848, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r"]
[30.695365, "o", "\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r"]
[30.695848, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C"]
[30.696368, "o", "\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.83141, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mResize image\u001b(B\u001b[m \r\n"]
[30.831678, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n"]
[30.831884, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n"]
[30.832089, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n"]
[30.832289, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[30.84096, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r"]
[30.84152, "o", "\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m \u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n"]
[30.841772, "o", "\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n"]
[30.841971, "o", "\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[31.156421, "o", "\u001b[A\u001b[A\u001b[A\r \r\nOPTIMA-35 v.0.4.1 \r\nSelect what you want to do (esc or q to exit) \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mResize image\u001b(B\u001b[m \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange EXIF \r\n"]
[31.156634, "o", "\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CConvert to grayscale \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange contrast \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CChange brightness \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CRename images \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CInvert image order \r\n\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[CAdd Watermark \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[31.163125, "o", "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b(B\u001b[m\u001b[33m\u001b[40m\rPress <space>, <tab> for multi-selection and <enter> to select and accept \r\u001b(B\u001b[m\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\r\n"]
[31.163206, "o", " \r\r\n \r"]
[31.163253, "o", "\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C"]
[31.163314, "o", "\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\n"]
[31.16336, "o", "\r\u001b[C\u001b[C\u001b(B\u001b[m\u001b[37m[\u001b(B\u001b[m\u001b(B\u001b[m\u001b[33m\u001b[1m*\u001b(B\u001b[m\u001b(B\u001b[m\u001b[37m]\u001b(B\u001b[m \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[31.51079, "o", "\u001b[A\u001b[A\u001b[A\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[?12l\u001b[?25h\u001b[?1l\u001b>"]
[31.510988, "o", "What should be the name for the new images? "]
[33.593907, "o", "e"]
[33.888461, "o", "\b \b"]
[33.989013, "o", "d"]
[34.134175, "o", "e"]
[34.173323, "o", "m"]
[34.24098, "o", "o"]
[34.728246, "o", "\r\n"]
[34.728423, "o", "Enter text for watermark. "]
[35.633165, "o", "D"]
[35.803621, "o", "e"]
[35.845136, "o", "m"]
[35.914241, "o", "o"]
[36.168864, "o", " "]
[36.853512, "o", "\b \b"]
[37.105764, "o", "\r\n"]
[37.12281, "o", "\u001b[?1h\u001b=\u001b[?25l\r\n"]
[37.130101, "o", "\u001b[A\rEnter Make \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mNikon\u001b(B\u001b[m \r\n"]
[37.130252, "o", "\u001b[C\u001b[CCanon \r\u001b[A"]
[37.137636, "o", "\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\u001b[A"]
[38.366811, "o", "\u001b[A\u001b[M\u001b[M\u001b[M\u001b[?12l\u001b[?25h\u001b[?1l\u001b>"]
[38.377981, "o", "\u001b[?1h\u001b=\u001b[?25l\r\n"]
[38.38132, "o", "\u001b[A\rEnter Model \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mFG\u001b(B\u001b[m \r\n\u001b[C\u001b[CF50 \r\n\u001b[C\u001b[CAE-1 \r\u001b[A\u001b[A"]
[38.384958, "o", "\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\u001b[A\u001b[A"]
[38.874015, "o", "\u001b[A\u001b[M\u001b[M\u001b[M\u001b[M"]
[38.874229, "o", "\u001b[?12l\u001b[?25h\u001b[?1l\u001b>"]
[38.886354, "o", "\u001b[?1h\u001b=\u001b[?25l\r\n"]
[38.889292, "o", "\u001b[A\rEnter Lens \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mNikon LENS SERIES E 50mm\u001b(B\u001b[m \r\n\u001b[C\u001b[CAF NIKKOR 35-70mm \r\n"]
[38.889375, "o", "\u001b[C\u001b[CCanon FD 50mm f/1.4 S.S.C \r\u001b[A\u001b[A"]
[38.892151, "o", "\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\u001b[A\u001b[A"]
[39.594436, "o", "\u001b[A\u001b[M\u001b[M\u001b[M\u001b[M\u001b[?12l\u001b[?25h\u001b[?1l\u001b>"]
[39.60723, "o", "\u001b[?1h\u001b=\u001b[?25l\r\n"]
[39.610311, "o", "\u001b[A\rEnter Iso \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30m100\u001b(B\u001b[m \r\n\u001b[C\u001b[C200 \r\n"]
[39.610499, "o", "\u001b[C\u001b[C400 \r\n\u001b[C\u001b[C800 \r\n\u001b[C\u001b[C1000 \r\n\u001b[C\u001b[C1600 \r\n\u001b[C\u001b[C3200 \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[39.613514, "o", "\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r"]
[39.613654, "o", "\r\n \r\r\n \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[40.285396, "o", "\u001b[A\rEnter Iso \r\n\u001b[C\u001b[C100 \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30m200\u001b(B\u001b[m \r\n\u001b[C\u001b[C400 \r\n\u001b[C\u001b[C800 \r\n"]
[40.285634, "o", "\u001b[C\u001b[C1000 \r\n\u001b[C\u001b[C1600 \r\n\u001b[C\u001b[C3200 \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[40.290088, "o", " \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r\r\n \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[40.486366, "o", "\u001b[A\rEnter Iso \r\n\u001b[C\u001b[C100 \r\n\u001b[C\u001b[C200 \r\n"]
[40.486584, "o", "\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30m400\u001b(B\u001b[m \r\n\u001b[C\u001b[C800 \r\n\u001b[C\u001b[C1000 \r\n\u001b[C\u001b[C1600 \r\n\u001b[C\u001b[C3200 \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[40.49292, "o", " \r\r\n \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\r\n \r\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\u001b[A"]
[41.095691, "o", "\u001b[A\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[?12l\u001b[?25h\u001b[?1l\u001b>"]
[41.107748, "o", "\u001b[?1h\u001b=\u001b[?25l\r\n"]
[41.11136, "o", "\u001b[A\rEnter Image Description \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mILFORD DELTA 3200\u001b(B\u001b[m \r\n\u001b[C\u001b[CILFORD ILFOCOLOR \r\n\u001b[C\u001b[CLomoChrome Turquoise \r\n\u001b[C\u001b[CKodak 200 \r\u001b[A\u001b[A\u001b[A"]
[41.115136, "o", "\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\r\n \r\u001b[A\u001b[A\u001b[A"]
[42.008703, "o", "\u001b[A\rEnter Image Description \r\n\u001b[C\u001b[CILFORD DELTA 3200 \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mILFORD ILFOCOLOR\u001b(B\u001b[m \r\n"]
[42.008925, "o", "\u001b[C\u001b[CLomoChrome Turquoise \r\n\u001b[C\u001b[CKodak 200 \r\u001b[A\u001b[A\u001b[A"]
[42.015056, "o", " \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\u001b[A\u001b[A\u001b[A"]
[42.414356, "o", "\u001b[A\u001b[M\u001b[M\u001b[M\u001b[M\u001b[M\u001b[?12l\u001b[?25h\u001b[?1l\u001b>"]
[42.431029, "o", "\u001b[?1h\u001b=\u001b[?25l\r\n"]
[42.43832, "o", "\u001b[A\rEnter User Comment \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mScanner.NORITSU-KOKI\u001b(B\u001b[m \r\n"]
[42.438554, "o", "\u001b[C\u001b[CScanner.NA \r\u001b[A"]
[42.445809, "o", "\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\u001b[A"]
[43.147181, "o", "\u001b[A\u001b[M\u001b[M\u001b[M\u001b[?12l\u001b[?25h\u001b[?1l\u001b>"]
[43.1585, "o", "\u001b[?1h\u001b=\u001b[?25l\r\n"]
[43.161793, "o", "\u001b[A\rEnter Artist \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mMr. Finchum\u001b(B\u001b[m \r\n"]
[43.16201, "o", "\u001b[C\u001b[CJohn Doe \r\u001b[A"]
[43.16618, "o", "\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\u001b[A"]
[43.620173, "o", "\u001b[A\u001b[M\u001b[M\u001b[M\u001b[?12l\u001b[?25h\u001b[?1l\u001b>"]
[43.631543, "o", "\u001b[?1h\u001b=\u001b[?25l\r\n"]
[43.63482, "o", "\u001b[A\rEnter Copyright Info \r\n\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mAll Rights Reserved\u001b(B\u001b[m \r\n\u001b[C\u001b[CCC BY-NC 4.0 \r\n\u001b[C\u001b[CNo Copyright \r\u001b[A\u001b[A"]
[43.638191, "o", "\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\r\n \r\u001b[A\u001b[A"]
[44.39754, "o", "\u001b[A\rEnter Copyright Info \r\n\u001b[C\u001b[CAll Rights Reserved \r\n"]
[44.397768, "o", "\u001b[C\u001b[C\u001b(B\u001b[m\u001b[47m\u001b[30mCC BY-NC 4.0\u001b(B\u001b[m \r\n\u001b[C\u001b[CNo Copyright \r\u001b[A\u001b[A"]
[44.404144, "o", " \r\r\n\u001b(B\u001b[m\u001b[37m\u001b[1m> \u001b(B\u001b[m\r\r\n \r\u001b[A\u001b[A"]
[44.707673, "o", "\u001b[A\u001b[M\u001b[M\u001b[M\u001b[M"]
[44.707849, "o", "\u001b[?12l\u001b[?25h\u001b[?1l\u001b>Enter a date (yyyy-mm-dd): "]
[45.955851, "o", "2"]
[46.054154, "o", "0"]
[46.404177, "o", "2"]
[46.497714, "o", "4"]
[47.123881, "o", "-"]
[47.416185, "o", "1"]
[47.489983, "o", "2"]
[47.757033, "o", "-"]
[48.21329, "o", "3"]
[48.282472, "o", "1"]
[49.029909, "o", "\r\n"]
[49.036256, "o", "Enter Latitude (xx.xxxxxx): "]
[49.59262, "o", "\r\n"]
[49.773864, "o", "1|------------> |4\r"]
[49.844145, "o", "2|-------------------------> |4\r"]
[49.917126, "o", "3|-------------------------------------> |4\r"]
[49.992149, "o", "4|-------------------------------------------------->|4\r\r\nDone\r\n"]
[50.012799, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[50.013073, "o", "\u001b]2;sam@potatohead:~/git/gitlab_public/optima-35\u0007\u001b]1;..lic/optima-35\u0007"]
[50.016341, "o", "\u001b]7;file://potatohead/home/sam/git/gitlab_public/optima-35\u0007"]
[50.047409, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[47m\u001b[38;5;30m \u001b[0m\u001b[38;5;30m\u001b[47m\u001b[47m\u001b[38;5;30m \u001b[0m\u001b[38;5;30m\u001b[47m\u001b[48;5;30m\u001b[37m\u001b[0m\u001b[37m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[48;5;30m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m/\u001b[38;5;250mg\u001b[0m\u001b[38;5;250m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[48;5;30m\u001b[38;5;255moptima-35\u001b[0m\u001b[38;5;255m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[48;5;30m\u001b[48;5;30m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[48;5;30m\u001b[43m\u001b[38;5;30m\u001b[0m\u001b[38;5;30m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m feature/enhance-modularity \u001b[30m!5 \u001b[30m?2\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b[K"]
[50.0481, "o", "\u001b[?2004h"]
[52.228733, "o", "\u001b[?2004l\r\r\n"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

View file

View file

@ -1,113 +0,0 @@
import re
import os
from datetime import datetime
# My packages
from utils.image_handler import ImageProcessor, ExifHandler
class OPTIMA35:
def __init__(self):
self.name = "OPTIMA-35"
self.version = "0.4.1"
self.image_processor = ImageProcessor()
self.exif_handler = ExifHandler()
self.settings = {
"input_folder": None,
"output_folder": None,
"file_format": None,
"resize": False,
"contrast": False,
"brightness": False,
"new_file_names": False,
"invert_image_order": False,
"copy_exif": False,
"own_exif": False,
"watermark": False,
"grayscale": False,
"jpg_quality": None,
"png_compression": None,
"font_size": None,
"optimize": False,
"gps": False
}
self.selected_exif = {}
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 process(self, image_input_file, image_output_file):
with self.image_processor.open_image(image_input_file) as img:
processed_img = img
image_name = os.path.basename(image_output_file)
if self.settings["resize"] != False:
processed_img = self.image_processor.resize_image(
image = processed_img, percent = self.settings["resize"]
)
if self.settings["watermark"] != False:
processed_img = self.image_processor.add_watermark(processed_img, self.settings["watermark"], int(self.settings["font_size"]))
if self.settings["grayscale"] != False: # There is a problem, if we first to grayscale and then watermark it braeks
processed_img = self.image_processor.grayscale(processed_img)
if self.settings["brightness"] != False: # Does the order of brightness and contrast matter?
processed_img = self.image_processor.change_brightness(processed_img, self.settings["brightness"])
if self.settings["contrast"] != False: # Does the order of brightness and contrast matter?
processed_img = self.image_processor.change_contrast(processed_img, self.settings["contrast"])
if self.settings["own_exif"] != False:
selected_exif = self.selected_exif
if "date_time_original" in self.selected_exif:
selected_exif = self.modify_timestamp_in_exif(selected_exif, image_name)
exif_data = self.exif_handler.build_exif_dict(selected_exif, self.image_processor.get_image_size(processed_img))
if self.settings["gps"] != False:
latitude = float(self.settings["gps"][0])
longitude = float(self.settings["gps"][1])
exif_data = self.exif_handler.add_geolocation_to_exif(exif_data, latitude, longitude)
elif self.settings["copy_exif"] == True:
# When copying exif from original, make sure to change Piexel X & Y Dimension to fit new size
try:
og_exif = self.exif_handler.get_exif_info(img)
og_exif["Exif"][40962], og_exif["Exif"][40963] = self.image_processor.get_image_size(processed_img)
exif_data = og_exif
except Exception:
# If an error happends it is because the picture does not have exif data
print("Copying EXIF data selected, but no EXIF data is available in the original image file.")
exif_data = None
elif self.settings["copy_exif"] == False:
exif_data = None
self.image_processor.save_image(
image = processed_img,
path = image_output_file,
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["optimize"]
)
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}"
if __name__ == "__main__":
print("Please load OPTIMA35 into the ui class...")
exit()

2
pip_README.md Normal file
View file

@ -0,0 +1,2 @@
Uses pillow and piexif to modify images, see my [forgejo instance](https://code.boxyfoxy.net/CodeByMrFinchum) or [GitLab](https://gitlab.com/CodeByMrFinchum)(backup) for more information.
Install [OptimaLab35](https://pypi.org/project/OptimaLab35/) in pip for a GUI.

34
pyproject.toml Normal file
View file

@ -0,0 +1,34 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "optima35"
dynamic = ["version"]
authors = [{ name = "Mr. Finchum" }]
description = "optima35 is a package to modify images, using pillow and piexif."
readme = "pip_README.md"
requires-python = ">=3.8, <4.0"
dependencies = ["piexif", "pillow"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Topic :: Software Development :: Libraries :: Python Modules",
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
"Operating System :: OS Independent",
]
[project.urls]
Source = "https://gitlab.com/CodeByMrFinchum/optima35"
Documentation = "https://gitlab.com/CodeByMrFinchum/optima35/-/blob/main/README.md"
Changelog = "https://gitlab.com/CodeByMrFinchum/optima35/-/blob/main/CHANGELOG.md"
[project.scripts]
optima35 = "optima35.__main__:main"
[tool.hatch.build.targets.wheel]
packages = ["src/optima35"]
[tool.hatch.version]
path = "src/optima35/__init__.py"

View file

@ -1,4 +0,0 @@
pyyaml
piexif
pillow
pyside6

View file

@ -1,4 +0,0 @@
pyyaml
piexif
pillow
simple_term_menu

1
src/optima35/__init__.py Normal file
View file

@ -0,0 +1 @@
__version__ = "1.0.0"

9
src/optima35/__main__.py Normal file
View file

@ -0,0 +1,9 @@
from . import __version__
# From ChatGPT
def main():
print("(C) 2024-2025 Mr. Finchum aka CodeByMrFinchum")
print(f"optima35 (v{__version__}) is a core library and not intended to be run directly.")
print("Please use OptimaLab35 for a UI, run pip install OptimaLab35 and start with OptimaLab35.")
if __name__ == "__main__":
main()

214
src/optima35/core.py Normal file
View file

@ -0,0 +1,214 @@
import re
import os
from datetime import datetime
from optima35.image_handler import ImageProcessor, ExifHandler
from optima35 import __version__
class OptimaManager:
def __init__(self):
self.name = "optima35"
self.version = __version__
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_three = filename[-3:len(filename)]
total_seconds = int(re.sub(r'\D+', '', last_three))
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: str,
resize: int = None,
watermark: str = None,
font_size: int = 2,
grayscale: bool = False,
brightness: float = None,
contrast: float = None
):
# My Code restructured by ChatGPT, but had to fix bugs
img = self.image_processor.open_image(image_input_file)
# Apply transformations
if resize is not None:
img = self.image_processor.resize_image(img, percent=resize)
if watermark is not None:
img = self.image_processor.add_watermark(img, watermark, font_size)
if grayscale:
img = self.image_processor.grayscale(img)
if brightness is not None:
img = self.image_processor.change_brightness(img, brightness)
if contrast is not None:
img = self.image_processor.change_contrast(img, contrast)
return img
def _handle_exif(
self,
image,
file_name,
dict_for_exif: dict = None,
gps: tuple[float, float] = None,
copy_exif: bool = False
):
# My Code restructured by ChatGPT, but had to fix bugs
# Build or copy EXIF data
if dict_for_exif:
if "date_time_original" in dict_for_exif:
dict_for_exif = self._modify_timestamp_in_exif(dict_for_exif, file_name)
exif_data = self.exif_handler.build_exif_bytes(
dict_for_exif, self.image_processor.get_image_size(image)
)
if gps:
exif_data = self.exif_handler.add_geolocation_to_exif(
exif_data, gps[0], gps[1]
)
elif copy_exif:
exif_data = self.exif_handler.get_exif_info(image)
else:
exif_data = None
return exif_data
def process_and_save_image(
self,
image_input_file: str,
image_output_file: str,
file_type: str = "jpg",
quality: int = 90,
compressing: int = 6,
optimize: bool = False,
resize: int = None,
watermark: str = None,
font_size: int = 2,
grayscale: bool = False,
brightness: float = None,
contrast: float = None,
dict_for_exif: dict = None,
gps: tuple[float, float] = None,
copy_exif: bool = False
) -> None:
"""
Processes an image with the given parameters and saves the output to a file.
Args:
image_input_file (str): Path to the input image file.
image_output_file (str): Path to save the processed image.
file_type (str): Output image format ('jpg', 'png'). Defaults to 'jpg'.
quality (int): JPEG quality (1-100). Defaults to 90.
compressing (int): PNG compression level (0-9). Defaults to 6.
optimize (bool): Optimize image for smaller file size. Defaults to False.
resize (int, optional): Resize percentage. Defaults to None.
watermark (str, optional): Watermark text to add. Defaults to None.
font_size (int): Font size for the watermark. Defaults to 2.
grayscale (bool): Convert image to grayscale. Defaults to False.
brightness (float, optional): Adjust brightness (e.g., 1.2 for 20% brighter). Defaults to None.
contrast (float, optional): Adjust contrast (e.g., 1.5 for 50% higher contrast). Defaults to None.
dict_for_exif (dict, optional): EXIF metadata to insert. Defaults to None.
gps (tuple[float, float], optional): GPS coordinates (latitude, longitude). Defaults to None.
copy_exif (bool): Copy EXIF metadata from the input image. Defaults to False.
Returns:
None
"""
# My Code restructured by ChatGPT
processed_img = self._process_image(
image_input_file,
resize,
watermark,
font_size,
grayscale,
brightness,
contrast,
)
# Handle EXIF metadata
exif_piexif_format = self._handle_exif(
image = processed_img,
file_name = image_output_file,
dict_for_exif = dict_for_exif,
gps = gps,
copy_exif = copy_exif
)
# Save the 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,
)
def process_image_object(
self,
image_input_file: str,
resize: int = None,
watermark: str = None,
font_size: int = 2,
grayscale: bool = False,
brightness: float = None,
contrast: float = None
):
"""
Processes an image with the given parameters and returns the modified image object.
Args:
image_input_file (str): Path to the input image file.
resize (int, optional): Resize percentage. Defaults to None.
watermark (str, optional): Watermark text to add. Defaults to None.
font_size (int): Font size for the watermark. Defaults to 2.
grayscale (bool): Convert image to grayscale. Defaults to False.
brightness (float, optional): Adjust brightness. Defaults to None.
contrast (float, optional): Adjust contrast. Defaults to None.
Returns:
Image: The processed image object.
"""
# My Code restructured by ChatGPT
processed_img = self._process_image(
image_input_file,
resize,
watermark,
font_size,
grayscale,
brightness,
contrast,
)
return self.image_processor.convert_pil_to_qtimage(processed_img)
def insert_exif_to_image(self, exif_dict: dict, image_path: str, gps: tuple[float, float] = None) -> None:
"""
Inserts EXIF metadata into an image.
Args:
exif_data (dict): A dictionary containing EXIF metadata as key-value pairs (e.g., strings, integers).
image_path (str): Absolute path to the target image file.
gps (tuple[float, float], optional): GPS coordinates as a tuple (latitude, longitude). Defaults to None.
Returns:
None: The function modifies the image file in place.
"""
# Restructured by ChatGPT
image_name, ending = os.path.splitext(os.path.basename(image_path))
img = self.image_processor.open_image(image_path)
selected_exif = exif_dict
if "date_time_original" in exif_dict:
selected_exif = self._modify_timestamp_in_exif(selected_exif, image_name)
exif_piexif_format = self.exif_handler.build_exif_bytes(
selected_exif, self.image_processor.get_image_size(img)
)
# GPS data
if gps is not None:
latitude = gps[0]
longitude = gps[1]
exif_piexif_format = self.exif_handler.add_geolocation_to_exif(exif_piexif_format, latitude, longitude)
self.exif_handler.insert_exif(exif_dict = exif_piexif_format, img_path = image_path)

View file

@ -1,6 +1,6 @@
from PIL import Image, ImageDraw, ImageFont, ImageEnhance from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageQt
import piexif import piexif
import time import piexif.helper
from fractions import Fraction from fractions import Fraction
class ImageProcessor: class ImageProcessor:
@ -21,11 +21,13 @@ class ImageProcessor:
return image.convert("L") return image.convert("L")
def change_contrast(self, image, change): def change_contrast(self, image, change):
"""Change contrast by percent."""
enhancer = ImageEnhance.Contrast(image) enhancer = ImageEnhance.Contrast(image)
new_img = enhancer.enhance(1 + (change/100)) new_img = enhancer.enhance(1 + (change/100))
return new_img return new_img
def change_brightness(self, image, change): def change_brightness(self, image, change):
"""Changes brightness by percent"""
enhancer = ImageEnhance.Brightness(image) enhancer = ImageEnhance.Brightness(image)
new_img = enhancer.enhance(1 + (change/100)) new_img = enhancer.enhance(1 + (change/100))
return new_img return new_img
@ -33,24 +35,20 @@ class ImageProcessor:
def resize_image(self, image, percent, resample = True): def resize_image(self, image, percent, resample = True):
"""Resize an image by giving a percent.""" """Resize an image by giving a percent."""
new_size = tuple(int(x * (percent / 100)) for x in image.size) new_size = tuple(int(x * (percent / 100)) for x in image.size)
if resample:
resized_image = image.resize(new_size) resized_image = image.resize(new_size)
else:
resized_image = image.resize((new_size),resample=Image.Resampling.NEAREST)
return resized_image return resized_image
def add_watermark(self, image, text, font_size_percentage): def add_watermark(self, image, text, font_size_percentage):
# Still not happy about this function.. """Addes a watermark to the image using default os font."""
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_percentage / 100) font_size = (imagewidth / 100) * font_size_percentage
try: # Try loading front, if notaviable return unmodified image try: # Try loading front, if notaviable return unmodified image
font = ImageFont.load_default(font_size) font = ImageFont.load_default(font_size)
except Exception as e: except Exception as e:
print(f"Error {e}\nloading font for watermark, please ensure font is installed...\n") print(f"Error {e}\nloading font for watermark, please ensure font is installed...\n")
time.sleep(0.1)
return image 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
@ -67,12 +65,11 @@ class ImageProcessor:
return image return image
def save_image(self, image, path, file_type, jpg_quality, png_compressing, optimize, exif_data): def save_image(self, image, path, file_type, jpg_quality, png_compressing, optimize, piexif_exif_data):
# partly optimized by chatGPT # partly optimized by chatGPT
""" """
Save an image to the specified path with optional EXIF data and optimization. Save an image to the specified path with optional EXIF data.
""" """
file_type = file_type.lower()
save_params = {"optimize": optimize} save_params = {"optimize": optimize}
# Add file-specific parameters # Add file-specific parameters
if file_type == "jpg" or "webp": if file_type == "jpg" or "webp":
@ -80,19 +77,22 @@ class ImageProcessor:
elif file_type == "png": elif file_type == "png":
save_params["compress_level"] = png_compressing save_params["compress_level"] = png_compressing
elif file_type not in ["webp", "jpg", "png"]: elif file_type not in ["webp", "jpg", "png"]:
input(f"Type: {file_type} is not supported. Press Enter to continue...") print(f"Type: {file_type} is not supported.")
return return
# Add EXIF data if available # Add EXIF data if available
if exif_data is not None: if piexif_exif_data is not None:
save_params["exif"] = piexif.dump(exif_data) save_params["exif"] = piexif.dump(piexif_exif_data)
if file_type == "webp": if file_type == "webp":
print("File format webp does not support all exif features, some information might get lost...\n") print("File format webp does not support all exif features, some information might get lost...\n")
time.sleep(0.1)
try: try:
image.save(f"{path}.{file_type}", **save_params) image.save(f"{path}.{file_type}", **save_params)
except Exception as e: except Exception as e:
print(f"Failed to save image: {e}") print(f"Failed to save image: {e}")
def convert_pil_to_qtimage(self, pillow_image):
qt_image = ImageQt.ImageQt(pillow_image)
return qt_image
class ExifHandler: class ExifHandler:
"""Function using piexif are here.""" """Function using piexif are here."""
def __init__(self): def __init__(self):
@ -101,8 +101,8 @@ class ExifHandler:
def get_exif_info(self, image): def get_exif_info(self, image):
return(piexif.load(image.info['exif'])) return(piexif.load(image.info['exif']))
def build_exif_dict(self, user_data, imagesize): def build_exif_bytes(self, user_data, imagesize):
"""Build a piexif-compatible EXIF dictionary from user data.""" """Build a piexif-compatible EXIF dictionary from a dicts."""
# Mostly made by ChatGPT, some adjustment # Mostly made by ChatGPT, some adjustment
zeroth_ifd = { zeroth_ifd = {
piexif.ImageIFD.Make: user_data["make"].encode("utf-8"), piexif.ImageIFD.Make: user_data["make"].encode("utf-8"),
@ -115,7 +115,8 @@ class ExifHandler:
piexif.ImageIFD.YResolution: (72, 1), piexif.ImageIFD.YResolution: (72, 1),
} }
exif_ifd = { exif_ifd = {
piexif.ExifIFD.UserComment: user_data["user_comment"].encode("utf-8"), piexif.ExifIFD.LensModel: user_data["lens"].encode("utf-8"),
piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(user_data["user_comment"]),
piexif.ExifIFD.ISOSpeedRatings: int(user_data["iso"].encode("utf-8")), piexif.ExifIFD.ISOSpeedRatings: int(user_data["iso"].encode("utf-8")),
piexif.ExifIFD.PixelXDimension: imagesize[0], piexif.ExifIFD.PixelXDimension: imagesize[0],
piexif.ExifIFD.PixelYDimension: imagesize[1], piexif.ExifIFD.PixelYDimension: imagesize[1],
@ -125,7 +126,7 @@ class ExifHandler:
return {"0th": zeroth_ifd, "Exif": exif_ifd} return {"0th": zeroth_ifd, "Exif": exif_ifd}
def deg_to_dms(self, decimal_coordinate, cardinal_directions): def _deg_to_dms(self, decimal_coordinate, cardinal_directions):
""" """
This function converts decimal coordinates into the DMS (degrees, minutes and seconds) format. This function converts decimal coordinates into the DMS (degrees, minutes and seconds) format.
It also determines the cardinal direction of the coordinates. It also determines the cardinal direction of the coordinates.
@ -147,7 +148,7 @@ class ExifHandler:
seconds = Fraction((decimal_minutes - minutes) * 60).limit_denominator(100) seconds = Fraction((decimal_minutes - minutes) * 60).limit_denominator(100)
return degrees, minutes, seconds, compass_direction return degrees, minutes, seconds, compass_direction
def dms_to_exif_format(self, dms_degrees, dms_minutes, dms_seconds): def _dms_to_exif_format(self, dms_degrees, dms_minutes, dms_seconds):
""" """
This function converts DMS (degrees, minutes and seconds) to values that can This function converts DMS (degrees, minutes and seconds) to values that can
be used with the EXIF (Exchangeable Image File Format). be used with the EXIF (Exchangeable Image File Format).
@ -176,12 +177,12 @@ class ExifHandler:
:param longitude: the eastwest position coordinate :param longitude: the eastwest position coordinate
""" """
# converts the latitude and longitude coordinates to DMS # converts the latitude and longitude coordinates to DMS
latitude_dms = self.deg_to_dms(latitude, ["S", "N"]) latitude_dms = self._deg_to_dms(latitude, ["S", "N"])
longitude_dms = self.deg_to_dms(longitude, ["W", "E"]) longitude_dms = self._deg_to_dms(longitude, ["W", "E"])
# convert the DMS values to EXIF values # convert the DMS values to EXIF values
exif_latitude = self.dms_to_exif_format(latitude_dms[0], latitude_dms[1], latitude_dms[2]) 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]) exif_longitude = self._dms_to_exif_format(longitude_dms[0], longitude_dms[1], longitude_dms[2])
try: try:
# https://exiftool.org/TagNames/GPS.html # https://exiftool.org/TagNames/GPS.html
@ -199,3 +200,7 @@ class ExifHandler:
return exif_data return exif_data
except Exception as e: except Exception as e:
print(f"Error: {str(e)}") print(f"Error: {str(e)}")
def insert_exif(self, exif_dict, img_path):
exif_bytes = piexif.dump(exif_dict)
piexif.insert(exif_bytes, img_path)

327
tui.py
View file

@ -1,327 +0,0 @@
import os
from datetime import datetime
# my packages
from optima.optima35 import OPTIMA35
from utils.utility import Utilities
from ui.simple_tui import SimpleTUI
class Optima35TUI():
def __init__(self, exif_file, settings_file):
self.o = OPTIMA35()
self.u = Utilities()
self.tui = SimpleTUI()
self.exif_file = exif_file
self.exif_data = self.u.read_yaml(exif_file)
self.setting_file = settings_file
self.settings = {
"input_folder": None,
"output_folder": None,
"file_format": None,
"resize": None,
"copy_exif": None,
"contrast": None,
"brightness": None,
"new_file_names": None,
"invert_image_order": False,
"watermark": None,
"gps": None,
"modifications": [],
}
self.settings_to_save = [
"resize",
"jpg_quality",
"png_compression",
"optimize",
"contrast",
"brightness"
]
def process(self):
if "Change EXIF" in self.settings["modifications"]:
self.selected_exif = self.collect_exif_data()
self.check_options() # Get all user selected data
input_folder_valid = os.path.exists(self.o.settings["input_folder"])
output_folder_valid = os.path.exists(self.o.settings["output_folder"])
if not input_folder_valid or not output_folder_valid:
print("Warning", f"Input location {input_folder_valid}\nOutput folder {output_folder_valid}...")
return
input_folder = self.o.settings["input_folder"]
output_folder = self.o.settings["output_folder"]
image_files = [
f for f in os.listdir(input_folder) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp"))
]
i = 1
for image_file in image_files:
input_path = os.path.join(input_folder, image_file)
if self.o.settings["new_file_names"] != False:
image_name = self.o.name_images(self.o.settings["new_file_names"], i, len(image_files), self.o.settings["invert_image_order"])
else:
image_name = os.path.splitext(image_file)[0]
output_path = os.path.join(output_folder, image_name)
self.o.process(input_path, output_path)
self.u.progress_bar(i, len(image_files))
i += 1
def check_options(self):
try:
if "Resize image" in self.settings["modifications"]:
self.o.settings["resize"] = self.settings["resize"]
else:
self.o.settings["resize"] = False
if "Convert to grayscale" in self.settings["modifications"]:
self.o.settings["grayscale"] = True
else:
self.o.settings["grayscale"] = False
if "Change contrast" in self.settings["modifications"]:
self.o.settings["contrast"] = self.settings["contrast"]
else:
self.o.settings["contrast"] = False
if "Change brightness" in self.settings["modifications"]:
self.o.settings["brightness"] = self.settings["brightness"]
else:
self.o.settings["brightness"] = False
if "Rename images" in self.settings["modifications"]:
self.o.settings["new_file_names"] = self.settings["new_file_names"]
else:
self.o.settings["new_file_names"] = False
if "Invert image order" in self.settings["modifications"]:
self.o.settings["invert_image_order"] = True
else:
self.o.settings["invert_image_order"] = False
if "Add Watermark" in self.settings["modifications"]:
self.o.settings["watermark"] = self.settings["watermark"]
else:
self.o.settings["watermark"] = False
self.o.settings["optimize"] = self.settings["optimize"]
self.o.settings["png_compression"] = self.settings["png_compression"]
self.o.settings["jpg_quality"] = self.settings["jpg_quality"]
self.o.settings["input_folder"] = self.settings["input_folder"]
self.o.settings["output_folder"] = self.settings["output_folder"]
self.o.settings["file_format"] = self.settings["file_format"]
self.o.settings["font_size"] = 2 # need to add option to select size
self.o.settings["copy_exif"] = self.settings["copy_exif"]
if "Change EXIF" in self.settings["modifications"]: #missing
self.o.selected_exif = self.selected_exif #
self.o.settings["own_exif"] = True
if self.settings["gps"] != None:
self.o.settings["gps"] = self.settings["gps"]
else:
self.o.settings["gps"] = False
else:
self.o.settings["own_exif"] = False
except Exception as e:
print(f"Whoops: {e}")
def load_or_ask_settings(self):
"""Load settings from a YAML file or ask the user if not present or incomplete."""
try:
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...")
self.ask_for_settings()
except Exception as e:
print(f"Error: {e}")
self.ask_for_settings()
def ask_for_settings(self):
print("Asking for new settings...\n")
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)
self.settings["jpg_quality"] = self.take_input_and_validate(question = "JPEG quality (1-100, 80 default): ", accepted_type = int, min_value = 1, max_value = 100)
self.settings["png_compression"] = self.take_input_and_validate(question = "PNG compression level (0-9, 6 default): ", accepted_type = int, min_value = 0, max_value = 9)
self.settings["optimize"] = self.tui.yes_no_menu("Optimize images i.e. compressing?")
self.write_settings(self.settings_to_save)
def write_settings(self, keys_to_save):
""""Write self.setting, but only specific values"""
keys = keys_to_save
filtered_settings = {key: self.settings[key] for key in keys if key in self.settings}
self.u.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.u.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_data[field])
user_data[field] = choise
user_data["software"] = f"{self.o.name} {self.o.version}"
new_date = self.get_date_input()
if new_date:
user_data["date_time_original"] = new_date
self.settings["gps"] = self.get_gps_input(user_data)
return user_data
def get_gps_input(self, test_exif):
while True:
lat = input("Enter Latitude (xx.xxxxxx): ")
if lat == "":
return False
long = input("Enter Longitude (xx.xxxxxx): ")
try:
self.o.exif_handler.add_geolocation_to_exif(test_exif, float(lat), float(long))
return [lat, long]
except Exception:
print("Invalid GPS formate, try again...")
def get_date_input(self):
# Partially chatGPT
while True:
date_input = input("Enter a date (yyyy-mm-dd): ")
if date_input == "":
return None # Skip if input is empty
try:
new_date = datetime.strptime(date_input, "%Y-%m-%d")
return new_date.strftime("%Y:%m:%d 00:00:00")
except ValueError:
print("Invalid date format. Please enter the date in yyyy-mm-dd format.")
def get_user_settings(self):
"""Get initial settings from the user."""
menu_options = [
"Resize image",
"Change EXIF",
"Convert to grayscale",
"Change contrast",
"Change brightness",
"Rename images",
"Invert image order",
"Add Watermark"
] # new option can be added here.
self.settings["input_folder"] = input("Enter path of input folder: ").strip() # Add: check if folder exists.
self.settings["output_folder"] = input("Enter path of output folder: ").strip()
self.settings["file_format"] = self.take_input_and_validate(question = "Enter export file format (jpg, png, webp): ", accepted_input = ["jpg", "png", "webp"], accepted_type = str)
self.settings["modifications"] = self.tui.multi_select_menu(
f"\n{self.o.name} v.{self.o.version} \nSelect what you want to do (esc or q to exit)",
menu_options
)
if "Change EXIF" not in self.settings["modifications"]:
self.settings["copy_exif"] = self.tui.yes_no_menu("Do you want to copy exif info from original file?")
if "Rename images" in self.settings["modifications"]:
self.settings["new_file_names"] = input("What should be the name for the new images? ") # Need
else:
self.settings["new_file_names"] = False
if "Invert image order" in self.settings["modifications"]:
self.settings["invert_image_order"] = True
else:
self.settings["invert_image_order"] = False
if "Add Watermark" in self.settings["modifications"]:
self.settings["watermark"] = input("Enter text for watermark. ")
else:
self.settings["watermark"] = False
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 run(self):
"""Run the main program."""
self.load_or_ask_settings()
self.get_user_settings()
self.process()
print("Done")
def main(exif_file, config_file):
app = Optima35TUI(exif_file, config_file)
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")

View file

View file

@ -1,87 +0,0 @@
from PySide6.QtCore import Signal
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QComboBox, QListWidget,
QLineEdit, QHBoxLayout, QPushButton, QMessageBox
)
# By ChatGPT
class ExifEditor(QMainWindow):
# Signal to emit the updated EXIF data
exif_data_updated = Signal(dict)
def __init__(self, exif_data):
super().__init__()
self.exif_data = exif_data
self.current_key = None
self.setWindowTitle("EXIF Editor")
self.resize(400, 300)
# Main widget and layout
main_widget = QWidget()
main_layout = QVBoxLayout()
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
# ComboBox to select lists
self.combo_box = QComboBox()
self.combo_box.addItems(self.exif_data.keys())
self.combo_box.currentTextChanged.connect(self.load_list)
main_layout.addWidget(self.combo_box)
# List widget to display items
self.list_widget = QListWidget()
main_layout.addWidget(self.list_widget)
# Line edit for adding items
self.line_edit = QLineEdit()
self.line_edit.setPlaceholderText("Enter new item...")
main_layout.addWidget(self.line_edit)
# Buttons: Add, Delete, Cancel
button_layout = QHBoxLayout()
self.add_button = QPushButton("Add")
self.add_button.clicked.connect(self.add_item)
self.delete_button = QPushButton("Delete")
self.delete_button.clicked.connect(self.delete_item)
self.cancel_button = QPushButton("Close")
self.cancel_button.clicked.connect(self.close_editor)
button_layout.addWidget(self.add_button)
button_layout.addWidget(self.delete_button)
button_layout.addWidget(self.cancel_button)
main_layout.addLayout(button_layout)
# Load the first list by default
self.load_list(self.combo_box.currentText())
def load_list(self, key):
"""Load the selected list into the list widget."""
self.current_key = key
self.list_widget.clear()
if key in self.exif_data:
self.list_widget.addItems(self.exif_data[key])
def add_item(self):
"""Add a new item to the selected list."""
new_item = self.line_edit.text().strip()
if new_item:
self.exif_data[self.current_key].append(new_item)
self.list_widget.addItem(new_item)
self.line_edit.clear()
else:
QMessageBox.warning(self, "Warning", "Cannot add an empty item.")
def delete_item(self):
"""Delete the selected item from the list."""
selected_item = self.list_widget.currentItem()
if selected_item:
item_text = selected_item.text()
self.exif_data[self.current_key].remove(item_text)
self.list_widget.takeItem(self.list_widget.row(selected_item))
else:
QMessageBox.warning(self, "Warning", "No item selected to delete.")
def close_editor(self):
"""Emit the updated exif_data and close the editor."""
self.exif_data_updated.emit(self.exif_data)
self.close()

View file

@ -1,553 +0,0 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'main_window.ui'
##
## Created by: Qt User Interface Compiler version 6.8.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QDateEdit,
QFrame, QGridLayout, QGroupBox, QHBoxLayout,
QLabel, QLineEdit, QMainWindow, QProgressBar,
QPushButton, QSizePolicy, QSpinBox, QStatusBar,
QTabWidget, QVBoxLayout, QWidget)
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(450, 708)
MainWindow.setMinimumSize(QSize(350, 677))
MainWindow.setMaximumSize(QSize(500, 1000))
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName(u"centralwidget")
self.gridLayout = QGridLayout(self.centralwidget)
self.gridLayout.setObjectName(u"gridLayout")
self.tabWidget = QTabWidget(self.centralwidget)
self.tabWidget.setObjectName(u"tabWidget")
self.tabWidget.setMaximumSize(QSize(500, 16777215))
self.tab_1 = QWidget()
self.tab_1.setObjectName(u"tab_1")
self.verticalLayout_10 = QVBoxLayout(self.tab_1)
self.verticalLayout_10.setObjectName(u"verticalLayout_10")
self.folder_group = QFrame(self.tab_1)
self.folder_group.setObjectName(u"folder_group")
self.folder_group.setMaximumSize(QSize(400, 16777215))
self.gridLayout_5 = QGridLayout(self.folder_group)
self.gridLayout_5.setObjectName(u"gridLayout_5")
self.input_path = QLineEdit(self.folder_group)
self.input_path.setObjectName(u"input_path")
self.gridLayout_5.addWidget(self.input_path, 0, 0, 1, 1)
self.output_path = QLineEdit(self.folder_group)
self.output_path.setObjectName(u"output_path")
self.gridLayout_5.addWidget(self.output_path, 0, 1, 1, 1)
self.input_folder_button = QPushButton(self.folder_group)
self.input_folder_button.setObjectName(u"input_folder_button")
self.gridLayout_5.addWidget(self.input_folder_button, 1, 0, 1, 1)
self.output_folder_button = QPushButton(self.folder_group)
self.output_folder_button.setObjectName(u"output_folder_button")
self.gridLayout_5.addWidget(self.output_folder_button, 1, 1, 1, 1)
self.verticalLayout_10.addWidget(self.folder_group)
self.groupBox = QGroupBox(self.tab_1)
self.groupBox.setObjectName(u"groupBox")
self.groupBox.setMaximumSize(QSize(400, 16777215))
self.gridLayout_4 = QGridLayout(self.groupBox)
self.gridLayout_4.setObjectName(u"gridLayout_4")
self.resize_checkbox = QCheckBox(self.groupBox)
self.resize_checkbox.setObjectName(u"resize_checkbox")
self.gridLayout_4.addWidget(self.resize_checkbox, 0, 0, 1, 1)
self.resize_spinBox = QSpinBox(self.groupBox)
self.resize_spinBox.setObjectName(u"resize_spinBox")
self.resize_spinBox.setEnabled(False)
self.resize_spinBox.setMinimum(1)
self.resize_spinBox.setMaximum(200)
self.resize_spinBox.setSingleStep(1)
self.resize_spinBox.setValue(80)
self.gridLayout_4.addWidget(self.resize_spinBox, 0, 1, 1, 1)
self.image_type = QComboBox(self.groupBox)
self.image_type.addItem(u"jpg")
self.image_type.addItem(u"png")
self.image_type.addItem(u"webp")
self.image_type.setObjectName(u"image_type")
self.gridLayout_4.addWidget(self.image_type, 1, 0, 1, 1)
self.jpg_quality_spinBox = QSpinBox(self.groupBox)
self.jpg_quality_spinBox.setObjectName(u"jpg_quality_spinBox")
self.jpg_quality_spinBox.setMinimum(1)
self.jpg_quality_spinBox.setMaximum(100)
self.jpg_quality_spinBox.setValue(80)
self.gridLayout_4.addWidget(self.jpg_quality_spinBox, 1, 1, 1, 1)
self.png_quality_spinBox = QSpinBox(self.groupBox)
self.png_quality_spinBox.setObjectName(u"png_quality_spinBox")
self.png_quality_spinBox.setEnabled(True)
self.png_quality_spinBox.setMinimum(1)
self.png_quality_spinBox.setMaximum(9)
self.png_quality_spinBox.setValue(6)
self.gridLayout_4.addWidget(self.png_quality_spinBox, 1, 2, 1, 1)
self.optimize_checkBox = QCheckBox(self.groupBox)
self.optimize_checkBox.setObjectName(u"optimize_checkBox")
self.gridLayout_4.addWidget(self.optimize_checkBox, 0, 2, 1, 1)
self.png_quality_spinBox.raise_()
self.resize_checkbox.raise_()
self.resize_spinBox.raise_()
self.image_type.raise_()
self.jpg_quality_spinBox.raise_()
self.optimize_checkBox.raise_()
self.verticalLayout_10.addWidget(self.groupBox)
self.groupBox_2 = QGroupBox(self.tab_1)
self.groupBox_2.setObjectName(u"groupBox_2")
self.groupBox_2.setMaximumSize(QSize(400, 16777215))
self.gridLayout_3 = QGridLayout(self.groupBox_2)
self.gridLayout_3.setObjectName(u"gridLayout_3")
self.watermark_lineEdit = QLineEdit(self.groupBox_2)
self.watermark_lineEdit.setObjectName(u"watermark_lineEdit")
self.watermark_lineEdit.setEnabled(False)
self.gridLayout_3.addWidget(self.watermark_lineEdit, 3, 0, 1, 3)
self.brightness_checkbox = QCheckBox(self.groupBox_2)
self.brightness_checkbox.setObjectName(u"brightness_checkbox")
self.gridLayout_3.addWidget(self.brightness_checkbox, 0, 0, 1, 1)
self.grayscale_checkBox = QCheckBox(self.groupBox_2)
self.grayscale_checkBox.setObjectName(u"grayscale_checkBox")
self.gridLayout_3.addWidget(self.grayscale_checkBox, 0, 2, 1, 1)
self.contrast_spinBox = QSpinBox(self.groupBox_2)
self.contrast_spinBox.setObjectName(u"contrast_spinBox")
self.contrast_spinBox.setEnabled(False)
self.contrast_spinBox.setMinimum(-100)
self.contrast_spinBox.setMaximum(100)
self.contrast_spinBox.setValue(10)
self.gridLayout_3.addWidget(self.contrast_spinBox, 1, 1, 1, 1)
self.watermark_checkbox = QCheckBox(self.groupBox_2)
self.watermark_checkbox.setObjectName(u"watermark_checkbox")
self.gridLayout_3.addWidget(self.watermark_checkbox, 2, 0, 1, 1)
self.brightness_spinBox = QSpinBox(self.groupBox_2)
self.brightness_spinBox.setObjectName(u"brightness_spinBox")
self.brightness_spinBox.setEnabled(False)
self.brightness_spinBox.setMinimum(-100)
self.brightness_spinBox.setMaximum(100)
self.brightness_spinBox.setValue(-10)
self.gridLayout_3.addWidget(self.brightness_spinBox, 0, 1, 1, 1)
self.contrast_checkbox = QCheckBox(self.groupBox_2)
self.contrast_checkbox.setObjectName(u"contrast_checkbox")
self.gridLayout_3.addWidget(self.contrast_checkbox, 1, 0, 1, 1)
self.font_size_comboBox = QComboBox(self.groupBox_2)
self.font_size_comboBox.addItem("")
self.font_size_comboBox.addItem("")
self.font_size_comboBox.addItem("")
self.font_size_comboBox.addItem("")
self.font_size_comboBox.addItem("")
self.font_size_comboBox.setObjectName(u"font_size_comboBox")
self.gridLayout_3.addWidget(self.font_size_comboBox, 2, 1, 1, 1)
self.verticalLayout_10.addWidget(self.groupBox_2)
self.rename_group = QGroupBox(self.tab_1)
self.rename_group.setObjectName(u"rename_group")
self.rename_group.setMaximumSize(QSize(400, 16777215))
self.gridLayout_6 = QGridLayout(self.rename_group)
self.gridLayout_6.setObjectName(u"gridLayout_6")
self.rename_checkbox = QCheckBox(self.rename_group)
self.rename_checkbox.setObjectName(u"rename_checkbox")
self.gridLayout_6.addWidget(self.rename_checkbox, 0, 0, 1, 1)
self.revert_checkbox = QCheckBox(self.rename_group)
self.revert_checkbox.setObjectName(u"revert_checkbox")
self.gridLayout_6.addWidget(self.revert_checkbox, 0, 1, 1, 1)
self.filename = QLineEdit(self.rename_group)
self.filename.setObjectName(u"filename")
self.filename.setEnabled(False)
self.gridLayout_6.addWidget(self.filename, 1, 0, 1, 2)
self.verticalLayout_10.addWidget(self.rename_group)
self.widget_9 = QWidget(self.tab_1)
self.widget_9.setObjectName(u"widget_9")
self.widget_9.setMaximumSize(QSize(400, 50))
self.horizontalLayout_3 = QHBoxLayout(self.widget_9)
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
self.restart_button = QPushButton(self.widget_9)
self.restart_button.setObjectName(u"restart_button")
self.restart_button.setEnabled(False)
self.restart_button.setFlat(False)
self.horizontalLayout_3.addWidget(self.restart_button)
self.progressBar = QProgressBar(self.widget_9)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setEnabled(True)
self.progressBar.setValue(0)
self.horizontalLayout_3.addWidget(self.progressBar)
self.start_button = QPushButton(self.widget_9)
self.start_button.setObjectName(u"start_button")
self.start_button.setEnabled(True)
self.horizontalLayout_3.addWidget(self.start_button)
self.verticalLayout_10.addWidget(self.widget_9)
self.tabWidget.addTab(self.tab_1, "")
self.tab_2 = QWidget()
self.tab_2.setObjectName(u"tab_2")
self.verticalLayout_9 = QVBoxLayout(self.tab_2)
self.verticalLayout_9.setObjectName(u"verticalLayout_9")
self.exif_group = QGroupBox(self.tab_2)
self.exif_group.setObjectName(u"exif_group")
self.horizontalLayout = QHBoxLayout(self.exif_group)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.exif_checkbox = QCheckBox(self.exif_group)
self.exif_checkbox.setObjectName(u"exif_checkbox")
self.exif_checkbox.setEnabled(True)
self.horizontalLayout.addWidget(self.exif_checkbox)
self.exif_copy_checkBox = QCheckBox(self.exif_group)
self.exif_copy_checkBox.setObjectName(u"exif_copy_checkBox")
self.horizontalLayout.addWidget(self.exif_copy_checkBox)
self.edit_exif_button = QPushButton(self.exif_group)
self.edit_exif_button.setObjectName(u"edit_exif_button")
self.edit_exif_button.setEnabled(False)
self.horizontalLayout.addWidget(self.edit_exif_button)
self.verticalLayout_9.addWidget(self.exif_group)
self.exif_options_group = QGroupBox(self.tab_2)
self.exif_options_group.setObjectName(u"exif_options_group")
self.exif_options_group.setEnabled(False)
self.gridLayout_7 = QGridLayout(self.exif_options_group)
self.gridLayout_7.setObjectName(u"gridLayout_7")
self.widget_7 = QWidget(self.exif_options_group)
self.widget_7.setObjectName(u"widget_7")
self.verticalLayout_7 = QVBoxLayout(self.widget_7)
self.verticalLayout_7.setObjectName(u"verticalLayout_7")
self.label_7 = QLabel(self.widget_7)
self.label_7.setObjectName(u"label_7")
self.verticalLayout_7.addWidget(self.label_7)
self.artist_comboBox = QComboBox(self.widget_7)
self.artist_comboBox.setObjectName(u"artist_comboBox")
self.verticalLayout_7.addWidget(self.artist_comboBox)
self.gridLayout_7.addWidget(self.widget_7, 3, 0, 1, 1)
self.widget_4 = QWidget(self.exif_options_group)
self.widget_4.setObjectName(u"widget_4")
self.verticalLayout_4 = QVBoxLayout(self.widget_4)
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
self.label_4 = QLabel(self.widget_4)
self.label_4.setObjectName(u"label_4")
self.verticalLayout_4.addWidget(self.label_4)
self.iso_comboBox = QComboBox(self.widget_4)
self.iso_comboBox.setObjectName(u"iso_comboBox")
self.verticalLayout_4.addWidget(self.iso_comboBox)
self.gridLayout_7.addWidget(self.widget_4, 1, 1, 1, 1)
self.widget_6 = QWidget(self.exif_options_group)
self.widget_6.setObjectName(u"widget_6")
self.verticalLayout_6 = QVBoxLayout(self.widget_6)
self.verticalLayout_6.setObjectName(u"verticalLayout_6")
self.label_6 = QLabel(self.widget_6)
self.label_6.setObjectName(u"label_6")
self.verticalLayout_6.addWidget(self.label_6)
self.user_comment_comboBox = QComboBox(self.widget_6)
self.user_comment_comboBox.setObjectName(u"user_comment_comboBox")
self.verticalLayout_6.addWidget(self.user_comment_comboBox)
self.gridLayout_7.addWidget(self.widget_6, 2, 1, 1, 1)
self.widget_2 = QWidget(self.exif_options_group)
self.widget_2.setObjectName(u"widget_2")
self.verticalLayout_2 = QVBoxLayout(self.widget_2)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.label_2 = QLabel(self.widget_2)
self.label_2.setObjectName(u"label_2")
self.verticalLayout_2.addWidget(self.label_2)
self.lens_comboBox = QComboBox(self.widget_2)
self.lens_comboBox.setObjectName(u"lens_comboBox")
self.verticalLayout_2.addWidget(self.lens_comboBox)
self.gridLayout_7.addWidget(self.widget_2, 1, 0, 1, 1)
self.widget_5 = QWidget(self.exif_options_group)
self.widget_5.setObjectName(u"widget_5")
self.verticalLayout_5 = QVBoxLayout(self.widget_5)
self.verticalLayout_5.setObjectName(u"verticalLayout_5")
self.label_5 = QLabel(self.widget_5)
self.label_5.setObjectName(u"label_5")
self.verticalLayout_5.addWidget(self.label_5)
self.image_description_comboBox = QComboBox(self.widget_5)
self.image_description_comboBox.setObjectName(u"image_description_comboBox")
self.verticalLayout_5.addWidget(self.image_description_comboBox)
self.gridLayout_7.addWidget(self.widget_5, 2, 0, 1, 1)
self.widget = QWidget(self.exif_options_group)
self.widget.setObjectName(u"widget")
self.verticalLayout = QVBoxLayout(self.widget)
self.verticalLayout.setObjectName(u"verticalLayout")
self.label = QLabel(self.widget)
self.label.setObjectName(u"label")
self.verticalLayout.addWidget(self.label)
self.make_comboBox = QComboBox(self.widget)
self.make_comboBox.setObjectName(u"make_comboBox")
self.verticalLayout.addWidget(self.make_comboBox)
self.gridLayout_7.addWidget(self.widget, 0, 0, 1, 1)
self.widget_3 = QWidget(self.exif_options_group)
self.widget_3.setObjectName(u"widget_3")
self.verticalLayout_3 = QVBoxLayout(self.widget_3)
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.label_3 = QLabel(self.widget_3)
self.label_3.setObjectName(u"label_3")
self.verticalLayout_3.addWidget(self.label_3)
self.model_comboBox = QComboBox(self.widget_3)
self.model_comboBox.setObjectName(u"model_comboBox")
self.verticalLayout_3.addWidget(self.model_comboBox)
self.gridLayout_7.addWidget(self.widget_3, 0, 1, 1, 1)
self.widget_8 = QWidget(self.exif_options_group)
self.widget_8.setObjectName(u"widget_8")
self.verticalLayout_8 = QVBoxLayout(self.widget_8)
self.verticalLayout_8.setObjectName(u"verticalLayout_8")
self.label_8 = QLabel(self.widget_8)
self.label_8.setObjectName(u"label_8")
self.verticalLayout_8.addWidget(self.label_8)
self.copyright_info_comboBox = QComboBox(self.widget_8)
self.copyright_info_comboBox.setObjectName(u"copyright_info_comboBox")
self.verticalLayout_8.addWidget(self.copyright_info_comboBox)
self.gridLayout_7.addWidget(self.widget_8, 3, 1, 1, 1)
self.verticalLayout_9.addWidget(self.exif_options_group)
self.gps_groupBox = QGroupBox(self.tab_2)
self.gps_groupBox.setObjectName(u"gps_groupBox")
self.gps_groupBox.setEnabled(False)
self.horizontalLayout_4 = QHBoxLayout(self.gps_groupBox)
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
self.gps_checkBox = QCheckBox(self.gps_groupBox)
self.gps_checkBox.setObjectName(u"gps_checkBox")
self.horizontalLayout_4.addWidget(self.gps_checkBox)
self.lat_lineEdit = QLineEdit(self.gps_groupBox)
self.lat_lineEdit.setObjectName(u"lat_lineEdit")
self.lat_lineEdit.setEnabled(False)
self.horizontalLayout_4.addWidget(self.lat_lineEdit)
self.long_lineEdit = QLineEdit(self.gps_groupBox)
self.long_lineEdit.setObjectName(u"long_lineEdit")
self.long_lineEdit.setEnabled(False)
self.horizontalLayout_4.addWidget(self.long_lineEdit)
self.verticalLayout_9.addWidget(self.gps_groupBox)
self.date_groupBox = QGroupBox(self.tab_2)
self.date_groupBox.setObjectName(u"date_groupBox")
self.date_groupBox.setEnabled(False)
self.horizontalLayout_2 = QHBoxLayout(self.date_groupBox)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.add_date_checkBox = QCheckBox(self.date_groupBox)
self.add_date_checkBox.setObjectName(u"add_date_checkBox")
self.horizontalLayout_2.addWidget(self.add_date_checkBox)
self.dateEdit = QDateEdit(self.date_groupBox)
self.dateEdit.setObjectName(u"dateEdit")
self.dateEdit.setEnabled(False)
self.dateEdit.setDateTime(QDateTime(QDate(2025, 1, 1), QTime(0, 0, 0)))
self.horizontalLayout_2.addWidget(self.dateEdit)
self.verticalLayout_9.addWidget(self.date_groupBox)
self.tabWidget.addTab(self.tab_2, "")
self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.statusBar = QStatusBar(MainWindow)
self.statusBar.setObjectName(u"statusBar")
MainWindow.setStatusBar(self.statusBar)
self.retranslateUi(MainWindow)
self.resize_checkbox.toggled.connect(self.resize_spinBox.setEnabled)
self.brightness_checkbox.toggled.connect(self.brightness_spinBox.setEnabled)
self.contrast_checkbox.toggled.connect(self.contrast_spinBox.setEnabled)
self.watermark_checkbox.toggled.connect(self.watermark_lineEdit.setEnabled)
self.rename_checkbox.toggled.connect(self.filename.setEnabled)
self.exif_checkbox.toggled.connect(self.exif_options_group.setEnabled)
self.exif_checkbox.toggled.connect(self.exif_copy_checkBox.setDisabled)
self.exif_copy_checkBox.toggled.connect(self.exif_checkbox.setDisabled)
self.exif_checkbox.toggled.connect(self.edit_exif_button.setEnabled)
self.add_date_checkBox.toggled.connect(self.dateEdit.setEnabled)
self.exif_checkbox.toggled.connect(self.date_groupBox.setEnabled)
self.exif_checkbox.toggled.connect(self.gps_groupBox.setEnabled)
self.gps_checkBox.toggled.connect(self.lat_lineEdit.setEnabled)
self.gps_checkBox.toggled.connect(self.long_lineEdit.setEnabled)
self.tabWidget.setCurrentIndex(0)
self.font_size_comboBox.setCurrentIndex(2)
self.restart_button.setDefault(False)
QMetaObject.connectSlotsByName(MainWindow)
# setupUi
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"OPTIMA-35", None))
self.input_path.setText("")
self.input_path.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Enter input folder", None))
self.output_path.setText("")
self.output_path.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Enter output folder", None))
self.input_folder_button.setText(QCoreApplication.translate("MainWindow", u"input", None))
self.output_folder_button.setText(QCoreApplication.translate("MainWindow", u"output", None))
self.groupBox.setTitle(QCoreApplication.translate("MainWindow", u"Essential group", None))
self.resize_checkbox.setText(QCoreApplication.translate("MainWindow", u"Resize", None))
self.optimize_checkBox.setText(QCoreApplication.translate("MainWindow", u"optimize", None))
self.groupBox_2.setTitle(QCoreApplication.translate("MainWindow", u"Extra stuff", None))
self.watermark_lineEdit.setText("")
self.watermark_lineEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Enter Watermark", None))
self.brightness_checkbox.setText(QCoreApplication.translate("MainWindow", u"Brightness", None))
self.grayscale_checkBox.setText(QCoreApplication.translate("MainWindow", u"Grayscale", None))
self.watermark_checkbox.setText(QCoreApplication.translate("MainWindow", u"Watermark", None))
self.contrast_checkbox.setText(QCoreApplication.translate("MainWindow", u"Contrast", None))
self.font_size_comboBox.setItemText(0, QCoreApplication.translate("MainWindow", u"Tiny", None))
self.font_size_comboBox.setItemText(1, QCoreApplication.translate("MainWindow", u"Small", None))
self.font_size_comboBox.setItemText(2, QCoreApplication.translate("MainWindow", u"Normal", None))
self.font_size_comboBox.setItemText(3, QCoreApplication.translate("MainWindow", u"Large", None))
self.font_size_comboBox.setItemText(4, QCoreApplication.translate("MainWindow", u"Huge", None))
self.font_size_comboBox.setCurrentText(QCoreApplication.translate("MainWindow", u"Normal", None))
self.rename_group.setTitle(QCoreApplication.translate("MainWindow", u"files", None))
self.rename_checkbox.setText(QCoreApplication.translate("MainWindow", u"Rename", None))
self.revert_checkbox.setText(QCoreApplication.translate("MainWindow", u"Revert order", None))
self.filename.setText("")
self.filename.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Enter file name", None))
self.restart_button.setText(QCoreApplication.translate("MainWindow", u"Restart", None))
self.start_button.setText(QCoreApplication.translate("MainWindow", u"Convert", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_1), QCoreApplication.translate("MainWindow", u"Main", None))
self.exif_group.setTitle(QCoreApplication.translate("MainWindow", u"EXIF EXPERIMENTAL", None))
self.exif_checkbox.setText(QCoreApplication.translate("MainWindow", u"own exif", None))
self.exif_copy_checkBox.setText(QCoreApplication.translate("MainWindow", u"copy exif", None))
self.edit_exif_button.setText(QCoreApplication.translate("MainWindow", u"edit exif", None))
self.exif_options_group.setTitle(QCoreApplication.translate("MainWindow", u"Must", None))
self.label_7.setText(QCoreApplication.translate("MainWindow", u"Artist", None))
self.label_4.setText(QCoreApplication.translate("MainWindow", u"ISO", None))
self.label_6.setText(QCoreApplication.translate("MainWindow", u"Scanner", None))
self.label_2.setText(QCoreApplication.translate("MainWindow", u"Lens", None))
self.label_5.setText(QCoreApplication.translate("MainWindow", u"Film", None))
self.label.setText(QCoreApplication.translate("MainWindow", u"Make", None))
self.make_comboBox.setCurrentText("")
self.make_comboBox.setPlaceholderText("")
self.label_3.setText(QCoreApplication.translate("MainWindow", u"Model", None))
self.label_8.setText(QCoreApplication.translate("MainWindow", u"Copyright", None))
self.gps_groupBox.setTitle(QCoreApplication.translate("MainWindow", u"GPS", None))
self.gps_checkBox.setText(QCoreApplication.translate("MainWindow", u"add gps", None))
self.lat_lineEdit.setText("")
self.lat_lineEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"latitude [S, N]", None))
self.long_lineEdit.setText("")
self.long_lineEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"longitude [W, E]", None))
self.date_groupBox.setTitle(QCoreApplication.translate("MainWindow", u"Optional", None))
self.add_date_checkBox.setText(QCoreApplication.translate("MainWindow", u"add date", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QCoreApplication.translate("MainWindow", u"EXIF", None))
# retranslateUi

View file

@ -1,913 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>450</width>
<height>708</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<height>677</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>1000</height>
</size>
</property>
<property name="windowTitle">
<string>OPTIMA-35</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="maximumSize">
<size>
<width>500</width>
<height>16777215</height>
</size>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_1">
<attribute name="title">
<string>Main</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_10">
<item>
<widget class="QFrame" name="folder_group">
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLineEdit" name="input_path">
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Enter input folder</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="output_path">
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Enter output folder</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="input_folder_button">
<property name="text">
<string>input</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="output_folder_button">
<property name="text">
<string>output</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<property name="title">
<string>Essential group</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QCheckBox" name="resize_checkbox">
<property name="text">
<string>Resize</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="resize_spinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>200</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="value">
<number>80</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QComboBox" name="image_type">
<item>
<property name="text">
<string notr="true">jpg</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">png</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">webp</string>
</property>
</item>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="jpg_quality_spinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>80</number>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QSpinBox" name="png_quality_spinBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>9</number>
</property>
<property name="value">
<number>6</number>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="optimize_checkBox">
<property name="text">
<string>optimize</string>
</property>
</widget>
</item>
</layout>
<zorder>png_quality_spinBox</zorder>
<zorder>resize_checkbox</zorder>
<zorder>resize_spinBox</zorder>
<zorder>image_type</zorder>
<zorder>jpg_quality_spinBox</zorder>
<zorder>optimize_checkBox</zorder>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<property name="title">
<string>Extra stuff</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="3" column="0" colspan="3">
<widget class="QLineEdit" name="watermark_lineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Enter Watermark</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="brightness_checkbox">
<property name="text">
<string>Brightness</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="grayscale_checkBox">
<property name="text">
<string>Grayscale</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="contrast_spinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimum">
<number>-100</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="watermark_checkbox">
<property name="text">
<string>Watermark</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="brightness_spinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimum">
<number>-100</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>-10</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="contrast_checkbox">
<property name="text">
<string>Contrast</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="font_size_comboBox">
<property name="currentText">
<string>Normal</string>
</property>
<property name="currentIndex">
<number>2</number>
</property>
<item>
<property name="text">
<string>Tiny</string>
</property>
</item>
<item>
<property name="text">
<string>Small</string>
</property>
</item>
<item>
<property name="text">
<string>Normal</string>
</property>
</item>
<item>
<property name="text">
<string>Large</string>
</property>
</item>
<item>
<property name="text">
<string>Huge</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="rename_group">
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<property name="title">
<string>files</string>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="QCheckBox" name="rename_checkbox">
<property name="text">
<string>Rename</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="revert_checkbox">
<property name="text">
<string>Revert order</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLineEdit" name="filename">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Enter file name</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_9" native="true">
<property name="maximumSize">
<size>
<width>400</width>
<height>50</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="restart_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Restart</string>
</property>
<property name="default">
<bool>false</bool>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="enabled">
<bool>true</bool>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="start_button">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Convert</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>EXIF</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QGroupBox" name="exif_group">
<property name="title">
<string>EXIF EXPERIMENTAL</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="exif_checkbox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>own exif</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="exif_copy_checkBox">
<property name="text">
<string>copy exif</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="edit_exif_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>edit exif</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="exif_options_group">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Must</string>
</property>
<layout class="QGridLayout" name="gridLayout_7">
<item row="3" column="0">
<widget class="QWidget" name="widget_7" native="true">
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Artist</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="artist_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="1" column="1">
<widget class="QWidget" name="widget_4" native="true">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>ISO</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="iso_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="2" column="1">
<widget class="QWidget" name="widget_6" native="true">
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Scanner</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="user_comment_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QWidget" name="widget_2" native="true">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Lens</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="lens_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QWidget" name="widget_5" native="true">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Film</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="image_description_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="0">
<widget class="QWidget" name="widget" native="true">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Make</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="make_comboBox">
<property name="currentText">
<string/>
</property>
<property name="placeholderText">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QWidget" name="widget_3" native="true">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Model</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="model_comboBox"/>
</item>
</layout>
</widget>
</item>
<item row="3" column="1">
<widget class="QWidget" name="widget_8" native="true">
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Copyright</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="copyright_info_comboBox"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gps_groupBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>GPS</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QCheckBox" name="gps_checkBox">
<property name="text">
<string>add gps</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lat_lineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>latitude [S, N]</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="long_lineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>longitude [W, E]</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="date_groupBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Optional</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="add_date_checkBox">
<property name="text">
<string>add date</string>
</property>
</widget>
</item>
<item>
<widget class="QDateEdit" name="dateEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="dateTime">
<datetime>
<hour>0</hour>
<minute>0</minute>
<second>0</second>
<year>2025</year>
<month>1</month>
<day>1</day>
</datetime>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusBar"/>
</widget>
<resources/>
<connections>
<connection>
<sender>resize_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>resize_spinBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>75</x>
<y>96</y>
</hint>
<hint type="destinationlabel">
<x>196</x>
<y>118</y>
</hint>
</hints>
</connection>
<connection>
<sender>brightness_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>brightness_spinBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>83</x>
<y>363</y>
</hint>
<hint type="destinationlabel">
<x>83</x>
<y>399</y>
</hint>
</hints>
</connection>
<connection>
<sender>contrast_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>contrast_spinBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>185</x>
<y>363</y>
</hint>
<hint type="destinationlabel">
<x>185</x>
<y>399</y>
</hint>
</hints>
</connection>
<connection>
<sender>watermark_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>watermark_lineEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>83</x>
<y>435</y>
</hint>
<hint type="destinationlabel">
<x>237</x>
<y>435</y>
</hint>
</hints>
</connection>
<connection>
<sender>rename_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>filename</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>105</x>
<y>522</y>
</hint>
<hint type="destinationlabel">
<x>182</x>
<y>560</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>exif_options_group</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>130</x>
<y>105</y>
</hint>
<hint type="destinationlabel">
<x>236</x>
<y>328</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>exif_copy_checkBox</receiver>
<slot>setDisabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>130</x>
<y>105</y>
</hint>
<hint type="destinationlabel">
<x>332</x>
<y>105</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_copy_checkBox</sender>
<signal>toggled(bool)</signal>
<receiver>exif_checkbox</receiver>
<slot>setDisabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>332</x>
<y>105</y>
</hint>
<hint type="destinationlabel">
<x>130</x>
<y>105</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>edit_exif_button</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>134</x>
<y>107</y>
</hint>
<hint type="destinationlabel">
<x>79</x>
<y>170</y>
</hint>
</hints>
</connection>
<connection>
<sender>add_date_checkBox</sender>
<signal>toggled(bool)</signal>
<receiver>dateEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>94</x>
<y>601</y>
</hint>
<hint type="destinationlabel">
<x>224</x>
<y>602</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>date_groupBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>126</x>
<y>103</y>
</hint>
<hint type="destinationlabel">
<x>224</x>
<y>589</y>
</hint>
</hints>
</connection>
<connection>
<sender>exif_checkbox</sender>
<signal>toggled(bool)</signal>
<receiver>gps_groupBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>94</x>
<y>103</y>
</hint>
<hint type="destinationlabel">
<x>224</x>
<y>535</y>
</hint>
</hints>
</connection>
<connection>
<sender>gps_checkBox</sender>
<signal>toggled(bool)</signal>
<receiver>lat_lineEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>72</x>
<y>547</y>
</hint>
<hint type="destinationlabel">
<x>192</x>
<y>547</y>
</hint>
</hints>
</connection>
<connection>
<sender>gps_checkBox</sender>
<signal>toggled(bool)</signal>
<receiver>long_lineEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>72</x>
<y>547</y>
</hint>
<hint type="destinationlabel">
<x>344</x>
<y>547</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -1,60 +0,0 @@
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

View file

View file

@ -1,46 +0,0 @@
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?")
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("")