Compare commits

...

15 commits
0.6.7 ... main

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
13 changed files with 424 additions and 250 deletions

View file

@ -1,69 +0,0 @@
---
include:
- local: .gitlab-ci/versioning/gitversion.yml
- local: .gitlab-ci/git/create_tag.yml
stages:
- build
- release
gitversion:
extends: .versioning:gitversion
stage: .pre
tags:
- gitlab-org-docker
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
build:
stage: build
image: python:3.9.21
tags:
- gitlab-org-docker
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
needs:
- job: gitversion
artifacts: true
script:
- sed -i "s/0.0.1/${GitVersion_MajorMinorPatch}/" src/optima35/__init__.py
- cat src/optima35/__init__.py
- python3 -m pip install build
- python3 -m build
artifacts:
paths:
- dist/*
expire_in: 1 day
publish:
stage: release
image: python:3.9.21
tags:
- gitlab-org-docker
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
variables:
TWINE_USERNAME: "__token__"
TWINE_PASSWORD: $TWINE_API
needs:
- job: build
artifacts: true
script:
- python3 -m pip install twine
- python3 -m twine upload dist/*
create_tag:
extends: .git:create_tag
stage: release
tags:
- gitlab-org-docker
variables:
VERSION: $GitVersion_SemVer
TOKEN: $GITLAB_TOKEN
needs:
- job: gitversion
artifacts: true
rules:
- if: $CI_COMMIT_TAG
when: never # Do not run this job when a tag is created manually
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch

View file

@ -1,15 +0,0 @@
---
.git:create_tag:
image: alpine:3.21
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0
GIT_LFS_SKIP_SMUDGE: 1
VERSION: ''
TOKEN: '' # Token with push privileges
script:
- apk add git
- git remote set-url origin https://oauth2:$TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH
- git tag $VERSION
- git push origin tag $VERSION

View file

@ -1,31 +0,0 @@
---
.versioning:gitversion:
image:
name: mcr.microsoft.com/dotnet/sdk:9.0
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0 # force a deep/non-shallow fetch need by gitversion
GIT_LFS_SKIP_SMUDGE: 1
cache: [] # caches and before / after scripts can mess things up
script:
- |
dotnet tool install --global GitVersion.Tool --version 5.*
export PATH="$PATH:/root/.dotnet/tools"
dotnet-gitversion -output buildserver
# We could just collect the output file gitversion.properties (with artifacts:report:dotenv: gitversion.properties as it is already in DOTENV format,
# however it contains ~33 variables which unnecessarily consumes many of the 50 max DOTENV variables of the free GitLab version.
# Limits are higher for licensed editions, see https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdotenv
grep 'GitVersion_LegacySemVer=' gitversion.properties >> gitversion.env
grep 'GitVersion_SemVer=' gitversion.properties >> gitversion.env
grep 'GitVersion_FullSemVer=' gitversion.properties >> gitversion.env
grep 'GitVersion_Major=' gitversion.properties >> gitversion.env
grep 'GitVersion_Minor=' gitversion.properties >> gitversion.env
grep 'GitVersion_Patch=' gitversion.properties >> gitversion.env
grep 'GitVersion_MajorMinorPatch=' gitversion.properties >> gitversion.env
grep 'GitVersion_BuildMetaData=' gitversion.properties >> gitversion.env
artifacts:
reports:
# propagates variables into the pipeline level
dotenv: gitversion.env

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,6 +1,58 @@
# 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.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 ### 0.6.4
- Released a stable-ish version to ensure compatibility with the current GUI in OptimaLab35 (v0.1.0). - 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. - This version serves as a baseline before potential breaking changes in future updates.
@ -19,6 +71,8 @@
- Working on to Publish on pypi - Working on to Publish on pypi
- Renaming of files and classes - Renaming of files and classes
---
## 0.5.x ## 0.5.x
### 0.5.0 ### 0.5.0
### **OPTIMA35 0.5.0: Code Cleaning and Preparation for Split** ### **OPTIMA35 0.5.0: Code Cleaning and Preparation for Split**
@ -30,6 +84,8 @@
- Updated **GUI** and **TUI** to work seamlessly with the new **OPTIMA35** class. - Updated **GUI** and **TUI** to work seamlessly with the new **OPTIMA35** class.
- Ensured compatibility with the newly organized codebase in the OPTIMA35 package. - 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.
@ -55,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:
@ -101,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.
@ -109,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**
@ -138,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:**

View file

@ -1,5 +1,5 @@
--- ---
mode: Mainline 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] 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-,/\\\\]*\\))?:" minor-version-bump-message: "^(build|chore|ci|docs|feat|refactor|revert|style|test)(\\([\\w\\s-,/\\\\]*\\))?:"
patch-version-bump-message: "^(fix|patch)(\\([\\w\\s-,/\\\\]*\\))?:" patch-version-bump-message: "^(fix|patch)(\\([\\w\\s-,/\\\\]*\\))?:"

View file

@ -1,52 +1,65 @@
# **OPTIMA35** # **OPTIMA35**
[optima35](https://gitlab.com/CodeByMrFinchum/optima35) is a Python package for managing and editing images, with a focus on analog photography (using pillow and piexif). For a graphical user interface, see [OptimaLab35](https://gitlab.com/CodeByMrFinchum/OptimaLab35). Developed on my [forgejo instance](https://code.boxyfoxy.net/CodeByMrFinchum), [GitLab](https://gitlab.com/CodeByMrFinchum) is used as backup.
## **Installation** **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.
Install with pip (dependencies will be installed automatically):
```bash
pip install optima35
```
and the GUI with
```bash
pip install OptimaLab35
```
## **Overview** 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.
**OPTIMA35** (**Organizing, Processing, Tweaking Images, and Modifying scanned Analogs from 35mm Film**) simplifies the editing and management of images and metadata. Though optimized for analog photography, it can handle any type of images. 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.
---
## **Features** ## **Features**
### **Image Processing** ### **Image Processing**
- Resize images - Resize images (upscale or downscale)
- Rename with custom order - Convert images to grayscale
- Grayscale conversion - Adjust brightness and contrast
- Brightness and contrast adjustment - Add customizable text-based watermarks
### **EXIF Management** ### **EXIF Management**
- Copy or add custom EXIF data - Add EXIF data using a simple dictionary
- Add GPS coordinates - Copy EXIF data from the original image
- Add or modify EXIF dates - Remove EXIF metadata completely
- Remove EXIF metadata - Add timestamps (e.g., original photo timestamp)
- Automatically adjust EXIF timestamps based on image file names
- Add GPS coordinates to images
### **Watermarking** ### **Streamlined Integration**
- Add customizable watermarks to images - Handles all required EXIF byte conversions behind the scenes
- Provides an intuitive API for frequently needed operations
---
## **Installation**
Install the GUI (dependencies are installed automatically)
```bash
pip install OptimaLab35
```
Or in case you only want optima35 (dependencies are installed automatically):
```bash
pip install optima35
```
---
## **Current Status** ## **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.
**Alpha Stage** ---
- Active development with frequent updates. # Contribution
- Breaking changes may occur in minor version updates.
- Check the [CHANGELOG](https://gitlab.com/CodeByMrFinchum/optima35/-/blob/main/CHANGELOG.md?ref_type=heads) for details on changes and updates.
## **Contributing and Feedback** 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.
Feedback, bug reports, and contributions are welcome! Please submit them through the [GitLab repository](https://gitlab.com/CodeByMrFinchum/optima35). ## Use of LLMs
# 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
@ -58,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)

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.

View file

@ -6,11 +6,13 @@ build-backend = "hatchling.build"
name = "optima35" name = "optima35"
dynamic = ["version"] dynamic = ["version"]
authors = [{ name = "Mr. Finchum" }] authors = [{ name = "Mr. Finchum" }]
description = "OPTIMA35 is a package to modify images with pillow and piexif." description = "optima35 is a package to modify images, using pillow and piexif."
readme = "README.md" readme = "pip_README.md"
requires-python = ">=3.8" requires-python = ">=3.8, <4.0"
dependencies = ["piexif", "pillow"] dependencies = ["piexif", "pillow"]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable",
"Topic :: Software Development :: Libraries :: Python Modules",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
"Operating System :: OS Independent", "Operating System :: OS Independent",
@ -18,6 +20,9 @@ classifiers = [
[project.urls] [project.urls]
Source = "https://gitlab.com/CodeByMrFinchum/optima35" 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] [project.scripts]
optima35 = "optima35.__main__:main" optima35 = "optima35.__main__:main"

View file

@ -1 +1 @@
__version__ = "0.0.1" __version__ = "1.0.0"

View file

@ -1,6 +1,7 @@
from . import __version__ from . import __version__
# From ChatGPT # From ChatGPT
def main(): 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(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.") print("Please use OptimaLab35 for a UI, run pip install OptimaLab35 and start with OptimaLab35.")

View file

@ -6,15 +6,15 @@ from optima35 import __version__
class OptimaManager: class OptimaManager:
def __init__(self): def __init__(self):
self.name = "OPTIMA35" self.name = "optima35"
self.version = __version__ self.version = __version__
self.image_processor = ImageProcessor() self.image_processor = ImageProcessor()
self.exif_handler = ExifHandler() self.exif_handler = ExifHandler()
def modify_timestamp_in_exif(self, data_for_exif: dict, filename: str): 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.""" """"Takes a dict formated for exif use by piexif and adjusts the date_time_original, changing the minutes and seconds to fit the number of the filname."""
last_tree = filename[-3:len(filename)] last_three = filename[-3:len(filename)]
total_seconds = int(re.sub(r'\D+', '', last_tree)) total_seconds = int(re.sub(r'\D+', '', last_three))
minutes = total_seconds // 60 minutes = total_seconds // 60
seconds = 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 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
@ -22,82 +22,119 @@ class OptimaManager:
data_for_exif["date_time_original"] = new_time.strftime("%Y:%m:%d %H:%M:%S") data_for_exif["date_time_original"] = new_time.strftime("%Y:%m:%d %H:%M:%S")
return data_for_exif return data_for_exif
def process_image(self, def _process_image(
image_input_file, self,
image_output_file, image_input_file: str,
file_type, resize: int = None,
quality, watermark: str = None,
compressing, font_size: int = 2,
optimize, grayscale: bool = False,
resize = None, brightness: float = None,
watermark = None, contrast: float = None
font_size = 2, ):
grayscale = False, # My Code restructured by ChatGPT, but had to fix bugs
brightness = None, img = self.image_processor.open_image(image_input_file)
contrast = None, # Apply transformations
dict_for_exif = None,
gps = None,
copy_exif = False):
# Partly optimized by ChatGPT
# Open the image file
with self.image_processor.open_image(image_input_file) as img:
processed_img = img
image_name = os.path.basename(image_output_file) # for date adjustment
# Resize
if resize is not None: if resize is not None:
processed_img = self.image_processor.resize_image( img = self.image_processor.resize_image(img, percent=resize)
image=processed_img, percent=resize
)
# Watermark
if watermark is not None: if watermark is not None:
processed_img = self.image_processor.add_watermark( img = self.image_processor.add_watermark(img, watermark, font_size)
processed_img, watermark, int(font_size)
)
# Grayscale
if grayscale: if grayscale:
processed_img = self.image_processor.grayscale(processed_img) img = self.image_processor.grayscale(img)
# Brightness
if brightness is not None: if brightness is not None:
processed_img = self.image_processor.change_brightness( img = self.image_processor.change_brightness(img, brightness)
processed_img, brightness
)
# Contrast
if contrast is not None: if contrast is not None:
processed_img = self.image_processor.change_contrast( img = self.image_processor.change_contrast(img, contrast)
processed_img, contrast return img
)
# EXIF data handling def _handle_exif(
exif_piexif_format = None self,
if dict_for_exif: # todo: maybe move to ui and only accept complete exif dicts.. image,
selected_exif = dict_for_exif 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: if "date_time_original" in dict_for_exif:
selected_exif = self.modify_timestamp_in_exif(selected_exif, image_name) dict_for_exif = self._modify_timestamp_in_exif(dict_for_exif, file_name)
exif_piexif_format = self.exif_handler.build_exif_dict( exif_data = self.exif_handler.build_exif_bytes(
selected_exif, self.image_processor.get_image_size(processed_img) 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,
) )
# GPS data # Handle EXIF metadata
if gps is not None: exif_piexif_format = self._handle_exif(
latitude = float(gps[0]) image = processed_img,
longitude = float(gps[1]) file_name = image_output_file,
exif_piexif_format = self.exif_handler.add_geolocation_to_exif(exif_piexif_format, latitude, longitude) dict_for_exif = dict_for_exif,
gps = gps,
copy_exif = copy_exif
)
# Copy EXIF data if selected, and ensure size is correct in exif data # Save the image
elif copy_exif:
try:
og_exif = self.exif_handler.get_exif_info(img)
og_exif["Exif"][40962], og_exif["Exif"][40963] = self.image_processor.get_image_size(processed_img)
exif_piexif_format = og_exif
except Exception:
print("Copying EXIF data selected, but no EXIF data is available in the original image file.")
# Save the processed image
self.image_processor.save_image( self.image_processor.save_image(
image = processed_img, image = processed_img,
path = image_output_file, path = image_output_file,
@ -105,5 +142,73 @@ class OptimaManager:
file_type = file_type, file_type = file_type,
jpg_quality = quality, jpg_quality = quality,
png_compressing = compressing, png_compressing = compressing,
optimize = optimize 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,5 +1,6 @@
from PIL import Image, ImageDraw, ImageFont, ImageEnhance from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageQt
import piexif import piexif
import piexif.helper
from fractions import Fraction from fractions import Fraction
class ImageProcessor: class ImageProcessor:
@ -34,18 +35,15 @@ 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):
"""Addes a watermark to the image using default os font.""" """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)
@ -79,7 +77,7 @@ 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 piexif_exif_data is not None: if piexif_exif_data is not None:
@ -91,6 +89,10 @@ class ImageProcessor:
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):
@ -99,7 +101,7 @@ 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 a dicts.""" """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 = {
@ -113,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],
@ -197,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)