From 551948d8286b2251c81c61f2fd8e5ea0ef171743 Mon Sep 17 00:00:00 2001 From: SitiWeb <70724099+SitiWeb@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:41:28 +0100 Subject: [PATCH] Enable AVIF support in image processing and update dependencies --- .github/workflows/build-release.yml | 55 ++++++++++++++++++++++++++++ controller.py | 6 ++++ image_processor.spec | 2 +- main.spec | 2 +- readme.md | 6 ++++ requirements.txt | 15 ++++++++ utils/image_processing.py | 56 ++++++++++++++++++++++++++++- 7 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/build-release.yml create mode 100644 requirements.txt diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..24d3c9d --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,55 @@ +name: Build & Release (Windows) + +on: + push: + branches: [ main ] + tags: + - 'v*' + workflow_dispatch: {} + +permissions: + contents: write + +jobs: + build-windows: + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Install dependencies + shell: pwsh + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + python -m pip install pyinstaller + + - name: Build (PyInstaller spec) + shell: pwsh + run: | + pyinstaller --noconfirm --clean main.spec + + - name: Upload build artifact (push/dispatch) + if: startsWith(github.ref, 'refs/heads/') || github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: image_processor-windows + path: | + dist/** + + - name: Create GitHub Release (tags) + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + name: ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + generate_release_notes: true + files: | + dist/** diff --git a/controller.py b/controller.py index 9ab639a..f1173db 100644 --- a/controller.py +++ b/controller.py @@ -6,6 +6,12 @@ from ui.options_window import OptionsWindow from config.encrypt_config import ConfigEncryptor from api.woocommerce_api import get_first_image from PIL import Image, ImageTk + +# Enable AVIF support for Pillow previews when the optional plugin is installed. +try: + import pillow_avif # type: ignore +except Exception: + pillow_avif = None from pprint import pformat from api.woocommerce_api import process_product_images, process_all_products, search_product, get_first_image_path, get_product import customtkinter as ctk diff --git a/image_processor.spec b/image_processor.spec index 24882f6..57c3c33 100644 --- a/image_processor.spec +++ b/image_processor.spec @@ -6,7 +6,7 @@ a = Analysis( pathex=[], binaries=[], datas=[], - hiddenimports=[], + hiddenimports=['pillow_avif'], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/main.spec b/main.spec index 7618bee..cb292c6 100644 --- a/main.spec +++ b/main.spec @@ -14,7 +14,7 @@ a = Analysis( pathex=[], binaries=[], datas=image_files, - hiddenimports=[], + hiddenimports=['pillow_avif'], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/readme.md b/readme.md index d965a96..806e9c7 100644 --- a/readme.md +++ b/readme.md @@ -12,6 +12,12 @@ Prerequisites pip install pillow + Optional (AVIF input): Install the Pillow AVIF plugin so previews and conversions can open `.avif` files: + + sh + + pip install pillow-avif-plugin + Additional Libraries: Ensure you have any additional libraries your utility functions (file_operations, image_processing) depend on. Application Setup diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bafa058 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +certifi==2026.1.4 +cffi==2.0.0 +charset-normalizer==3.4.4 +cryptography==46.0.3 +customtkinter==5.2.2 +darkdetect==0.8.0 +idna==3.11 +packaging==26.0 +pillow==12.1.0 +pillow-avif-plugin==1.5.5 +pycparser==3.0 +requests==2.32.5 +urllib3==2.6.3 +Wand==0.6.13 +WooCommerce==3.0.0 diff --git a/utils/image_processing.py b/utils/image_processing.py index 305b23f..9831f11 100644 --- a/utils/image_processing.py +++ b/utils/image_processing.py @@ -1,7 +1,13 @@ import os +import tempfile from wand.image import Image from wand.color import Color +try: + from PIL import Image as PILImage +except Exception: # Pillow is also used elsewhere; keep this optional here. + PILImage = None + class ImageProcessor: def __init__(self, canvas_width=900, canvas_height=900, background_color="transparent", image_size="fit"): """ @@ -48,7 +54,20 @@ class ImageProcessor: image_path = os.path.normpath(image_path) output_path = os.path.normpath(output_path) - with Image(filename=image_path) as img: + converted_tmp_path = None + img = None + try: + try: + img = Image(filename=image_path) + except Exception as e: + # Wand/ImageMagick AVIF support depends on the installed ImageMagick build. + # If it can't read AVIF, fall back to Pillow (+ pillow-avif-plugin) and convert to PNG. + if os.path.splitext(image_path)[1].lower() != ".avif": + raise + converted_tmp_path = self._convert_avif_to_temp_png(image_path, log) + img = Image(filename=converted_tmp_path) + self.log_message(f"Opened AVIF via Pillow fallback: {image_path}", log) + self.log_message(f"Original image size: {img.width}x{img.height}", log) if self.image_size == "contain": self._contain(img) @@ -71,6 +90,41 @@ class ImageProcessor: # Save the image to the final output path canvas.save(filename=final_output_path) self.log_message(f"Saved to: {final_output_path}", log) + finally: + try: + if img is not None: + img.close() + finally: + if converted_tmp_path and os.path.exists(converted_tmp_path): + try: + os.remove(converted_tmp_path) + except Exception: + pass + + + def _convert_avif_to_temp_png(self, image_path, log=None): + if PILImage is None: + raise RuntimeError( + "AVIF input requires Pillow. Install Pillow + pillow-avif-plugin to enable AVIF decoding." + ) + + try: + import pillow_avif # type: ignore + except Exception: + raise RuntimeError( + "AVIF input requires the optional dependency 'pillow-avif-plugin'. " + "Install it with: pip install pillow-avif-plugin" + ) + + with PILImage.open(image_path) as im: + # Preserve alpha if present; Wand will composite onto the selected background. + if im.mode not in ("RGB", "RGBA"): + im = im.convert("RGBA") + tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png") + tmp.close() + im.save(tmp.name, format="PNG") + self.log_message(f"Converted AVIF to temporary PNG: {tmp.name}", log) + return tmp.name def _cover(self, img:Image):