2 Commits

Author SHA1 Message Date
SitiWeb
43c5bdac8c Add image processor PNG file to UI assets 2026-01-23 16:53:14 +01:00
SitiWeb
551948d828 Enable AVIF support in image processing and update dependencies 2026-01-23 16:41:28 +01:00
10 changed files with 178 additions and 7 deletions

55
.github/workflows/build-release.yml vendored Normal file
View File

@@ -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/**

View File

@@ -6,6 +6,12 @@ from ui.options_window import OptionsWindow
from config.encrypt_config import ConfigEncryptor from config.encrypt_config import ConfigEncryptor
from api.woocommerce_api import get_first_image from api.woocommerce_api import get_first_image
from PIL import Image, ImageTk 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 pprint import pformat
from api.woocommerce_api import process_product_images, process_all_products, search_product, get_first_image_path, get_product from api.woocommerce_api import process_product_images, process_all_products, search_product, get_first_image_path, get_product
import customtkinter as ctk import customtkinter as ctk

View File

@@ -6,7 +6,7 @@ a = Analysis(
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[], datas=[],
hiddenimports=[], hiddenimports=['pillow_avif'],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},
runtime_hooks=[], runtime_hooks=[],
@@ -30,6 +30,7 @@ exe = EXE(
upx_exclude=[], upx_exclude=[],
runtime_tmpdir=None, runtime_tmpdir=None,
console=False, console=False,
icon='ui/images/image_processor.ico',
disable_windowed_traceback=False, disable_windowed_traceback=False,
argv_emulation=False, argv_emulation=False,
target_arch=None, target_arch=None,

30
main.py
View File

@@ -1,8 +1,10 @@
""" """
Main module for the Image Processor application. Main module for the Image Processor application.
""" """
from PIL import Image from PIL import Image, ImageTk
import customtkinter as ctk import customtkinter as ctk
import os
import sys
from ui.menu import MenuBar # Import the new MenuBar class from ui.menu import MenuBar # Import the new MenuBar class
from ui.log_frame import LogWindow from ui.log_frame import LogWindow
from ui.button_frame import ButtonFrame from ui.button_frame import ButtonFrame
@@ -15,6 +17,15 @@ from controller import AppController
from ui.preview_frame import PreviewFrame # Import the new PreviewFrame class from ui.preview_frame import PreviewFrame # Import the new PreviewFrame class
def resource_path(relative_path: str) -> str:
"""Get absolute path to a resource (dev or PyInstaller)."""
try:
base_path = sys._MEIPASS # type: ignore[attr-defined]
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
class ImageProcessorApp: class ImageProcessorApp:
""" """
@@ -34,6 +45,23 @@ class ImageProcessorApp:
self.root.title("Image Processor") self.root.title("Image Processor")
self.root.geometry("553x800") self.root.geometry("553x800")
# Window/taskbar icon (Windows prefers .ico)
try:
ico_path = resource_path("ui/images/image_processor.ico")
if os.path.exists(ico_path):
self.root.iconbitmap(ico_path)
except Exception:
pass
# Cross-platform icon (uses PNG)
try:
png_path = resource_path("ui/images/image_processor.png")
if os.path.exists(png_path):
self._icon_photo = ImageTk.PhotoImage(Image.open(png_path))
self.root.iconphoto(True, self._icon_photo)
except Exception:
pass
# Initialize the controller # Initialize the controller
self.controller = AppController(self.root) self.controller = AppController(self.root)

View File

@@ -3,8 +3,12 @@ import glob
import os import os
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
# Collect all PNG and JPG images in the ui/images directory # Collect UI images/icons in the ui/images directory
image_files = [(file, "ui/images") for file in glob.glob("ui/images/*.*") if file.endswith(('.png', '.jpg', '.jpeg'))] image_files = [
(file, "ui/images")
for file in glob.glob("ui/images/*.*")
if file.lower().endswith((".png", ".jpg", ".jpeg", ".ico"))
]
block_cipher = None block_cipher = None
@@ -14,7 +18,7 @@ a = Analysis(
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=image_files, datas=image_files,
hiddenimports=[], hiddenimports=['pillow_avif'],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},
runtime_hooks=[], runtime_hooks=[],
@@ -33,13 +37,15 @@ exe = EXE(
a.zipfiles, a.zipfiles,
a.datas, a.datas,
[], [],
name='main', name='image_processor',
debug=False, debug=False,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,
upx=True, upx=True,
upx_exclude=[], upx_exclude=[],
runtime_tmpdir=None, runtime_tmpdir=None,
icon='ui/images/image_processor.ico',
disable_windowed_traceback=False, disable_windowed_traceback=False,
argv_emulation=False, argv_emulation=False,

View File

@@ -12,6 +12,12 @@ Prerequisites
pip install pillow 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. Additional Libraries: Ensure you have any additional libraries your utility functions (file_operations, image_processing) depend on.
Application Setup Application Setup

15
requirements.txt Normal file
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,7 +1,13 @@
import os import os
import tempfile
from wand.image import Image from wand.image import Image
from wand.color import Color 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: class ImageProcessor:
def __init__(self, canvas_width=900, canvas_height=900, background_color="transparent", image_size="fit"): 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) image_path = os.path.normpath(image_path)
output_path = os.path.normpath(output_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) self.log_message(f"Original image size: {img.width}x{img.height}", log)
if self.image_size == "contain": if self.image_size == "contain":
self._contain(img) self._contain(img)
@@ -71,6 +90,41 @@ class ImageProcessor:
# Save the image to the final output path # Save the image to the final output path
canvas.save(filename=final_output_path) canvas.save(filename=final_output_path)
self.log_message(f"Saved to: {final_output_path}", log) 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): def _cover(self, img:Image):