Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions WARP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# WARP.md

This file provides guidance to WARP (warp.dev) when working with code in this repository.

## Project Overview

FastStack is an ultra-fast JPEG viewer for Windows designed for culling and selecting RAW files for focus stacking. It displays JPEGs as proxies for RAW files, optimized for instant next/prev navigation, smooth zoom/pan, and integration with Helicon Focus for processing focus stacks.

**Key Technologies:**
- Python 3.11+ with PySide6 (Qt6) for UI
- Qt Quick (QML) for the rendering layer
- PyTurboJPEG (libjpeg-turbo) for fast JPEG decoding
- Byte-aware LRU caching for image data
- ThreadPoolExecutor for concurrent prefetching

## Development Commands

### Running the Application

```powershell
# Development run with path argument
python -m faststack.app "C:\path\to\images"

# Or from the inner faststack directory
python -m faststack.app "C:\path\to\images"

# Run from the installed package entry point (if installed)
faststack "C:\path\to\images"
```

### Testing

```powershell
# Run all tests
pytest

# Run tests from the faststack directory
pytest faststack/faststack/tests

# Run specific test file
pytest faststack/faststack/tests/test_cache.py

# Run with verbose output
pytest -v
```

### Building Distribution

```powershell
# Build standalone Windows executable using PyInstaller
pyinstaller faststack/faststack.spec

# Output will be in dist/FastStack/FastStack.exe
```

### Installing Dependencies

```powershell
# Install from requirements.txt (preferred for dev)
pip install -r faststack/requirements.txt

# Or install the package in editable mode
pip install -e faststack/
```

## Architecture

### Core Module Structure

The codebase follows a modular architecture split into distinct concerns:

**faststack/faststack/** (main package):
- `app.py` - Main entry point, `AppController` manages application state and coordinates all subsystems
- `config.py` - Configuration management via INI file (`%APPDATA%\faststack\faststack.ini`)
- `models.py` - Core data models (`ImageFile`, `EntryMetadata`, `DecodedImage`, `Sidecar`)
- `logging_setup.py` - Application logging setup

**faststack/faststack/imaging/** - Image processing and caching:
- `jpeg.py` - JPEG decoding with PyTurboJPEG (fallback to Pillow), supports resized decoding
- `cache.py` - Byte-aware LRU cache (`ByteLRUCache`) that tracks memory usage in bytes
- `prefetch.py` - Background prefetching using `ThreadPoolExecutor` with generation-based cancellation

**faststack/faststack/io/** - I/O operations:
- `indexer.py` - Directory scanning, JPG→RAW pairing by stem and timestamp proximity (±2s)
- `sidecar.py` - Manages `faststack.json` sidecar file with atomic writes
- `watcher.py` - Filesystem watching for directory changes
- `helicon.py` - Launches Helicon Focus with selected RAW files

**faststack/faststack/ui/** - UI components:
- `provider.py` - `ImageProvider` for Qt image handling, `UIState` for QML bindings
- `keystrokes.py` - Keyboard event handling (`Keybinder`)

**faststack/faststack/qml/** - QML UI files:
- `Main.qml` - Main window and image viewer
- `Components.qml` - Reusable QML components
- `SettingsDialog.qml` - Settings dialog
- `FilterDialog.qml` - Filtering interface

### Key Architectural Patterns

#### 1. Two-Tier Caching with Display-Aware Prefetching

The app uses a sophisticated caching strategy:
- **Display generation tracking**: When window size or zoom state changes, `display_generation` increments, invalidating cached images
- **Cache keys**: Format `{index}_{display_generation}` ensures stale images are not reused
- **Prefetch radius**: Configurable (default 4), decodes images at indices `[i-N, i+N]`
- **Generation-based cancellation**: When navigating, `prefetcher.generation` increments, and workers check this before caching to avoid stale work

#### 2. Zero-Copy Image Pipeline

To minimize memory overhead:
- JPEG decoding produces contiguous `numpy` arrays (uint8, h×w×3)
- QImage is created with a direct pointer to the numpy buffer (`QImage(buf, w, h, w*3, Format_RGB888)`)
- The numpy array reference is kept alive for the QImage lifetime to prevent dangling pointers
- This approach is implemented via `DecodedImage.buffer` which stores a `memoryview`

#### 3. RAW-JPG Pairing Logic

`indexer.py` pairs JPEGs with RAWs by:
1. Scanning directory for JPGs and RAWs
2. Matching by stem (e.g., `IMG_0123.JPG` → `IMG_0123.CR3`)
3. Validating timestamp proximity (±2 seconds) to handle burst shooting
4. Supporting multiple RAW formats: `.CR3`, `.CR2`, `.ARW`, `.NEF`, `.ORF`, `.RW2`, `.RAF`, `.DNG`

#### 4. Sidecar Metadata Management

All user edits are non-destructive, stored in `faststack.json`:
- Schema version 2 format
- Tracks: flags, rejections, stack IDs, stacking status/date
- Atomic writes: write to temp file, then replace
- Filesystem watcher paused during writes to avoid recursion

#### 5. Performance Optimizations

Key techniques for sub-10ms cache hits:
- **Memory-mapped file I/O**: `mmap` for faster JPEG loading
- **PyTurboJPEG scaling factors**: Use hardware-accelerated downscaling (1/8, 1/4, 1/2) before Pillow resizing
- **Debounced resize handling**: 150ms debounce for window resize events
- **Optimal thread pool sizing**: `min(cpu_count() * 2, 8)` workers for I/O-bound JPEG decoding
- **Selective cache clearing**: Only clear when display dimensions or zoom state changes
- **BILINEAR resampling**: For large downscales (>4x), use faster BILINEAR instead of LANCZOS

## Development Guidelines

### Adding New Keyboard Shortcuts

Edit `ui/keystrokes.py` (`Keybinder.handle_key_press()`) to add new key bindings. The method maps Qt key events to AppController methods.

### Modifying Cache Behavior

The cache size and prefetch radius are configurable in `%APPDATA%\faststack\faststack.ini`:
```ini
[core]
cache_size_gb = 1.5
prefetch_radius = 4
```

Access via `config.getfloat('core', 'cache_size_gb')` or `config.getint('core', 'prefetch_radius')`.

### Adding Support for New RAW Formats

Add extensions to `RAW_EXTENSIONS` set in `io/indexer.py`:
```python
RAW_EXTENSIONS = {
".ORF", ".RW2", ".CR2", ".CR3", ".ARW", ".NEF", ".RAF", ".DNG",
".orf", ".rw2", ".cr2", ".cr3", ".arw", ".nef", ".raf", ".dng",
}
```

### Extending Sidecar Metadata

To add new metadata fields:
1. Update `EntryMetadata` dataclass in `models.py`
2. Update `Sidecar.version` in `models.py` if breaking changes
3. Handle migration logic in `SidecarManager.load()` in `io/sidecar.py`

### Working with the UI (QML)

- QML files are in `qml/` directory
- `Main.qml` is the entry point, connected to `AppController` via Qt signals/slots
- UI state is exposed via `UIState` object in `ui/provider.py`
- Use `@Slot` decorator for methods callable from QML
- Emit `dataChanged` signal from AppController to trigger UI updates

### PyInstaller Builds

The `faststack.spec` file handles packaging:
- Collects all PySide6 data files
- Includes turbojpeg binaries from `turbojpeg.lib_path`
- Adds hidden imports for `PySide6.QtQml`
- Produces single-folder distribution in `dist/FastStack/`

To add resources or fix missing imports, edit `faststack.spec`.

## Common Pitfalls

### Image Decode Performance Issues

If decoding is slow:
- Verify PyTurboJPEG is installed correctly (check logs for "PyTurboJPEG is available")
- Check if `turbojpeg.dll` is found (Windows) - should be in PyInstaller build
- Consider increasing prefetch radius for smoother navigation at cost of memory

### Cache Not Invalidating on Window Resize

The app uses debounced resize handling. If cache seems stale:
- Check `display_generation` is incrementing (logged as "Display size changed to...")
- Verify `cache_key = f"{index}_{display_generation}"` format in `prefetch.py`
- Ensure `sync_ui_state()` is called after generation increment

### Sidecar File Corruption

If `faststack.json` becomes corrupted:
- The app will log error and start with empty sidecar
- Consider implementing backup strategy in `SidecarManager.save()`
- Version number mismatch triggers fresh start

### Threading Issues with Prefetcher

When adding features that interact with the prefetcher:
- Always increment `generation` when invalidating work
- Check `self.generation != local_generation` before cache operations
- Use `cancel_all()` before clearing image list or display changes
121 changes: 121 additions & 0 deletions docs/COLOR_PROFILE_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# ICC Color Profile Support - Fix for Oversaturated Colors

## Problem
Images displayed in FastStack appeared overly bright and "cartoonish" compared to the same images viewed in Photoshop. The colors looked unrealistic and oversaturated.

## Root Cause
**FastStack was ignoring embedded ICC color profiles in JPEG files.**

When digital cameras or photo editing software save JPEG files, they often embed an ICC (International Color Consortium) color profile that describes the color space of the image. Common profiles include:
- **sRGB** - Standard RGB, most common for web and general use
- **Adobe RGB (1998)** - Wider gamut, common in professional photography
- **ProPhoto RGB** - Even wider gamut, used in high-end photography

The raw pixel values in a JPEG are meaningless without knowing what color space they're in. For example:
- RGB value (255, 0, 0) in sRGB is a different red than (255, 0, 0) in Adobe RGB
- Adobe RGB can represent more saturated colors than sRGB

### What Was Happening
1. **Photoshop** reads the embedded ICC profile and correctly transforms colors to the display's color space (usually sRGB)
2. **FastStack (before fix)** was decoding JPEGs and displaying the raw pixel values without any color transformation
3. This caused colors to appear incorrect - typically oversaturated and too bright

### Technical Details
Both TurboJPEG and Pillow's basic `Image.open()` extract raw RGB pixel values but **do not** automatically apply ICC profile transformations. The embedded profile is available via `Image.info['icc_profile']`, but must be explicitly processed.

## Solution
Added proper ICC color management to the JPEG decoding pipeline using Pillow's `ImageCms` module (which wraps the industry-standard LittleCMS2 library).

### Changes Made to `faststack/imaging/jpeg.py`

1. **Added ICC Profile Support Functions:**
- `_get_srgb_profile()` - Creates/caches an sRGB display profile
- `_apply_icc_profile(img)` - Transforms images from their embedded color space to sRGB

2. **Updated All Decode Functions:**
- `decode_jpeg_rgb()` - Now applies ICC transformation
- `decode_jpeg_thumb_rgb()` - Now applies ICC transformation
- `decode_jpeg_resized()` - Now applies ICC transformation BEFORE resizing

3. **Color Transformation Process:**
```python
# 1. Open JPEG and read embedded ICC profile
img = Image.open(io.BytesIO(jpeg_bytes))

# 2. Extract embedded profile from image metadata
source_profile = ImageCms.ImageCmsProfile(io.BytesIO(img.info['icc_profile']))

# 3. Create sRGB display profile
srgb_profile = ImageCms.createProfile('sRGB')

# 4. Transform from source color space to sRGB for display
img_converted = ImageCms.profileToProfile(
img,
source_profile,
srgb_profile,
renderingIntent=ImageCms.Intent.PERCEPTUAL
)
```

4. **Rendering Intent:**
We use `PERCEPTUAL` rendering intent, which is designed for photographic images and preserves the overall appearance while mapping out-of-gamut colors intelligently.

### Hybrid Approach: Best of Both Worlds
The implementation uses a **hybrid approach** that combines speed and accuracy:

1. **ICC Profile Extraction**: First, Pillow quickly extracts the ICC profile metadata (very fast, no full decode)
2. **Fast Decoding**: TurboJPEG decodes the raw pixel data at maximum speed
3. **Color Transformation**: If an ICC profile exists, transform the decoded array to sRGB using ImageCms
4. **Smart Caching**: ICC profiles and transformations are cached - when all photos in a directory use the same profile (typical for camera photos), only the first image pays the full cost

This gives us:
- ✅ **Fast decoding** with TurboJPEG (2-3x faster than Pillow for large images)
- ✅ **Accurate colors** with proper ICC profile handling
- ✅ **Smart caching** - 2.3x faster color transformation for subsequent images with same profile
- ✅ **Fallback to Pillow** if TurboJPEG is unavailable or fails

### Performance Impact

**First image with a new ICC profile (cold cache):**
- ICC profile extraction: ~0.5ms (metadata-only read)
- Profile object creation: ~5ms
- Transform creation: ~7ms
- Color transformation: ~10ms
- **Total overhead**: ~22ms

**Subsequent images with same ICC profile (warm cache):**
- ICC profile extraction: ~0.5ms
- Profile hash lookup: <0.1ms
- Cached transform application: ~9ms
- **Total overhead**: ~10ms
- **Speedup**: 2.3x faster than cold cache

**Images without ICC profiles:**
- No overhead at all - uses raw decoded data

**Real-world scenario:**
- A typical photo shoot with 100 images from the same camera:
- First image: 22ms overhead
- Next 99 images: 10ms overhead each
- Average: 10.1ms per image
- Without caching, every image would be 22ms

## Testing
Run `test_icc.py` to verify ICC profile handling:
```bash
python test_icc.py <path_to_jpeg>
```

The test will:
1. Check if the JPEG has an embedded ICC profile
2. Decode it with color management
3. Display statistics about the decoded image

## References
- [ICC Color Management](https://en.wikipedia.org/wiki/ICC_profile)
- [Pillow ImageCms Documentation](https://pillow.readthedocs.io/en/stable/reference/ImageCms.html)
- [LittleCMS](https://www.littlecms.com/)
- [Understanding Color Spaces](https://www.cambridgeincolour.com/tutorials/color-spaces.htm)

## Result
Images now display with accurate, natural-looking colors that match what you see in Photoshop and other color-managed applications.
36 changes: 36 additions & 0 deletions faststack/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
# ChangeLog

## [0.7.0] - 2025-11-20

### Added
- **High-DPI Display Support:** Images now render at full physical pixel resolution on 4K displays by accounting for `devicePixelRatio` in display size calculations.
- **Ctrl+0 Zoom Reset:** Added keyboard shortcut to reset zoom and pan to fit window (like Photoshop), with visual feedback.
- **Active Filter Indicator:** Footer now displays active filename filter in yellow bold text for better visibility.
- **Directory Path Display:** Title bar now shows the current working directory path, centered between menu and window controls.
- **WARP.md Documentation:** Created comprehensive development guide for Warp AI assistant with architecture, commands, and development patterns.

### Security
- **Executable Path Validation:** Added comprehensive validation for Photoshop and Helicon Focus executables:
- Validates executable exists and is in safe location (Program Files)
- Checks file type (.exe on Windows)
- Warns about executables outside known safe paths
- Detects directory traversal attempts
- **Subprocess Security Hardening:**
- Replaced unsafe `.split()` with `shlex.split()` for proper argument parsing
- Explicitly set `shell=False` to prevent shell injection attacks
- Added file descriptor management (`stdin`, `stdout`, `stderr` redirection)
- Validates file paths before subprocess execution
- Uses `close_fds=True` to prevent information leakage
- **Input Validation:** Added pre-execution validation for image file paths in both Photoshop and Helicon Focus launchers.

### Fixed
- **Property Name Mismatch:** Corrected `get_stack_summary` to `stackSummary` in UIState to match QML property naming conventions.
- **FilterDialog Theme Support:** Enhanced FilterDialog with proper Material theme support and background styling for consistent dark/light mode appearance.
- **Missing Signal Emissions:** Added `stackSummaryChanged` signal emission when stacks are created, cleared, or processed.

### Changed
- **Improved Error Handling:** Replaced broad `Exception` catches with specific exception types (`OSError`, `subprocess.SubprocessError`, `FileNotFoundError`, `IOError`, `PermissionError`).
- **Better Logging:** Changed `log.error()` to `log.exception()` to include full tracebacks for debugging.
- **Argument Parsing:** Now uses `shlex.split()` with platform-aware parsing (Windows vs POSIX) for proper handling of quoted paths and special characters.

### Testing
- **Executable Validator Tests:** Added comprehensive test suite for executable path validation with 8 test cases covering various security scenarios.

## [0.6.0] - 2025-11-03

### Fixed
Expand Down
Loading