From a46390ba82b4817b5d9c21fd91ba69cb674b88d0 Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Thu, 23 Jan 2025 15:34:28 +0100 Subject: [PATCH 01/49] Fix: Fixed issue when renaming resulted in wrong order. --- CHANGELOG.md | 3 +++ src/OptimaLab35/gui.py | 4 ++-- src/OptimaLab35/utils/utility.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 016e5b5..f800a48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## 0.3.x +### 0.3.4: Fix +- Fixed an issue where renaming images, while converting could result in wrong numbering. + ### 0.3.1 - 0.3.3 - Repo only: Fix building pipeline diff --git a/src/OptimaLab35/gui.py b/src/OptimaLab35/gui.py index 278812e..df94cfc 100644 --- a/src/OptimaLab35/gui.py +++ b/src/OptimaLab35/gui.py @@ -170,7 +170,6 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): def populate_exif(self): # partly chatGPT # Mapping of EXIF fields to comboboxes in the UI - print("populate") combo_mapping = { "make": self.ui.make_comboBox, "model": self.ui.model_comboBox, @@ -232,6 +231,7 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): image_files = [ f for f in os.listdir(path) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp")) ] + image_files.sort() return image_files def control_before_start(self, process): @@ -369,7 +369,7 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): 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.name} (v{self.version}) & {self.o.name} (v{self.o.version})" + user_data["software"] = f"{self.name} (v{self.version}) with {self.o.name} (v{self.o.version})" return user_data def get_selected_exif(self): diff --git a/src/OptimaLab35/utils/utility.py b/src/OptimaLab35/utils/utility.py index 6dfa17d..f604656 100644 --- a/src/OptimaLab35/utils/utility.py +++ b/src/OptimaLab35/utils/utility.py @@ -75,8 +75,8 @@ class Utilities: "AE-1" ], "user_comment": [ - "Scanner.NORITSU-KOKI", - "Scanner.NA" + "Scanner: NORITSU-KOKI", + "Scanner: NA" ] } self.write_yaml(file, def_exif) From bcdad62e8ae3fe8020554aef14a303fadd0c18ad Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Thu, 23 Jan 2025 16:33:05 +0100 Subject: [PATCH 02/49] patch: Added check if any exif options are empty --- src/OptimaLab35/gui.py | 26 ++++++++++++++++++++++++-- src/OptimaLab35/ui/main_window.py | 3 +-- src/OptimaLab35/ui/main_window.ui | 18 +----------------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/OptimaLab35/gui.py b/src/OptimaLab35/gui.py index df94cfc..5aac4c1 100644 --- a/src/OptimaLab35/gui.py +++ b/src/OptimaLab35/gui.py @@ -295,7 +295,13 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): def start_process(self): self.toggle_buttons(False) - self.update_settings() # Get all user selected data + u = self.update_settings() + print(f"u in startinset_xif: {u}") + if u != None: # Get all user selected data + QMessageBox.warning(self, "Warning", f"Error: {u}") + self.toggle_buttons(True) + return + if self.control_before_start("image") == False: self.toggle_buttons(True) return @@ -327,7 +333,13 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): def startinsert_exif(self): self.toggle_buttons(False) - self.update_settings() # Get all user selected data + u = self.update_settings() + print(f"u in startinset_xif: {u}") + if u != None: # Get all user selected data + QMessageBox.warning(self, "Warning", f"Error: {u}") + self.toggle_buttons(True) + return + if self.control_before_start("exif") == False: self.toggle_buttons(True) return @@ -384,6 +396,12 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): self.settings["gps"] = None return selected_exif + def check_selected_exif(self, exif): + for key in exif: + if len(exif[key]) == 0: + return f"{key} is empty" + return True + def update_settings(self): """Update .settings from all GUI elements.""" # Basic @@ -417,6 +435,10 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): self.settings["user_selected_exif"] = None self.settings["gps"] = None + u = self.check_selected_exif(self.settings["user_selected_exif"]) + if u != True: + return u + # Helper functions, low level def handle_exif_file(self, do): # TODO: add check if data is missing. diff --git a/src/OptimaLab35/ui/main_window.py b/src/OptimaLab35/ui/main_window.py index 8749b23..c0e4990 100644 --- a/src/OptimaLab35/ui/main_window.py +++ b/src/OptimaLab35/ui/main_window.py @@ -344,7 +344,7 @@ class Ui_MainWindow(object): self.edit_exif_button = QPushButton(self.exif_group) self.edit_exif_button.setObjectName(u"edit_exif_button") - self.edit_exif_button.setEnabled(False) + self.edit_exif_button.setEnabled(True) self.horizontalLayout.addWidget(self.edit_exif_button) @@ -584,7 +584,6 @@ class Ui_MainWindow(object): self.resize_Slider.valueChanged.connect(self.resize_spinBox.setValue) self.exif_checkbox.toggled.connect(self.gps_groupBox.setEnabled) self.contrast_spinBox.valueChanged.connect(self.contrast_horizontalSlider.setValue) - self.exif_checkbox.toggled.connect(self.edit_exif_button.setEnabled) self.add_date_checkBox.toggled.connect(self.dateEdit.setEnabled) self.jpg_quality_spinBox.valueChanged.connect(self.jpg_quality_Slider.setValue) self.rename_checkbox.toggled.connect(self.revert_checkbox.setEnabled) diff --git a/src/OptimaLab35/ui/main_window.ui b/src/OptimaLab35/ui/main_window.ui index 0f89e52..59d707a 100644 --- a/src/OptimaLab35/ui/main_window.ui +++ b/src/OptimaLab35/ui/main_window.ui @@ -575,7 +575,7 @@ - false + true Edit Exif @@ -1172,22 +1172,6 @@ - - exif_checkbox - toggled(bool) - edit_exif_button - setEnabled(bool) - - - 134 - 107 - - - 79 - 170 - - - add_date_checkBox toggled(bool) From 3d0830aec5479bc8f90df32768dbca7d9331e24f Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Thu, 23 Jan 2025 16:36:45 +0100 Subject: [PATCH 03/49] 0.3.5 --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f800a48..d8e28d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Changelog ## 0.3.x -### 0.3.4: Fix +### 0.3.6: Patch +- Added check if any exif options are empty. +- Also made the exif editor aviable without checking the exif box. + +### 0.3.5: Fix - Fixed an issue where renaming images, while converting could result in wrong numbering. -### 0.3.1 - 0.3.3 +### 0.3.1 - 0.3.4 - Repo only: Fix building pipeline ### 0.3.0 From 2a5efcd88c784902caa4a752b5e17b8123a80c68 Mon Sep 17 00:00:00 2001 From: CodeByMrFinchum Date: Thu, 23 Jan 2025 16:37:26 +0100 Subject: [PATCH 04/49] Change url hompage to source. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index be43fbf..d3b5772 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ ] [project.urls] -Homepage = "https://gitlab.com/CodeByMrFinchum/OptimaLab35" +Source = "https://gitlab.com/CodeByMrFinchum/OptimaLab35" [project.scripts] OptimaLab35 = "OptimaLab35.main:main" From 09025105ea3c96eec9e4d2b3b81d27e40134c771 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Tue, 28 Jan 2025 15:03:47 +0000 Subject: [PATCH 05/49] Feat: Added compatibility with optima35 v1.0 --- CHANGELOG.md | 20 +++ README.md | 84 +++++---- pyproject.toml | 4 +- src/OptimaLab35/__main__.py | 7 + src/OptimaLab35/gui.py | 19 +-- src/OptimaLab35/main.py | 54 ------ src/OptimaLab35/tui.py | 330 ------------------------------------ 7 files changed, 76 insertions(+), 442 deletions(-) create mode 100644 src/OptimaLab35/__main__.py delete mode 100644 src/OptimaLab35/main.py delete mode 100644 src/OptimaLab35/tui.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e28d9..2299d75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog +## 0.4.x +### 0.4.0 +- Fixed a critical issue that prevented the program from functioning. +- Updated compatibility to align with the **upcoming** optima35 **release**. + +**Removal of TUI:** +- The TUI version is no longer compatible with optima35 v1.0. +- Maintaining two UIs has become too time-consuming, as the primary focus is on the GUI, which provides the best user experience. Recently, the TUI version was only receiving patches without any meaningful enhancements. + +--- + ## 0.3.x +### 0.3.7: prepear for optima35 release +- Added a maximum version of dependencies list. + ### 0.3.6: Patch - Added check if any exif options are empty. - Also made the exif editor aviable without checking the exif box. @@ -14,6 +28,8 @@ ### 0.3.0 - Repo only: adding pipeline +--- + ## 0.2.x ### 0.2.3 - Refactored code for improved readability. @@ -34,6 +50,8 @@ - Added a new experimental preview window to display an image and show how changing values affects it. - Programm now warns for potential overwrite of existing files. +--- + ## 0.1.x ### 0.1.1 - Update metadata, preview, readme, bump in version for pip @@ -41,6 +59,8 @@ ### 0.1.0 - Preserved the current working GUI by pinning `optima35` to a specific version for guaranteed compatibility. +--- + ## 0.0.x ### 0.0.4-a2 - Adding __version__ to `__init__.py` so version is automaticly updated in program as well as pypi. diff --git a/README.md b/README.md index cdb82e4..f489d04 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,56 @@ # **OptimaLab35** -[OptimaLab35](https://gitlab.com/CodeByMrFinchum/OptimaLab35) is a graphical and terminal user interface for [optima35](https://gitlab.com/CodeByMrFinchum/optima35). It is under **heavy development**, and both UI elements and cross-platform compatibility may change. +_Last updated: 28 Jan 2025_ ## **Overview** -**OptimaLab35** extends **OPTIMA35** (**Organizing, Processing, Tweaking Images, and Modifying scanned Analogs from 35mm Film**) by providing an intuitive interface for image and metadata management. While tailored for analog photography, it supports any type of image. +**OptimaLab35** enhances **OPTIMA35** (**Organizing, Processing, Tweaking Images, and Modifying scanned Analogs from 35mm Film**) by offering a user-friendly graphical interface for efficient image and metadata management. +It serves as a GUI for the [OPTIMA35 library](https://gitlab.com/CodeByMrFinchum/optima35), providing an intuitive way to interact with the core functionalities. + +--- ## **Current Status** +### **Alpha Stage** -### **Versioning and Compatibility** +OptimaLab35 is built using **PySide6** and **Qt**, offering a modern and flexible interface for **OPTIMA35**. -The preserved version **v0.1.0** ensures stability with the current GUI design. It depends on **optima35==0.6.4**, a version confirmed to work seamlessly with this release. Future updates may introduce breaking changes, especially as the project evolves across platforms. +The program is under **active development**, and while versions released on PyPI should not contain major bugs, occasional issues may arise. -### **Installation** +For the most accurate and detailed update information, please refer to the well-maintained [**CHANGELOG**](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/blob/main/CHANGELOG.md), as this README may occasionally lag behind the latest updates. -Install via pip (dependencies are automatically managed, except for `simple-term-menu` used in TUI mode, which is Linux-only): +--- + +## **Features** + +### **Image Processing** +- Resize images (upscale or downscale) +- Convert images to grayscale +- Adjust brightness and contrast +- Add customizable text-based watermarks + +### **Image preview** +- Load a single image and see how change in brightness and contrast changes the image + +### **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 + +--- + +## **Installation** + +Install via **pip** (dependencies are handled automatically): ```bash pip install OptimaLab35 ``` -## **Development and Notes** +--- -**Alpha Stage** -- UI designs (GUI and TUI) are evolving, and breaking changes may occur. -- The [**CHANGELOG**](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/blob/main/CHANGELOG.md) provides detailed updates. -- Some safety checks are still under development. - -**Modes:** -- **GUI**: Default if **PySide6** is available. -- **TUI**: Fallback if **PySide6** is missing or can be explicitly started using the `--tui` flag. - -### Preview GUI +## Preview GUI **PREVIEW** might be out of date. **Main tab** @@ -49,35 +69,7 @@ pip install OptimaLab35 ![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/exif_editor.png){width=40%} -**Info window** - -![main](https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/info_window.png){width=40%} - -## **Features** - -### **Image Processing** -- Resizing -- Renaming with custom order -- Grayscale conversion -- Brightness and contrast adjustment - -### **EXIF Management** -- Copy or add custom EXIF data -- Add GPS coordinates -- Add or modify EXIF dates -- Remove EXIF metadata - -### **Watermarking** -- Add customizable watermarks - -## **Dependencies** - -**GUI Mode:** -- `optima35` -- `pyside6` - -**TUI Mode (Linux only):** -- `simple-term-menu` +--- # Use of LLMs In the interest of transparency, I disclose that Generative AI (GAI) large language models (LLMs), including OpenAI’s ChatGPT and Ollama models (e.g., OpenCoder and Qwen2.5-coder), have been used to assist in this project. diff --git a/pyproject.toml b/pyproject.toml index d3b5772..987ed7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [{ name = "Mr. Finchum" }] description = "User interface for optima35." readme = "pip_README.md" requires-python = ">=3.8" -dependencies = ["optima35>=0.6.7", "pyside6", "PyYAML", "packaging"] +dependencies = ["optima35>=1.0.0, <2.0.0", "pyside6", "PyYAML"] classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", @@ -20,7 +20,7 @@ classifiers = [ Source = "https://gitlab.com/CodeByMrFinchum/OptimaLab35" [project.scripts] -OptimaLab35 = "OptimaLab35.main:main" +OptimaLab35 = "OptimaLab35.__main__:main" [tool.hatch.build.targets.wheel] packages = ["src/OptimaLab35"] diff --git a/src/OptimaLab35/__main__.py b/src/OptimaLab35/__main__.py new file mode 100644 index 0000000..557cfbf --- /dev/null +++ b/src/OptimaLab35/__main__.py @@ -0,0 +1,7 @@ +from OptimaLab35 import gui, __version__ + +def main(): + gui.main() + +if __name__ == "__main__": + main() diff --git a/src/OptimaLab35/gui.py b/src/OptimaLab35/gui.py index 5aac4c1..4fc4afb 100644 --- a/src/OptimaLab35/gui.py +++ b/src/OptimaLab35/gui.py @@ -296,7 +296,6 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): def start_process(self): self.toggle_buttons(False) u = self.update_settings() - print(f"u in startinset_xif: {u}") if u != None: # Get all user selected data QMessageBox.warning(self, "Warning", f"Error: {u}") self.toggle_buttons(True) @@ -321,7 +320,7 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): input_path = os.path.join(input_folder, image_file) - self.o.insert_dict_to_image( + self.o.insert_exif_to_image( exif_dict = self.settings["user_selected_exif"], image_path = input_path, gps = self.settings["gps"]) @@ -334,7 +333,6 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): def startinsert_exif(self): self.toggle_buttons(False) u = self.update_settings() - print(f"u in startinset_xif: {u}") if u != None: # Get all user selected data QMessageBox.warning(self, "Warning", f"Error: {u}") self.toggle_buttons(True) @@ -435,9 +433,10 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): self.settings["user_selected_exif"] = None self.settings["gps"] = None - u = self.check_selected_exif(self.settings["user_selected_exif"]) - if u != True: - return u + if self.settings["user_selected_exif"] is not None: + u = self.check_selected_exif(self.settings["user_selected_exif"]) + if u != True: + return u # Helper functions, low level def handle_exif_file(self, do): @@ -476,10 +475,10 @@ class PreviewWindow(QMainWindow, Ui_Preview_Window): if not os.path.isfile(path): return try: - img = self.o.process_image( - save = False, + img = self.o.process_image_object( image_input_file = path, - image_output_file = "", + resize = 50, + watermark = "PREVIEW", grayscale = self.ui.grayscale_checkBox.isChecked(), brightness = int(self.ui.brightness_spinBox.text()), contrast = int(self.ui.contrast_spinBox.text()) @@ -526,7 +525,7 @@ class ImageProcessorRunnable(QRunnable): image_name = os.path.splitext(image_file)[0] output_path = os.path.join(output_folder, image_name) - self.o.process_image( + self.o.process_and_save_image( image_input_file = input_path, image_output_file = output_path, file_type = self.settings["file_format"], diff --git a/src/OptimaLab35/main.py b/src/OptimaLab35/main.py deleted file mode 100644 index 833078b..0000000 --- a/src/OptimaLab35/main.py +++ /dev/null @@ -1,54 +0,0 @@ -import os -from argparse import ArgumentParser -from OptimaLab35 import gui, __version__ - -# Try importing TUI only if simple-term-menu is installed -try: - from OptimaLab35 import tui - simple_term_menu_installed = True -except ImportError: - simple_term_menu_installed = False - -# Check if PySide is installed -def check_pyside_installed(): - try: - import PySide6 # Replace with PySide2 if using that version - return True - except ImportError: - return False - -def start_gui(): - gui.main() - -def start_tui(): - if simple_term_menu_installed: - tui.main() - else: - print("Error: simple-term-menu is not installed. Please install it to use the TUI mode.") - exit(1) - -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__": - main() diff --git a/src/OptimaLab35/tui.py b/src/OptimaLab35/tui.py deleted file mode 100644 index b5cfc1f..0000000 --- a/src/OptimaLab35/tui.py +++ /dev/null @@ -1,330 +0,0 @@ -import os -from datetime import datetime -# my packages -from optima35.core import OptimaManager -from OptimaLab35.utils.utility import Utilities -from OptimaLab35.ui.simple_tui import SimpleTUI -from OptimaLab35 import __version__ - -class OptimaLab35_lite(): - def __init__(self): - self.name = "OptimaLab35-lite" - self.version = __version__ - self.o = OptimaManager() - self.u = Utilities() - self.tui = SimpleTUI() - self.u.program_configs() - self.exif_file = os.path.expanduser("~/.config/OptimaLab35/exif.yaml") - self.available_exif_data = self.u.read_yaml(self.exif_file) - self.setting_file = os.path.expanduser("~/.config/OptimaLab35/tui_settings.yaml") - self.settings = { - "input_folder": None, - "output_folder": None, - "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): - self._check_options() # Get all user selected data - input_folder_valid = os.path.exists(self.settings["input_folder"]) - output_folder_valid = os.path.exists(self.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.settings["input_folder"] - output_folder = self.settings["output_folder"] - - image_files = [ - f for f in os.listdir(input_folder) if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp")) - ] - i = 1 - for image_file in image_files: - input_path = os.path.join(input_folder, image_file) - if self.settings["new_file_names"] != False: - image_name = self.u.append_number_to_name(self.settings["new_file_names"], i, len(image_files), self.settings["invert_image_order"]) - else: - image_name = os.path.splitext(image_file)[0] - output_path = os.path.join(output_folder, image_name) - self.o.process_image( - image_input_file = input_path, - image_output_file = output_path, - file_type = self.settings["file_format"], - quality = self.settings["jpg_quality"], - compressing = self.settings["png_compression"], - optimize = self.settings["optimize"], - resize = self.settings["resize"], - watermark = self.settings["watermark"], - font_size = self.settings["font_size"], - grayscale = self.settings["grayscale"], - brightness = self.settings["brightness"], - contrast = self.settings["contrast"], - dict_for_exif = self.selected_exif, - gps = self.settings["gps"], - copy_exif = self.settings["copy_exif"]) - self.u.progress_bar(i, len(image_files)) - i += 1 - - def _check_options(self): - try: - if "Resize image" in self.settings["modifications"]: - self.settings["resize"] = self.settings["resize"] - else: - self.settings["resize"] = None - - if "Convert to grayscale" in self.settings["modifications"]: - self.settings["grayscale"] = True - else: - self.settings["grayscale"] = False - - if "Change contrast" in self.settings["modifications"]: - self.settings["contrast"] = self.settings["contrast"] - else: - self.settings["contrast"] = None - - if "Change brightness" in self.settings["modifications"]: - self.settings["brightness"] = self.settings["brightness"] - else: - self.settings["brightness"] = None - - if "Rename images" in self.settings["modifications"]: - self.settings["new_file_names"] = self.settings["new_file_names"] - 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"] = self.settings["watermark"] - else: - self.settings["watermark"] = None - - self.settings["optimize"] = self.settings["optimize"] - self.settings["png_compression"] = self.settings["png_compression"] - self.settings["jpg_quality"] = self.settings["jpg_quality"] - - self.settings["input_folder"] = self.settings["input_folder"] - self.settings["output_folder"] = self.settings["output_folder"] - self.settings["file_format"] = self.settings["file_format"] - self.settings["font_size"] = 2 # need to add option to select size - - self.settings["copy_exif"] = self.settings["copy_exif"] - - if "Change EXIF" in self.settings["modifications"]: #missing - self.selected_exif = self._collect_exif_data() # - else: - self.selected_exif = None - - 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") - print(f"Settings for {self.name} v{self.version} will be saved {self.setting_file}...") - self.settings["resize"] = self.take_input_and_validate(question = "Default resize percentage (below 100 downscale, above upscale): ", accepted_type = int, min_value = 10, max_value = 200) - self.settings["contrast"] = self.take_input_and_validate(question = "Default contrast percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100) - self.settings["brightness"] = self.take_input_and_validate(question = "Default brighness percentage (negative = decrease, positive = increase): ", accepted_type = int, min_value = -100, max_value = 100) - 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.""" - print(f"Exif file can be found {self.exif_file}...") - 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.available_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 None - long = input("Enter Longitude (xx.xxxxxx): ") - try: - self.o.exif_handler.add_geolocation_to_exif(test_exif, float(lat), float(long)) - return [float(lat), float(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.name} v{self.version} for {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(): - app = OptimaLab35_lite() - app.run() - -if __name__ == "__main__": - main() From d608156206f7083816536dd04c932ed034e0faa5 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Tue, 28 Jan 2025 15:19:18 +0000 Subject: [PATCH 06/49] refactor: Removed tui leftover. --- src/OptimaLab35/ui/simple_tui.py | 60 -------------------------------- src/OptimaLab35/utils/utility.py | 25 ------------- 2 files changed, 85 deletions(-) delete mode 100644 src/OptimaLab35/ui/simple_tui.py diff --git a/src/OptimaLab35/ui/simple_tui.py b/src/OptimaLab35/ui/simple_tui.py deleted file mode 100644 index f8abd32..0000000 --- a/src/OptimaLab35/ui/simple_tui.py +++ /dev/null @@ -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 diff --git a/src/OptimaLab35/utils/utility.py b/src/OptimaLab35/utils/utility.py index f604656..25990c5 100644 --- a/src/OptimaLab35/utils/utility.py +++ b/src/OptimaLab35/utils/utility.py @@ -90,28 +90,3 @@ class Utilities: ending_number = current_image ending = f"{ending_number:0{total_digits}}" return f"{base_name}_{ending}" - - 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("") From 51108ef86e29ce5897d65a7fdec2775afadff5c0 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Thu, 30 Jan 2025 14:29:45 +0000 Subject: [PATCH 07/49] feat: Initial flatpak support --- .gitignore | 2 + CHANGELOG.md | 16 + flatpak/app-icon.xcf | Bin 0 -> 78930 bytes flatpak/build_flatpak.sh | 5 + flatpak/flathub.json | 3 + flatpak/flatpak-pip-generator | 533 ++++++++++++++++++ flatpak/net.boxyfoxy.OptimaLab35.json | 32 ++ flatpak/python3-modules.json | 107 ++++ pyproject.toml | 2 +- src/flatpak/app-icon.png | Bin 0 -> 50453 bytes src/flatpak/net.boxyfoxy.OptimaLab35.desktop | 11 + .../net.boxyfoxy.OptimaLab35.metainfo.xml | 33 ++ src/pyproject.toml | 28 + 13 files changed, 771 insertions(+), 1 deletion(-) create mode 100644 flatpak/app-icon.xcf create mode 100755 flatpak/build_flatpak.sh create mode 100644 flatpak/flathub.json create mode 100755 flatpak/flatpak-pip-generator create mode 100644 flatpak/net.boxyfoxy.OptimaLab35.json create mode 100644 flatpak/python3-modules.json create mode 100644 src/flatpak/app-icon.png create mode 100644 src/flatpak/net.boxyfoxy.OptimaLab35.desktop create mode 100644 src/flatpak/net.boxyfoxy.OptimaLab35.metainfo.xml create mode 100644 src/pyproject.toml diff --git a/.gitignore b/.gitignore index 3e21bf9..7a56971 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ test/ dist/ .ropeproject/ __pycache__/ +.flatpak-builder/ +flatpak-build-dir/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2299d75..5643294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.6.x +### 0.6.0: Initial Flatpak Support +- Started Flatpak package building. +- Not added to Flathub yet, as only stable software is hosted there. +- Not fully completed, icon, name, and description are included, but the version is missing for some reason. +- Local build and installation work. The Bash script `build_flatpak.sh` in the `flatpak/` directory generates all pip dependencies, then builds and installs the app locally. +- `requirements-parser` has to be installed from pip to finish installing the flatpak (maybe more pypi packages..) + +--- + +## 0.5.x +### 0.5.0 +- Removed all leftover of tui code that was hiding in some classes. + +--- + ## 0.4.x ### 0.4.0 - Fixed a critical issue that prevented the program from functioning. diff --git a/flatpak/app-icon.xcf b/flatpak/app-icon.xcf new file mode 100644 index 0000000000000000000000000000000000000000..5e25541fcfa2f5082c856c4f21138a66acc4743a GIT binary patch literal 78930 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%0DVmO`0%mf9BN9 z(7UR4Tkpc&oxN*&H}@`2_h+~;6hI#qK16)D_|WlT#E@d}F}y$@YCi0I2>NhjXfk{m zrl1d5AHF`68P*JXhB)ZM-G{zU1561_7)%eKPZd6GFoiI|Fr9!twfHo{l;hJ6$N~Yf zSfGh&o&u*Lht3Np&GpjL?ZRKqfL%XP1)h@qRI zmAQt7&W5(;nj5+tTAXWiuHCt&hwg{g=Nh1EgHbhfJuxbcu0ck1(sfI;P4rCHIhRcT zjLPV;X{J$4Et}4|&Ggr(%x*Vx4^`nt=L% z$^g3hfGUBCfm#8&ih;U;8iMKpx*CE?f|7!IlBp`FEj&pA6$V6|!K{&PlPWq=dU&`5 z3J{1ggbgIkC{>fBGP(NXsuT~>L#+Z)v7)}^%2(9HQngGfnyYKBy15$XDxIr$uIjnk z=PID9gBWkp4P8;hpp(|dO1#oaD<8$BBA(R`1_d^GK& zp&!lu=mdx^f#@KJ?tZ6Gt4gP5UM`u8E4Mayl zbQ?q`LUbua2Sao>MCU_vMMTF$bW=p9MRZ|AhemX7Fi_>}h^~+52#Icy=p>0QljuN+ z?v&I)7X4+6DS6M_q>uK!8~YVz_(v~5^a;=!I0*2~$y?yA1M?!JL&f*Om^kH#X3CUX znkiFG>gcj&@X3K4-P!KjW|*CaV?@vKz!$hTI(8Xc^72NXF9S**U`CSs!Hgbxhf`;9 z>O5w|$d?>F%M1_snWNX40U;kWBS9W%hJgIjsk=ILU#ISDdb+&Zsk1x!zEf9t>Ke~{ z<(c<9^QUJX_ULiv7W?*B9n%9F%!|Bd-Km_pnb$qf8{E&-sx>7VBM)=oVS zBQP&==2Xty%!h9aH<$gznglUf$^QWn{_&%#f5nIC_Uu zXL0I0X6VV696ig7Gx?dL*O`$eA2dTs9%;ss{L`tsI(1*C?resPyxXa>JNmv;S9t0g z&wS;X_dN5bXCC(GaaZ$^TispGEHrXJv(U&L9i7vR6uGLSeoId*yLeX$F~D1 Od?5aF$h|85=l&nL@-4^! literal 0 HcmV?d00001 diff --git a/flatpak/build_flatpak.sh b/flatpak/build_flatpak.sh new file mode 100755 index 0000000..3c0f4ea --- /dev/null +++ b/flatpak/build_flatpak.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# runtime, skd, and base has to be installed, see net.boxyfoxy.net.OptimaLab35.json +# uses [flatpak-pip-generator](https://github.com/flatpak/flatpak-builder-tools/tree/master/pip) to download and build all dependency from pip +python flatpak-pip-generator --runtime='org.kde.Sdk//6.8' piexif pillow optima35 PyYAML hatchling +flatpak-builder --user --install flatpak-build-dir net.boxyfoxy.OptimaLab35.json --force-clean diff --git a/flatpak/flathub.json b/flatpak/flathub.json new file mode 100644 index 0000000..637604e --- /dev/null +++ b/flatpak/flathub.json @@ -0,0 +1,3 @@ +{ + "only-arches": ["x86_64"] +} diff --git a/flatpak/flatpak-pip-generator b/flatpak/flatpak-pip-generator new file mode 100755 index 0000000..013181a --- /dev/null +++ b/flatpak/flatpak-pip-generator @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 + +__license__ = 'MIT' + +import argparse +import json +import hashlib +import os +import re +import shutil +import subprocess +import sys +import tempfile +import urllib.request + +from collections import OrderedDict +from typing import Dict + +try: + import requirements +except ImportError: + exit('Requirements modules is not installed. Run "pip install requirements-parser"') + +parser = argparse.ArgumentParser() +parser.add_argument('packages', nargs='*') +parser.add_argument('--python2', action='store_true', + help='Look for a Python 2 package') +parser.add_argument('--cleanup', choices=['scripts', 'all'], + help='Select what to clean up after build') +parser.add_argument('--requirements-file', '-r', + help='Specify requirements.txt file') +parser.add_argument('--build-only', action='store_const', + dest='cleanup', const='all', + help='Clean up all files after build') +parser.add_argument('--build-isolation', action='store_true', + default=False, + help=( + 'Do not disable build isolation. ' + 'Mostly useful on pip that does\'t ' + 'support the feature.' + )) +parser.add_argument('--ignore-installed', + type=lambda s: s.split(','), + default='', + help='Comma-separated list of package names for which pip ' + 'should ignore already installed packages. Useful when ' + 'the package is installed in the SDK but not in the ' + 'runtime.') +parser.add_argument('--checker-data', action='store_true', + help='Include x-checker-data in output for the "Flatpak External Data Checker"') +parser.add_argument('--output', '-o', + help='Specify output file name') +parser.add_argument('--runtime', + help='Specify a flatpak to run pip inside of a sandbox, ensures python version compatibility') +parser.add_argument('--yaml', action='store_true', + help='Use YAML as output format instead of JSON') +parser.add_argument('--ignore-errors', action='store_true', + help='Ignore errors when downloading packages') +parser.add_argument('--ignore-pkg', nargs='*', + help='Ignore a package when generating the manifest. Can only be used with a requirements file') +opts = parser.parse_args() + +if opts.yaml: + try: + import yaml + except ImportError: + exit('PyYAML modules is not installed. Run "pip install PyYAML"') + + +def get_pypi_url(name: str, filename: str) -> str: + url = 'https://pypi.org/pypi/{}/json'.format(name) + print('Extracting download url for', name) + with urllib.request.urlopen(url) as response: + body = json.loads(response.read().decode('utf-8')) + for release in body['releases'].values(): + for source in release: + if source['filename'] == filename: + return source['url'] + raise Exception('Failed to extract url from {}'.format(url)) + + +def get_tar_package_url_pypi(name: str, version: str) -> str: + url = 'https://pypi.org/pypi/{}/{}/json'.format(name, version) + with urllib.request.urlopen(url) as response: + body = json.loads(response.read().decode('utf-8')) + for ext in ['bz2', 'gz', 'xz', 'zip', 'none-any.whl']: + for source in body['urls']: + if source['url'].endswith(ext): + return source['url'] + err = 'Failed to get {}-{} source from {}'.format(name, version, url) + raise Exception(err) + + +def get_package_name(filename: str) -> str: + if filename.endswith(('bz2', 'gz', 'xz', 'zip')): + segments = filename.split('-') + if len(segments) == 2: + return segments[0] + return '-'.join(segments[:len(segments) - 1]) + elif filename.endswith('whl'): + segments = filename.split('-') + if len(segments) == 5: + return segments[0] + candidate = segments[:len(segments) - 4] + # Some packages list the version number twice + # e.g. PyQt5-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux2014_x86_64.whl + if candidate[-1] == segments[len(segments) - 4]: + return '-'.join(candidate[:-1]) + return '-'.join(candidate) + else: + raise Exception( + 'Downloaded filename: {} does not end with bz2, gz, xz, zip, or whl'.format(filename) + ) + + +def get_file_version(filename: str) -> str: + name = get_package_name(filename) + segments = filename.split(name + '-') + version = segments[1].split('-')[0] + for ext in ['tar.gz', 'whl', 'tar.xz', 'tar.gz', 'tar.bz2', 'zip']: + version = version.replace('.' + ext, '') + return version + + +def get_file_hash(filename: str) -> str: + sha = hashlib.sha256() + print('Generating hash for', filename.split('/')[-1]) + with open(filename, 'rb') as f: + while True: + data = f.read(1024 * 1024 * 32) + if not data: + break + sha.update(data) + return sha.hexdigest() + + +def download_tar_pypi(url: str, tempdir: str) -> None: + with urllib.request.urlopen(url) as response: + file_path = os.path.join(tempdir, url.split('/')[-1]) + with open(file_path, 'x+b') as tar_file: + shutil.copyfileobj(response, tar_file) + + +def parse_continuation_lines(fin): + for line in fin: + line = line.rstrip('\n') + while line.endswith('\\'): + try: + line = line[:-1] + next(fin).rstrip('\n') + except StopIteration: + exit('Requirements have a wrong number of line continuation characters "\\"') + yield line + + +def fprint(string: str) -> None: + separator = '=' * 72 # Same as `flatpak-builder` + print(separator) + print(string) + print(separator) + + +packages = [] +if opts.requirements_file: + requirements_file_input = os.path.expanduser(opts.requirements_file) + try: + with open(requirements_file_input, 'r') as req_file: + reqs = parse_continuation_lines(req_file) + reqs_as_str = '\n'.join([r.split('--hash')[0] for r in reqs]) + reqs_list_raw = reqs_as_str.splitlines() + py_version_regex = re.compile(r';.*python_version .+$') # Remove when pip-generator can handle python_version + reqs_list = [py_version_regex.sub('', p) for p in reqs_list_raw] + if opts.ignore_pkg: + reqs_new = '\n'.join(i for i in reqs_list if i not in opts.ignore_pkg) + else: + reqs_new = reqs_as_str + packages = list(requirements.parse(reqs_new)) + with tempfile.NamedTemporaryFile('w', delete=False, prefix='requirements.') as req_file: + req_file.write(reqs_new) + requirements_file_output = req_file.name + except FileNotFoundError as err: + print(err) + sys.exit(1) + +elif opts.packages: + packages = list(requirements.parse('\n'.join(opts.packages))) + with tempfile.NamedTemporaryFile('w', delete=False, prefix='requirements.') as req_file: + req_file.write('\n'.join(opts.packages)) + requirements_file_output = req_file.name +else: + if not len(sys.argv) > 1: + exit('Please specifiy either packages or requirements file argument') + else: + exit('This option can only be used with requirements file') + +for i in packages: + if i["name"].lower().startswith("pyqt"): + print("PyQt packages are not supported by flapak-pip-generator") + print("However, there is a BaseApp for PyQt available, that you should use") + print("Visit https://github.com/flathub/com.riverbankcomputing.PyQt.BaseApp for more information") + sys.exit(0) + +with open(requirements_file_output, 'r') as req_file: + use_hash = '--hash=' in req_file.read() + +python_version = '2' if opts.python2 else '3' +if opts.python2: + pip_executable = 'pip2' +else: + pip_executable = 'pip3' + +if opts.runtime: + flatpak_cmd = [ + 'flatpak', + '--devel', + '--share=network', + '--filesystem=/tmp', + '--command={}'.format(pip_executable), + 'run', + opts.runtime + ] + if opts.requirements_file: + if os.path.exists(requirements_file_output): + prefix = os.path.realpath(requirements_file_output) + flag = '--filesystem={}'.format(prefix) + flatpak_cmd.insert(1,flag) +else: + flatpak_cmd = [pip_executable] + +output_path = '' + +if opts.output: + output_path = os.path.dirname(opts.output) + output_package = os.path.basename(opts.output) +elif opts.requirements_file: + output_package = 'python{}-{}'.format( + python_version, + os.path.basename(opts.requirements_file).replace('.txt', ''), + ) +elif len(packages) == 1: + output_package = 'python{}-{}'.format( + python_version, packages[0].name, + ) +else: + output_package = 'python{}-modules'.format(python_version) +if opts.yaml: + output_filename = os.path.join(output_path, output_package) + '.yaml' +else: + output_filename = os.path.join(output_path, output_package) + '.json' + +modules = [] +vcs_modules = [] +sources = {} + +unresolved_dependencies_errors = [] + +tempdir_prefix = 'pip-generator-{}'.format(output_package) +with tempfile.TemporaryDirectory(prefix=tempdir_prefix) as tempdir: + pip_download = flatpak_cmd + [ + 'download', + '--exists-action=i', + '--dest', + tempdir, + '-r', + requirements_file_output + ] + if use_hash: + pip_download.append('--require-hashes') + + fprint('Downloading sources') + cmd = ' '.join(pip_download) + print('Running: "{}"'.format(cmd)) + try: + subprocess.run(pip_download, check=True) + os.remove(requirements_file_output) + except subprocess.CalledProcessError: + os.remove(requirements_file_output) + print('Failed to download') + print('Please fix the module manually in the generated file') + if not opts.ignore_errors: + print('Ignore the error by passing --ignore-errors') + raise + + try: + os.remove(requirements_file_output) + except FileNotFoundError: + pass + + fprint('Downloading arch independent packages') + for filename in os.listdir(tempdir): + if not filename.endswith(('bz2', 'any.whl', 'gz', 'xz', 'zip')): + version = get_file_version(filename) + name = get_package_name(filename) + try: + url = get_tar_package_url_pypi(name, version) + print('Downloading {}'.format(url)) + download_tar_pypi(url, tempdir) + except Exception as err: + # Can happen if only an arch dependent wheel is available like for wasmtime-27.0.2 + unresolved_dependencies_errors.append(err) + print('Deleting', filename) + try: + os.remove(os.path.join(tempdir, filename)) + except FileNotFoundError: + pass + + files = {get_package_name(f): [] for f in os.listdir(tempdir)} + + for filename in os.listdir(tempdir): + name = get_package_name(filename) + files[name].append(filename) + + # Delete redundant sources, for vcs sources + for name in files: + if len(files[name]) > 1: + zip_source = False + for f in files[name]: + if f.endswith('.zip'): + zip_source = True + if zip_source: + for f in files[name]: + if not f.endswith('.zip'): + try: + os.remove(os.path.join(tempdir, f)) + except FileNotFoundError: + pass + + vcs_packages = { + x.name: {'vcs': x.vcs, 'revision': x.revision, 'uri': x.uri} + for x in packages + if x.vcs + } + + fprint('Obtaining hashes and urls') + for filename in os.listdir(tempdir): + name = get_package_name(filename) + sha256 = get_file_hash(os.path.join(tempdir, filename)) + is_pypi = False + + if name in vcs_packages: + uri = vcs_packages[name]['uri'] + revision = vcs_packages[name]['revision'] + vcs = vcs_packages[name]['vcs'] + url = 'https://' + uri.split('://', 1)[1] + s = 'commit' + if vcs == 'svn': + s = 'revision' + source = OrderedDict([ + ('type', vcs), + ('url', url), + (s, revision), + ]) + is_vcs = True + else: + name = name.casefold() + is_pypi = True + url = get_pypi_url(name, filename) + source = OrderedDict([ + ('type', 'file'), + ('url', url), + ('sha256', sha256)]) + if opts.checker_data: + source['x-checker-data'] = { + 'type': 'pypi', + 'name': name} + if url.endswith(".whl"): + source['x-checker-data']['packagetype'] = 'bdist_wheel' + is_vcs = False + sources[name] = {'source': source, 'vcs': is_vcs, 'pypi': is_pypi} + +# Python3 packages that come as part of org.freedesktop.Sdk. +system_packages = ['cython', 'easy_install', 'mako', 'markdown', 'meson', 'pip', 'pygments', 'setuptools', 'six', 'wheel'] + +fprint('Generating dependencies') +for package in packages: + + if package.name is None: + print('Warning: skipping invalid requirement specification {} because it is missing a name'.format(package.line), file=sys.stderr) + print('Append #egg= to the end of the requirement line to fix', file=sys.stderr) + continue + elif package.name.casefold() in system_packages: + print(f"{package.name} is in system_packages. Skipping.") + continue + + if len(package.extras) > 0: + extras = '[' + ','.join(extra for extra in package.extras) + ']' + else: + extras = '' + + version_list = [x[0] + x[1] for x in package.specs] + version = ','.join(version_list) + + if package.vcs: + revision = '' + if package.revision: + revision = '@' + package.revision + pkg = package.uri + revision + '#egg=' + package.name + else: + pkg = package.name + extras + version + + dependencies = [] + # Downloads the package again to list dependencies + + tempdir_prefix = 'pip-generator-{}'.format(package.name) + with tempfile.TemporaryDirectory(prefix='{}-{}'.format(tempdir_prefix, package.name)) as tempdir: + pip_download = flatpak_cmd + [ + 'download', + '--exists-action=i', + '--dest', + tempdir, + ] + try: + print('Generating dependencies for {}'.format(package.name)) + subprocess.run(pip_download + [pkg], check=True, stdout=subprocess.DEVNULL) + for filename in sorted(os.listdir(tempdir)): + dep_name = get_package_name(filename) + if dep_name.casefold() in system_packages: + continue + dependencies.append(dep_name) + + except subprocess.CalledProcessError: + print('Failed to download {}'.format(package.name)) + + is_vcs = True if package.vcs else False + package_sources = [] + for dependency in dependencies: + casefolded = dependency.casefold() + if casefolded in sources and sources[casefolded].get("pypi") is True: + source = sources[casefolded] + elif dependency in sources and sources[dependency].get("pypi") is False: + source = sources[dependency] + elif ( + casefolded.replace("_", "-") in sources + and sources[casefolded.replace("_", "-")].get("pypi") is True + ): + source = sources[casefolded.replace("_", "-")] + elif ( + dependency.replace("_", "-") in sources + and sources[dependency.replace("_", "-")].get("pypi") is False + ): + source = sources[dependency.replace("_", "-")] + else: + continue + + if not (not source['vcs'] or is_vcs): + continue + + package_sources.append(source['source']) + + if package.vcs: + name_for_pip = '.' + else: + name_for_pip = pkg + + module_name = 'python{}-{}'.format(python_version, package.name) + + pip_command = [ + pip_executable, + 'install', + '--verbose', + '--exists-action=i', + '--no-index', + '--find-links="file://${PWD}"', + '--prefix=${FLATPAK_DEST}', + '"{}"'.format(name_for_pip) + ] + if package.name in opts.ignore_installed: + pip_command.append('--ignore-installed') + if not opts.build_isolation: + pip_command.append('--no-build-isolation') + + module = OrderedDict([ + ('name', module_name), + ('buildsystem', 'simple'), + ('build-commands', [' '.join(pip_command)]), + ('sources', package_sources), + ]) + if opts.cleanup == 'all': + module['cleanup'] = ['*'] + elif opts.cleanup == 'scripts': + module['cleanup'] = ['/bin', '/share/man/man1'] + + if package.vcs: + vcs_modules.append(module) + else: + modules.append(module) + +modules = vcs_modules + modules +if len(modules) == 1: + pypi_module = modules[0] +else: + pypi_module = { + 'name': output_package, + 'buildsystem': 'simple', + 'build-commands': [], + 'modules': modules, + } + +print() +with open(output_filename, 'w') as output: + if opts.yaml: + class OrderedDumper(yaml.Dumper): + def increase_indent(self, flow=False, indentless=False): + return super(OrderedDumper, self).increase_indent(flow, False) + + def dict_representer(dumper, data): + return dumper.represent_dict(data.items()) + + OrderedDumper.add_representer(OrderedDict, dict_representer) + + output.write("# Generated with flatpak-pip-generator " + " ".join(sys.argv[1:]) + "\n") + yaml.dump(pypi_module, output, Dumper=OrderedDumper) + else: + output.write(json.dumps(pypi_module, indent=4)) + print('Output saved to {}'.format(output_filename)) + +if len(unresolved_dependencies_errors) != 0: + print("Unresolved dependencies. Handle them manually") + for e in unresolved_dependencies_errors: + print(f"- ERROR: {e}") + + workaround = """Example how to handle wheels which only support specific architectures: + - type: file + url: https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + sha256: 7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e + only-arches: + - aarch64 + - type: file + url: https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + sha256: 666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 + only-arches: + - x86_64 + """ + raise Exception(f"Not all dependencies can be determined. Handle them manually.\n{workaround}") diff --git a/flatpak/net.boxyfoxy.OptimaLab35.json b/flatpak/net.boxyfoxy.OptimaLab35.json new file mode 100644 index 0000000..3d5bd66 --- /dev/null +++ b/flatpak/net.boxyfoxy.OptimaLab35.json @@ -0,0 +1,32 @@ +{ + "id": "net.boxyfoxy.OptimaLab35", + "runtime": "org.kde.Platform", + "runtime-version": "6.8", + "sdk": "org.kde.Sdk", + "sdk-version": "6.8", + "base": "io.qt.PySide.BaseApp", + "base-version": "6.8", + "command": "OptimaLab35", + "version": "1.0", + "finish-args": ["--socket=wayland", "--socket=x11"], + "modules": [ + "python3-modules.json", + { + "name": "OptimaLab35", + "buildsystem": "simple", + "build-commands": [ + "pip install --no-build-isolation --prefix=/app .", + "ls", + "install -D flatpak/net.boxyfoxy.OptimaLab35.desktop /app/share/applications/net.boxyfoxy.OptimaLab35.desktop", + "install -D flatpak/net.boxyfoxy.OptimaLab35.metainfo.xml /app/share/metainfo/net.boxyfoxy.OptimaLab35.metainfo.xml", + "install -D flatpak/app-icon.png /app/share/icons/hicolor/512x512/apps/net.boxyfoxy.OptimaLab35.png" + ], + "sources": [ + { + "type": "dir", + "path": "../src" + } + ] + } + ] +} diff --git a/flatpak/python3-modules.json b/flatpak/python3-modules.json new file mode 100644 index 0000000..b37a339 --- /dev/null +++ b/flatpak/python3-modules.json @@ -0,0 +1,107 @@ +{ + "name": "python3-modules", + "buildsystem": "simple", + "build-commands": [], + "modules": [ + { + "name": "python3-piexif", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"piexif\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/2c/d8/6f63147dd73373d051c5eb049ecd841207f898f50a5a1d4378594178f6cf/piexif-1.1.3-py2.py3-none-any.whl", + "sha256": "3bc435d171720150b81b15d27e05e54b8abbde7b4242cddd81ef160d283108b6" + } + ] + }, + { + "name": "python3-pillow", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pillow\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", + "sha256": "368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20" + } + ] + }, + { + "name": "python3-optima35", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"optima35\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/da/44/f304f2f1f333204dcf57cb883c15acf32aaecdca13730b9822dce79a1b3e/optima35-1.0.1-py3-none-any.whl", + "sha256": "76f5623c4d6bfa57230c9d485a16f6336e91fc8a0a6ad88d42618b5193c9f587" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/2c/d8/6f63147dd73373d051c5eb049ecd841207f898f50a5a1d4378594178f6cf/piexif-1.1.3-py2.py3-none-any.whl", + "sha256": "3bc435d171720150b81b15d27e05e54b8abbde7b4242cddd81ef160d283108b6" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", + "sha256": "368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20" + } + ] + }, + { + "name": "python3-PyYAML", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"PyYAML\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", + "sha256": "d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e" + } + ] + }, + { + "name": "python3-hatchling", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"hatchling\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", + "sha256": "d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", + "sha256": "09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", + "sha256": "a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", + "sha256": "44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/2b/c5/6422dbc59954389b20b2aba85b737ab4a552e357e7ea14b52f40312e7c84/trove_classifiers-2025.1.15.22-py3-none-any.whl", + "sha256": "5f19c789d4f17f501d36c94dbbf969fb3e8c2784d008e6f5164dd2c3d6a2b07c" + } + ] + } + ] +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 987ed7c..b9e2577 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "OptimaLab35" dynamic = ["version"] -authors = [{ name = "Mr. Finchum" }] +authors = [{ name = "Mr Finchum" }] description = "User interface for optima35." readme = "pip_README.md" requires-python = ">=3.8" diff --git a/src/flatpak/app-icon.png b/src/flatpak/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5407c4605581b0a322f4f84438a98fb71efc4631 GIT binary patch literal 50453 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%0Dqp<jVmX-G|ITfC#zWK7>o}8_!N}z6_E61 zP2RpU+1Ufnk5AszWWUA{d~pvZ?PEzv{5cn{=wMZR!Y;)#X69)P%y|>)*l}{6wi6uf zzsp<01zy&PJy2SIYnJ}Rw&*crWr=Kwh0Rv0Mu8)|klBx{u%+SpybKG05O6GbsvKuX zp>8V8xU32W)>MjaHTbSIEm1mE0{2pPO0tQFSOBrWqS5t!f)P+=e*-Seu=dTvObW`- zQ_na&uN)#`EArqFWwUSj=xtC*8Mn>LfkSN_fne+MnwCqE9Nqed2{v18g z)4dxgdBfp)Hn>{lrfms}+hWb!yU~U(`MtJ~M4(zrjseIa9glk*8=*4je|Up(A?$M* zoC>l`QS-T|G*SoLu#~2k5_iG%g;J!@guHHx#SfKGZXb{-Gc-5c)EqJJJZON-f7={O zUtd^3vD0koq^JJnIVPR7xBo@c6c4jFhUb@u-PM8yl~XyNvr+UqglIfo+3II|4=nQqh4I5;POn_jsO&{3+OS zuI#|LtsK{ByW4(K*p(tsUe4>nU@EJ{5kE5OM!fGe&&bd1xwdBUCu-gu#N_?ycj=>& zxs?C;^8pi+>4tgnr%xRPxK_LEmF<~Qw;9bRxk`;c69k3f-8-LnqrX15<%jq;{?^&p zc*r=;pN*FTGdNX~`Gkv;1ncW`6eszhd+}Zzy$~_n6IMpKWyc5po!#Bf;oKh~V*ngx zH;Kg%0kVucI|soUC5o?fpA}ujsud`h`%1w|gfIMeW8*@z4En<0rB*~v+aJ+LR1n@9 z&Gd@HNi4MZ9eIy}hMX`!Mh0VC?%9mk)yYJ}ORux_%kxA1--QqT&=_JPQC2mE7d&H0(sVlW@LdiS~I-Y?4V2ui$!MD6nJx8u}6FnIH> zH6I8oKwCmY{1Bh9h+m<0Gm2UPjq)2pe{%}1Cts=1zA^b~1WrB`c=}YLYc-KwB*7*B zXSUbbwZC#=G8nbv}LUjO)74Xh-wxf6{aTI@h}MN)dE=OIrUDU> zXsdl>e$SqX(n&v1b{o?MW>%0Wp%gGIv1e6q@l2^b$P}|AK#D;AlMR|%yi@yK@0xdR z_XMft+wmL2*B3>^!_2g3Yz3`#P9CeZf&!0DfE1QINWm2Is0+~0kjDv++jk6|%;}sO z1#}lHLocV*zhiWnPda{>aQKX{LQ6aHF#i>?Q2%PbVU$k)4>^2lzE@{l$U{I5D>!Iy z#c0U~$P-1x8_haCfV$GFAZT{}Wb(y&FWz5^y_>K*?(bZX7aK3{`bfvOguE_Bn|&eV zI8aeuof`$EyfSSOh*He^okn>hHi&iLDP!}@9xo@i(%$hb@ThK=T`e}ukP=Zu|K2Av z`!N*)waFD}%+}CF1agidujVOY>D9NAHsi08Wf(dpa1tvlMjv+�F*3O0tyY($tJ{ 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 literal 0 HcmV?d00001 diff --git a/src/flatpak/net.boxyfoxy.OptimaLab35.desktop b/src/flatpak/net.boxyfoxy.OptimaLab35.desktop new file mode 100644 index 0000000..157050d --- /dev/null +++ b/src/flatpak/net.boxyfoxy.OptimaLab35.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Version=1.0 +Type=Application + +Name=OptimaLab35 +Comment=A simple tool for editing images and managing metadata, designed for streamlined organization and EXIF adjustments. +Categories=Graphics;Photography; + +Icon=net.boxyfoxy.OptimaLab35 +Exec=OptimaLab35 +Terminal=false diff --git a/src/flatpak/net.boxyfoxy.OptimaLab35.metainfo.xml b/src/flatpak/net.boxyfoxy.OptimaLab35.metainfo.xml new file mode 100644 index 0000000..2b149ef --- /dev/null +++ b/src/flatpak/net.boxyfoxy.OptimaLab35.metainfo.xml @@ -0,0 +1,33 @@ + + + net.boxyfoxy.OptimaLab35 + + OptimaLab35 + A simple tool for editing images and managing metadata, designed for streamlined organization and EXIF adjustments. + + CC0-1.0 + AGPL-3.0-only + 1.0.0 + + 1.0.0 + 2024-01-30 + + +

+ OptimaLab35 is a image editing and metadata management tool, designed with analog photography in mind. It provides an intuitive way to modify and add EXIF data, making scanned film images easier to organize. With features tailored for efficient batch processing, it helps photographers maintain a structured archive while preserving key details about their images. +

+
+ + net.boxyfoxy.OptimaLab35.desktop + + + https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/main_tab.png + + + https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/exif_tab.png + + + https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/preview_window.png + + +
diff --git a/src/pyproject.toml b/src/pyproject.toml new file mode 100644 index 0000000..0b86791 --- /dev/null +++ b/src/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "OptimaLab35" +dynamic = ["version"] +authors = [{ name = "Mr Finchum" }] +description = "User interface for optima35." +requires-python = ">=3.8" +dependencies = ["optima35>=1.0.0, <2.0.0", "PyYAML"] +classifiers = [ + "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/OptimaLab35" + +[project.scripts] +OptimaLab35 = "OptimaLab35.__main__:main" + +[tool.hatch.build.targets.wheel] +packages = ["OptimaLab35"] + +[tool.hatch.version] +path = "OptimaLab35/__init__.py" From 89c3fb3e68f36696de5301a0cb7be1f148feb334 Mon Sep 17 00:00:00 2001 From: Mr Finchum Date: Thu, 30 Jan 2025 16:45:02 +0000 Subject: [PATCH 08/49] feat: Enhanced preview window --- CHANGELOG.md | 17 +++-- src/OptimaLab35/gui.py | 93 ++++++++++++++++++++++------ src/OptimaLab35/ui/main_window.py | 5 -- src/OptimaLab35/ui/main_window.ui | 7 --- src/OptimaLab35/ui/preview_window.py | 24 ++++++- src/OptimaLab35/ui/preview_window.ui | 27 +++++++- src/OptimaLab35/ui/simple_dialog.py | 12 +++- 7 files changed, 141 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5643294..c103ff4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,14 @@ # Changelog -## 0.6.x -### 0.6.0: Initial Flatpak Support +## 0.7.x +### 0.7.0: Enhanced Preview +- Images loaded into the preview window are now scaled while maintaining aspect ratio. +- Added live updates: changes to brightness, contrast, or grayscale are applied immediately. + - ⚠ This may crush the system depending on image size and system specifications. +- Removed Settings from menuBar, and extended the about window. +--- + +## 0.6.0: Initial Flatpak Support - Started Flatpak package building. - Not added to Flathub yet, as only stable software is hosted there. - Not fully completed, icon, name, and description are included, but the version is missing for some reason. @@ -10,14 +17,12 @@ --- -## 0.5.x -### 0.5.0 +## 0.5.0 - Removed all leftover of tui code that was hiding in some classes. --- -## 0.4.x -### 0.4.0 +## 0.4.0 - Fixed a critical issue that prevented the program from functioning. - Updated compatibility to align with the **upcoming** optima35 **release**. diff --git a/src/OptimaLab35/gui.py b/src/OptimaLab35/gui.py index 4fc4afb..1194845 100644 --- a/src/OptimaLab35/gui.py +++ b/src/OptimaLab35/gui.py @@ -10,7 +10,7 @@ from OptimaLab35.ui.exif_handler_window import ExifEditor from OptimaLab35.ui.simple_dialog import SimpleDialog # Import the SimpleDialog class from OptimaLab35 import __version__ -from PySide6.QtCore import QRunnable, QThreadPool, Signal, QObject, QRegularExpression +from PySide6.QtCore import QRunnable, QThreadPool, Signal, QObject, QRegularExpression, Qt from PySide6 import QtWidgets from PySide6.QtWidgets import ( @@ -75,7 +75,6 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): self.ui.edit_exif_button.clicked.connect(self.open_exif_editor) self.ui.actionAbout.triggered.connect(self.info_window) - self.ui.actionPreview.triggered.connect(self.open_preview_window) self.ui.preview_Button.clicked.connect(self.open_preview_window) regex = QRegularExpression(r"^\d{1,2}\.\d{1,6}$") @@ -88,7 +87,7 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): # UI related function, changing parts, open, etc. def open_preview_window(self): self.preview_window.values_selected.connect(self.update_values) - self.preview_window.show() + self.preview_window.showMaximized() def update_values(self, value1, value2, checkbox_state): # Update main window's widgets with the received values @@ -98,12 +97,20 @@ class OptimaLab35(QMainWindow, Ui_MainWindow): self.ui.grayscale_checkBox.setChecked(checkbox_state) def info_window(self): - # ChatGPT, mainly info_text = f"""

{self.name} v{self.version}

-

(C) 2024-2025 Mr. Finchum aka CodeByMrFinchum

-

{self.name} is a GUI for {self.o.name} (v{self.o.version}).

-

Both projects are in active development, for more details, visit:

+

(C) 2024-2025 Mr Finchum aka CodeByMrFinchum

+

{self.name} is a GUI for {self.o.name} (v{self.o.version}), enhancing its functionality with a\nuser-friendly interface for efficient image and metadata management.

+ +

Features:

+
    +
  • Image processing: resize, grayscale, brightness/contrast adjustments
  • +
  • Live image preview: see changes before applying
  • +
  • EXIF management: add, copy, remove metadata, GPS support
  • +
  • Watermarking: add custom text-based watermarks
  • +
+ +

For more details, visit: