Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43c5bdac8c | ||
|
|
551948d828 |
55
.github/workflows/build-release.yml
vendored
Normal file
55
.github/workflows/build-release.yml
vendored
Normal 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/**
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ a = Analysis(
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hiddenimports=['pillow_avif'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
@@ -30,6 +30,7 @@ exe = EXE(
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
icon='ui/images/image_processor.ico',
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
|
||||
30
main.py
30
main.py
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
Main module for the Image Processor application.
|
||||
"""
|
||||
from PIL import Image
|
||||
from PIL import Image, ImageTk
|
||||
import customtkinter as ctk
|
||||
import os
|
||||
import sys
|
||||
from ui.menu import MenuBar # Import the new MenuBar class
|
||||
from ui.log_frame import LogWindow
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
@@ -34,6 +45,23 @@ class ImageProcessorApp:
|
||||
self.root.title("Image Processor")
|
||||
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
|
||||
self.controller = AppController(self.root)
|
||||
|
||||
|
||||
14
main.spec
14
main.spec
@@ -3,8 +3,12 @@ import glob
|
||||
import os
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
# Collect all PNG and JPG images in the ui/images directory
|
||||
image_files = [(file, "ui/images") for file in glob.glob("ui/images/*.*") if file.endswith(('.png', '.jpg', '.jpeg'))]
|
||||
# Collect UI images/icons in the ui/images directory
|
||||
image_files = [
|
||||
(file, "ui/images")
|
||||
for file in glob.glob("ui/images/*.*")
|
||||
if file.lower().endswith((".png", ".jpg", ".jpeg", ".ico"))
|
||||
]
|
||||
|
||||
block_cipher = None
|
||||
|
||||
@@ -14,7 +18,7 @@ a = Analysis(
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=image_files,
|
||||
hiddenimports=[],
|
||||
hiddenimports=['pillow_avif'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
@@ -33,7 +37,7 @@ exe = EXE(
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='main',
|
||||
name='image_processor',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
@@ -41,6 +45,8 @@ exe = EXE(
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
|
||||
icon='ui/images/image_processor.ico',
|
||||
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
|
||||
@@ -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
|
||||
|
||||
15
requirements.txt
Normal file
15
requirements.txt
Normal 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
|
||||
BIN
ui/images/image_processor.ico
Normal file
BIN
ui/images/image_processor.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
ui/images/image_processor.png
Normal file
BIN
ui/images/image_processor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user