Skip to content

Commit d284924

Browse files
committed
v2.0.0: Modular architecture, CLI mode, presets, and full test suite
- Restructured from monolithic main.py into pixieswitch/ package - Fixed critical thread safety bug (Qt widgets modified from worker threads) - Fixed animated image resize (now applies to all frames, not just first) - Fixed strip_metadata silently destroying animation frames - Fixed preset filename injection (path traversal via user input) - Added CLI mode: python -m pixieswitch convert --help - Added settings persistence (QSettings, restored on startup) - Added folder drag-and-drop (recursive image discovery) - Added keyboard shortcuts (Ctrl+O, Delete, Ctrl+Enter, Escape, etc.) - Added conversion presets (Web Optimized, Archive, Social Media, custom) - Added image preview with dimensions and file size - Added conversion stats (success/fail count, elapsed time, bytes written) - Added cancel button with Escape shortcut - Added "Open Output Folder" button - Added overwrite and close-during-conversion warnings - Added dark/light theme auto-detection (Qt 6.5+) - Added APNG output support for animated sources - Added progress bar text overlay (12/50) and title bar progress - Added file count display in status bar - Added drag reordering in file list - Added proper .ico for Windows builds - Added CI pipeline (GitHub Actions, ruff, mypy, pytest) - Added pyproject.toml for unified tool configuration - 32 tests covering converter, formats, presets, and settings - ruff and mypy clean
1 parent fb42e97 commit d284924

25 files changed

+2056
-438
lines changed

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
jobs:
10+
test:
11+
strategy:
12+
matrix:
13+
os: [ubuntu-latest, windows-latest, macos-latest]
14+
python-version: ["3.10", "3.11", "3.12", "3.13"]
15+
runs-on: ${{ matrix.os }}
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: ${{ matrix.python-version }}
21+
- name: Install dependencies
22+
run: |
23+
pip install -r requirements.txt
24+
pip install pytest pytest-cov ruff mypy
25+
- name: Lint
26+
run: ruff check pixieswitch/ tests/
27+
- name: Type check
28+
run: mypy pixieswitch/ --ignore-missing-imports
29+
- name: Test
30+
run: pytest --cov=pixieswitch --cov-report=term-missing -v

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ __pycache__/
22
*.pyc
33
dist/
44
build/
5-
*.spec
65
.venv/
76
venv/
7+
*.egg-info/
8+
.mypy_cache/
9+
.pytest_cache/
10+
.ruff_cache/
11+
.claude/
12+
tests/fixtures/

CLAUDE.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Project Guide - PixieSwitch
2+
3+
## Project overview
4+
PixieSwitch is a batch image converter with a PySide6 GUI and CLI mode. It supports JPG, PNG, GIF, WebP, TIFF, BMP, JPEG2000, HEIF/HEIC/AVIF formats with animation preservation, metadata control, and resizing.
5+
6+
## Build & run
7+
```bash
8+
pip install -r requirements.txt # runtime deps
9+
pip install pytest pytest-cov ruff mypy # dev deps
10+
python -m pixieswitch # GUI
11+
python -m pixieswitch convert --help # CLI
12+
python main.py # legacy shim
13+
```
14+
15+
## Test, lint, type-check
16+
```bash
17+
pytest --cov=pixieswitch -v
18+
ruff check pixieswitch/ tests/
19+
mypy pixieswitch/ --ignore-missing-imports
20+
```
21+
22+
## Architecture
23+
```
24+
pixieswitch/
25+
__init__.py - APP_NAME, __version__
26+
__main__.py - entry point (CLI args -> cli.py, else -> GUI)
27+
converter.py - core conversion (ConvertTask -> ConvertResult, no GUI deps)
28+
formats.py - format constants, human_size(), safe_filename()
29+
settings.py - QSettings wrapper with typed properties
30+
cli.py - argparse CLI mode
31+
gui/
32+
main_window.py - ConverterWindow (main widget)
33+
drop_list.py - DropListWidget (file list with drag/drop/dedup)
34+
style.py - dark/light theme detection
35+
presets.py - preset management (JSON in user config dir)
36+
```
37+
38+
## Key design decisions
39+
- **Thread safety**: Worker threads emit a Qt signal; all widget updates happen in the GUI thread slot. Callbacks use `weakref` to the window to prevent use-after-free on close.
40+
- **Converter has zero GUI deps**: `converter.py` and `formats.py` are testable without Qt installed. GUI tests are skipped via `pytest.importorskip("PySide6")`.
41+
- **Animation handling**: Frames are extracted and processed (resize, strip metadata) individually before saving. This avoids the historical bugs where resize/strip only affected the first frame.
42+
- **Presets stored as JSON**: One file per preset in `QStandardPaths.AppConfigLocation/presets/`. Names are sanitized via `safe_filename()` with path-traversal checks.
43+
44+
## Conventions
45+
- Line length: 100 (configured in pyproject.toml)
46+
- Linter: ruff (E, F, W, I, UP, B, SIM rules)
47+
- Type checker: mypy (ignore-missing-imports for optional deps like PySide6, pillow-heif, imageio)
48+
- Tests: pytest, fixtures auto-generate small test images in `tests/fixtures/` (gitignored)
49+
- No GUI tests (low ROI); test the converter and format logic directly
50+
51+
## Watch out for
52+
- Pillow type stubs are incomplete. `Image.open()` returns `ImageFile` but operations return `Image.Image`. Use `type: ignore[assignment]` on the open call.
53+
- `QSettings` serializes bools differently per platform. The `AppSettings` properties handle this with `in (True, "true")` checks.
54+
- The `_running` flag + button disable + shortcut guard are all needed to prevent double-convert. Shortcuts bypass `QPushButton.setEnabled(False)`.

PixieSwitch.spec

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# -*- mode: python ; coding: utf-8 -*-
2+
3+
4+
a = Analysis(
5+
['main.py'],
6+
pathex=[],
7+
binaries=[],
8+
datas=[],
9+
hiddenimports=['pixieswitch', 'pixieswitch.gui', 'pixieswitch.gui.main_window'],
10+
hookspath=[],
11+
hooksconfig={},
12+
runtime_hooks=[],
13+
excludes=[],
14+
noarchive=False,
15+
optimize=0,
16+
)
17+
pyz = PYZ(a.pure)
18+
19+
exe = EXE(
20+
pyz,
21+
a.scripts,
22+
[],
23+
exclude_binaries=True,
24+
name='PixieSwitch',
25+
debug=False,
26+
bootloader_ignore_signals=False,
27+
strip=False,
28+
upx=True,
29+
console=False,
30+
disable_windowed_traceback=False,
31+
argv_emulation=False,
32+
target_arch=None,
33+
codesign_identity=None,
34+
entitlements_file=None,
35+
icon=['pixieswitch.ico'],
36+
)
37+
coll = COLLECT(
38+
exe,
39+
a.binaries,
40+
a.datas,
41+
strip=False,
42+
upx=True,
43+
upx_exclude=[],
44+
name='PixieSwitch',
45+
)

README.md

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
1-
# PixieSwitch Batch Image Converter (GUI)
1+
# PixieSwitch - Batch Image Converter (GUI + CLI)
22

3-
**PixieSwitch** is a fun, simple GUI to convert images between popular formats. Drop in files, pick an output format, tweak a few options, and go. It handles batch conversion, optional metadata preservation/stripping, basic resizing, and animations (GIF/WebP).
3+
**PixieSwitch** is a fun, simple GUI to convert images between popular formats. Drop in files, pick an output format, tweak a few options, and go. It handles batch conversion, optional metadata preservation/stripping, basic resizing, and animations (GIF/WebP/APNG).
44

55
## Features
66

7-
- Drag & Drop multiple images
7+
- Drag & Drop files **and folders** (recursively adds images)
88
- Output formats: JPG/JPEG, PNG, GIF, WebP, TIFF, BMP, JPEG2000 (JP2/J2K), and optional HEIF/HEIC/AVIF (with pillow-heif)
9-
- Animation-aware: Keep animation for GIF/WebP (when source is animated)
9+
- Animation-aware: Keep animation for GIF/WebP/APNG (when source is animated)
1010
- Metadata: preserve EXIF (JPEG) or strip all metadata
1111
- Resize: constrain longest edge (px)
1212
- Quality: set lossy compression quality for JPEG/WebP/AVIF/etc.
13-
- PNG compression level: 0 (fast/big) 9 (slow/small)
13+
- PNG compression level: 0 (fast/big) ... 9 (slow/small)
1414
- Suffix & Output folder controls
1515
- Multithreaded conversion
16+
- **Settings persistence** - your options are remembered between sessions
17+
- **Keyboard shortcuts** - Ctrl+O (add), Delete (remove), Ctrl+Enter (convert), etc.
18+
- **Presets** - built-in (Web Optimized, Archive, Social Media) and custom
19+
- **Image preview** - select a file to see thumbnail, dimensions, and size
20+
- **Conversion stats** - success/fail count, elapsed time, bytes written
21+
- **Dark/light theme** - auto-detected from system settings
22+
- **CLI mode** - `python -m pixieswitch convert --help`
1623

1724
## Install
1825

19-
Python 3.9+ recommended (tested on 3.13). Windows, macOS, Linux.
26+
Python 3.10+ recommended (tested on 3.13). Windows, macOS, Linux.
2027

2128
1. Create & activate a virtual env (recommended)
2229
2. `pip install -r requirements.txt`
23-
3. `python main.py`
30+
3. `python -m pixieswitch` (GUI) or `python main.py`
31+
32+
### CLI usage
33+
34+
```
35+
python -m pixieswitch convert --input *.png --format webp --quality 85 --output ./out/
36+
python -m pixieswitch convert --input ./photos/ --format jpg --resize 1920
37+
python -m pixieswitch convert --help
38+
```
2439

2540
### Optional extras
2641

@@ -29,39 +44,58 @@ Python 3.9+ recommended (tested on 3.13). Windows, macOS, Linux.
2944

3045
## Build a standalone app
3146

32-
### Windows (EXE)
33-
3447
```
3548
pip install pyinstaller
36-
pyinstaller --noconfirm --windowed --name PixieSwitch --icon NONE main.py
49+
pyinstaller PixieSwitch.spec
3750
# Output in dist/PixieSwitch/
3851
```
3952

40-
### macOS (App bundle/DMG)
53+
## Development
4154

4255
```
43-
pip install pyinstaller
44-
pyinstaller --noconfirm --windowed --name PixieSwitch --icon NONE main.py
45-
# Optional: codesign/notarize per Apple docs
56+
pip install -r requirements.txt
57+
pip install pytest pytest-cov ruff mypy
58+
pytest --cov=pixieswitch -v
59+
ruff check pixieswitch/ tests/
60+
mypy pixieswitch/ --ignore-missing-imports
4661
```
4762

48-
### Linux (AppImage)
63+
## Keyboard shortcuts
4964

50-
Use pyinstaller for a folder build, then package with tools like appimagetool if desired.
65+
| Action | Shortcut |
66+
|---|---|
67+
| Add Files | Ctrl+O |
68+
| Remove Selected | Delete |
69+
| Clear List | Ctrl+Shift+Delete |
70+
| Convert | Ctrl+Enter |
71+
| Cancel | Escape |
72+
| Browse Output | Ctrl+Shift+O |
5173

5274
## Usage tips
5375

54-
- Transparency JPEG: JPEG has no alpha; we composite on white by default.
76+
- Transparency -> JPEG: JPEG has no alpha; we composite on white by default.
5577
- EXIF vs Strip: "Strip ALL metadata" overrides "Preserve EXIF."
56-
- Animated output: Only GIF & WebP are guaranteed here. Other targets will use the first frame.
78+
- Animated output: GIF, WebP, and APNG are supported. Other targets will use the first frame.
5779
- JPEG2000: Quality handling varies by Pillow build. If unsupported, you'll see an error per file.
5880

59-
## Roadmap
81+
## Project structure
6082

61-
- APNG output (where Pillow supports it)
62-
- Color profile controls, ICC copy
63-
- Presets & CLI runner
64-
- Bulk folder watch mode
83+
```
84+
pixieswitch/
85+
__init__.py # APP_NAME, __version__
86+
__main__.py # Entry point
87+
converter.py # Core conversion logic (no GUI deps)
88+
formats.py # Format constants
89+
settings.py # QSettings wrapper
90+
cli.py # CLI mode
91+
gui/
92+
main_window.py # Main window
93+
drop_list.py # Drag-and-drop list widget
94+
style.py # Theme detection and stylesheets
95+
presets.py # Preset management
96+
main.py # Thin shim -> pixieswitch.__main__
97+
tests/ # pytest test suite
98+
```
6599

66100
## License
67101

0 commit comments

Comments
 (0)