Skip to content
Merged

Test #23

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
6 changes: 5 additions & 1 deletion faststack/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ Todo: Make it work on Linux / Mac. Create Windows .exe. Write better docum
- Added batch delete with confirmation dialog.
- Added the --cachedebug command line argument which gives info on the image cache in the status bar. Doesn't seem to slow down the program at all, just takes up room in the status bar.A
- Added a setting that switches between image display optimized for speed or quality.
- **Auto-Levels:** Automatic image enhancement with configurable threshold and strength
- **Image Metadata:** Extract and display EXIF metadata (I key)
- **Image Processing:** Auto white balance, texture enhancement, and straightening
- **Crop Operations:** Fixed crop functionality with rotation support

## [1.3.0] - 2025-11-23

- Added the ability to crop images, via the cr(O)p hotkey. It can be a freeform crop, or constrained to several popular aspect ratios.
- Sorts images by time.
- Added the Stack Source Raws feature in the Action menu - if you import your images with stackcopy.py --lightroomimport (https://github.com/AlanRockefeller/faststack) and you are viewing a photo stacked in-camera, this feature will open the raw images that made this stack in Helicon Focus.
- Some fixes to the image cache - it doesn't expire when it shouldn't, does expire when it should, and warns you when the cache is full so you can consider increassing the cache size in settings.
- Some fixes to the image cache - it doesn't expire when it shouldn't, does expire when it should, and warns you when the cache is full so you can consider increasing the cache size in settings.


## [1.2.0] - 2025-11-22
Expand Down
2 changes: 2 additions & 0 deletions faststack/faststack.egg-info/PKG-INFO
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ This tool is optimized for speed, using `libjpeg-turbo` for decoding, aggressive
- `E`: Edit in Photoshop
- `Ctrl+C`: Copy image path to clipboard
- `C`: Clear all stacks
+- `H`: Show RGB Histogram
+- `I`: Show EXIF Metadata
Comment on lines +72 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove extraneous + prefix from keyboard shortcut entries.

The lines appear to have +- as a prefix, which looks like diff notation accidentally included in the file content. This will display incorrectly in the rendered documentation.

-+- `H`: Show RGB Histogram
-+- `I`: Show EXIF Metadata
+- `H`: Show RGB Histogram
+- `I`: Show EXIF Metadata
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+- `H`: Show RGB Histogram
+- `I`: Show EXIF Metadata
- `H`: Show RGB Histogram
- `I`: Show EXIF Metadata
🤖 Prompt for AI Agents
In faststack/faststack.egg-info/PKG-INFO around lines 72 to 73, the keyboard
shortcut entries incorrectly include diff-style '+-' prefixes; remove the
leading '+ ' or '+-' characters so the lines read "H: Show RGB Histogram" and
"I: Show EXIF Metadata" (no leading plus or minus), ensuring the PKG-INFO
content contains only the intended text.

262 changes: 146 additions & 116 deletions faststack/faststack/app.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions faststack/faststack/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"optimize_for": "speed", # "speed" or "quality"
"auto_level_threshold": "0.1",
"auto_level_strength": "1.0",
"auto_level_strength_auto": "False",
},
"helicon": {
"exe": "C:\\Program Files\\Helicon Software\\Helicon Focus 8\\HeliconFocus.exe",
Expand All @@ -35,6 +36,7 @@
"mode": "lab", # "lab" or "rgb"
"strength": "0.7",
"warm_bias": "6",
"tint_bias": "0",
"luma_lower_bound": "30",
"luma_upper_bound": "220",
"rgb_lower_bound": "5",
Expand Down
54 changes: 44 additions & 10 deletions faststack/faststack/imaging/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,30 +129,40 @@ def load_image(self, filepath: str, cached_preview: Optional[DecodedImage] = Non
self._preview_image = None
return False

def _apply_edits(self, img: Image.Image) -> Image.Image:
def _apply_edits(self, img: Image.Image, is_export: bool = False) -> Image.Image:
"""Applies all current edits to the provided PIL Image."""
# 1. Rotation
rotation = self.current_edits['rotation']
if rotation == 90:
img = img.transpose(Image.Transpose.ROTATE_90)
img = img.transpose(Image.Transpose.ROTATE_270) # 90 CW = 270 CCW
elif rotation == 180:
img = img.transpose(Image.Transpose.ROTATE_180)
elif rotation == 270:
img = img.transpose(Image.Transpose.ROTATE_270)
img = img.transpose(Image.Transpose.ROTATE_90) # 270 CW = 90 CCW

# 2. Free Rotation (Straighten)
straighten_angle = self.current_edits['straighten_angle']
if abs(straighten_angle) > 0.001:
img = img.rotate(straighten_angle, resample=Image.Resampling.BICUBIC, expand=True)
# PIL rotate is CCW. We want UI CW. Use negative.
# expand=True changes dimensions.
img = img.rotate(-straighten_angle, resample=Image.Resampling.BICUBIC, expand=True)

# 3. Cropping
crop_box = self.current_edits.get('crop_box')
if crop_box:
if crop_box and len(crop_box) == 4:
width, height = img.size
# Normalized 0-1000 to pixels
left = int(crop_box[0] * width / 1000)
top = int(crop_box[1] * height / 1000)
right = int(crop_box[2] * width / 1000)
bottom = int(crop_box[3] * height / 1000)

# Bounds check
left = max(0, min(width - 1, left))
top = max(0, min(height - 1, top))
right = max(left + 1, min(width, right))
bottom = max(top + 1, min(height, bottom))

img = img.crop((left, top, right, bottom))
Comment on lines 150 to 166
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*test*.py" | grep -i "editor\|imaging" | head -20

Repository: AlanRockefeller/faststack

Length of output: 111


🏁 Script executed:

rg -l "crop" --type=py | grep -i test

Repository: AlanRockefeller/faststack

Length of output: 51


🏁 Script executed:

rg -n "def test.*crop" --type=py

Repository: AlanRockefeller/faststack

Length of output: 51


🏁 Script executed:

# Also check the structure of imaging tests
fd -t f "test" faststack/faststack/imaging/ 2>/dev/null || echo "No imaging tests found in that exact location"

Repository: AlanRockefeller/faststack

Length of output: 51


🏁 Script executed:

# Look for pytest test directory
fd -t d "test" faststack/ 2>/dev/null | head -10

Repository: AlanRockefeller/faststack

Length of output: 95


🏁 Script executed:

cat -n faststack/faststack/tests/test_editor.py

Repository: AlanRockefeller/faststack

Length of output: 1003


🏁 Script executed:

# Also check the editor.py file to understand the full cropping implementation
wc -l faststack/faststack/imaging/editor.py

Repository: AlanRockefeller/faststack

Length of output: 110


🏁 Script executed:

# Get the cropping method context
sed -n '140,180p' faststack/faststack/imaging/editor.py

Repository: AlanRockefeller/faststack

Length of output: 1850


🏁 Script executed:

# Double-check if there are any other test files that might test crop functionality
rg -l "ImageEditor\|crop" faststack/faststack/tests/ --type=py

Repository: AlanRockefeller/faststack

Length of output: 51


Add pytest test cases for the cropping logic in ImageEditor.

Per coding guidelines, every new branch in imaging/ modules must have focused pytest test cases. The crop bounds checking and normalization logic (lines 150-166) introduces multiple branches—including crop_box validation, normalization from 0-1000 range to pixels, and bounds clamping with max/min operations—that are not currently tested. Add tests covering edge cases such as invalid crop_box values, extreme normalized values, and boundary conditions where left/top/right/bottom need adjustment.

🤖 Prompt for AI Agents
In faststack/faststack/imaging/editor.py around lines 150 to 166, the cropping
logic (validation of crop_box length, normalization from 0-1000 to pixel coords,
and bounds clamping) lacks pytest coverage; add focused tests that call the
ImageEditor crop path and assert expected output images or box coordinates for:
invalid crop_box values (None, wrong length, non-numeric) to ensure no crop is
applied or proper error handling, extreme normalized values (0, 1000, values
outside range like -100, 1100) to verify normalization maps to image edges, and
boundary conditions where left/top/right/bottom would be adjusted (e.g.,
normalized coords that would produce zero-width/height) to confirm clamping
enforces minimum size and stays within image dimensions.


# 3. Exposure (gamma-based)
Expand Down Expand Up @@ -350,7 +360,7 @@ def get_preview_data(self) -> Optional[DecodedImage]:

# Always start from a fresh copy of the small preview image
img = self._preview_image.copy()
img = self._apply_edits(img)
img = self._apply_edits(img, is_export=False)

# The image is in RGB mode after _apply_edits
buffer = img.tobytes()
Expand Down Expand Up @@ -383,7 +393,7 @@ def save_image(self) -> Optional[Tuple[Path, Path]]:
return None

final_img = self.original_image.copy()
final_img = self._apply_edits(final_img)
final_img = self._apply_edits(final_img, is_export=True)

original_path = self.current_filepath
try:
Expand All @@ -402,14 +412,38 @@ def save_image(self) -> Optional[Tuple[Path, Path]]:
# Re-open original to correctly detect format and get EXIF
with Image.open(original_path) as original_img:
original_format = original_img.format or original_path.suffix.lstrip('.').upper()
exif_data = original_img.info.get('exif')

# Handle EXIF
exif_bytes = original_img.info.get('exif')

# Try to reset orientation to Normal (1) if EXIF exists
if exif_bytes:
try:
# Load exif data as an object
exif = original_img.getexif()
# Tag 274 is Orientation. Set to 1 (Normal)
if 274 in exif:
exif[274] = 1
# Serialize back to bytes - Pillow >= 8.2.0 required for tobytes()
# If tobytes() is missing, we might skip writing modified EXIF or write original
if hasattr(exif, 'tobytes'):
exif_bytes = exif.tobytes()
else:
# Fallback for older Pillow: skip writing EXIF if we can't sanitize it
# to avoid double-rotation bug.
print("Warning: Pillow too old to sanitize EXIF bytes. Skipping EXIF write to prevent double-rotation.")
exif_bytes = None
except Exception as e:
print(f"Warning: Failed to sanitize EXIF orientation: {e}")
# Fallback: safer to skip EXIF than write bad orientation
exif_bytes = None

save_kwargs = {}
if original_format == 'JPEG':
save_kwargs['format'] = 'JPEG'
save_kwargs['quality'] = 95
if exif_data:
save_kwargs['exif'] = exif_data
if exif_bytes:
save_kwargs['exif'] = exif_bytes
else:
save_kwargs['format'] = original_format

Expand Down
Loading