Skip to content

New features and bug fixes#21

Merged
AlanRockefeller merged 9 commits intomainfrom
test
Dec 8, 2025
Merged

New features and bug fixes#21
AlanRockefeller merged 9 commits intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Dec 8, 2025

Fixed crop, added crop rotation, added auto levels

Summary by CodeRabbit

Release Notes

  • New Features

    • RGB histogram accessible via H key with real-time zoom/pan updates
    • Batch delete with confirmation dialog; maintains current image position
    • EXIF metadata viewer (toggle with I key)
    • Auto levels adjustment with configurable strength
    • Texture enhancement slider for image detail control
    • Image straightening/rotation capabilities
    • Speed vs. quality optimization setting
    • Enhanced crop rectangle with rotation support and aspect ratio constraints
    • Double-click slider reset and manual numeric input in editor
  • Improvements

    • Faster image caching mechanism
    • File timestamp preservation when saving edits
    • Updated keyboard shortcuts (E closes editor, S saves and closes)
    • Renamed editor buttons for clarity
    • Enhanced white balance with multiplicative color gains
    • Improved highlights recovery
  • Documentation

    • Updated to Version 1.4 (December 1, 2025)
    • New developer documentation added

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 8, 2025

Walkthrough

Version 1.4 release adds TurboJPEG-based image caching acceleration, RGB histogram visualization (H key), batch image deletion with confirmation, auto-levels functionality with configurable threshold and strength, texture enhancement, improved white balance using multiplicative gains, straightening angle support, and EXIF metadata extraction and display (I key). New configuration options control speed-versus-quality optimization and auto-levels behavior.

Changes

Cohort / File(s) Summary
Documentation & Metadata
ChangeLog.md, README.md, faststack.egg-info/PKG-INFO, AGENTS.md
Version 1.4 release notes, updated feature descriptions, new development guide, and reformatted package metadata.
Configuration & Project
pyproject.toml, faststack/faststack/config.py, requirements.txt, faststack.json, verify_wb.py, debug_output.txt, next.prompt
Version bump to 1.4, new config keys for optimize_for/auto_level_threshold/auto_level_strength, email formatting, test utilities, and design specification.
Imaging Cache & Decoding
faststack/faststack/imaging/cache.py, faststack/faststack/imaging/jpeg.py, faststack/faststack/imaging/prefetch.py
New cache key builder and size calculator; TurboJPEG integration with fast_dct flag and scaling helpers for JPEG decode; ICC color management caching and saturation compensation.
Imaging Metadata & Editing
faststack/faststack/imaging/metadata.py, faststack/faststack/imaging/editor.py
New EXIF extraction and formatting utilities; auto_levels method, rotation helpers, texture enhancement, improved white balance (multiplicative), straightening angle, and file timestamp preservation.
IO Operations
faststack/faststack/io/executable_validator.py, faststack/faststack/io/helicon.py, faststack/faststack/io/indexer.py, faststack/faststack/io/sidecar.py, faststack/faststack/io/watcher.py
Whitespace and formatting normalization; behavior unchanged.
Logging & Models
faststack/faststack/logging_setup.py, faststack/faststack/models.py
Formatting updates; no functional changes.
UI Backend
faststack/faststack/ui/keystrokes.py, faststack/faststack/ui/provider.py
New keyboard shortcuts (I for EXIF, L for auto_levels); significantly expanded UIState with editor state, histogram, cache debug info, and new signals/properties.
QML Components
faststack/faststack/qml/Components.qml, faststack/faststack/qml/DeleteBatchDialog.qml, faststack/faststack/qml/ExifDialog.qml, faststack/faststack/qml/FilterDialog.qml, faststack/faststack/qml/HistogramWindow.qml, faststack/faststack/qml/ImageEditorDialog.qml, faststack/faststack/qml/JumpToImageDialog.qml, faststack/faststack/qml/Main.qml, faststack/faststack/qml/SettingsDialog.qml
Enhanced zoom/pan with histogram integration; new batch-delete and EXIF-display dialogs; expanded editor with texture slider, brightness, auto-levels button, and reversed whites control; updated histogram window with H-key focus; settings dialog with optimize-for and auto-level controls; main QML with batch-dialog support and EXIF integration.
Tests
faststack/faststack/tests/benchmark_decode.py, faststack/faststack/tests/benchmark_decode_bilinear.py, faststack/faststack/tests/check_turbo.py, faststack/faststack/tests/debug_metadata.py, faststack/faststack/tests/test_cache.py, faststack/faststack/tests/test_editor.py, faststack/faststack/tests/test_executable_validator.py, faststack/faststack/tests/test_metadata.py, faststack/faststack/tests/test_new_features.py, faststack/faststack/tests/test_pairing.py, faststack/faststack/tests/test_prefetch_logic.py, faststack/faststack/tests/test_sidecar.py
New benchmark scripts for JPEG decode with/without TurboJPEG; TurboJPEG availability probe; EXIF metadata debug harness; new tests for editor timestamp preservation, metadata extraction, auto-levels/highlights/straightening, and prefetcher priority-based task cancellation; formatting updates to existing tests.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as QML UI
    participant Controller as Python Controller
    participant ImageEditor as ImageEditor
    participant Cache as Image Cache
    participant Decoder as JPEG Decoder

    rect rgb(200, 220, 255)
        Note over User,Decoder: Auto-Levels Flow
        User->>UI: Press L (auto_levels) or click button
        UI->>Controller: quick_auto_levels()
        Controller->>ImageEditor: auto_levels(threshold=0.1)
        ImageEditor->>ImageEditor: compute blacks/whites from histogram
        ImageEditor->>Controller: return (blacks, whites)
        Controller->>UI: emit autoLevelClippingThresholdChanged
        UI->>UI: update histogram display with new values
    end

    rect rgb(220, 200, 255)
        Note over User,Decoder: EXIF Display Flow (I Key)
        User->>UI: Press I (show EXIF)
        UI->>Controller: show_exif_dialog() or toggle
        Controller->>ImageEditor: get_exif_data(image_path)
        ImageEditor->>Controller: return {summary, full}
        Controller->>UI: emit populateExifDialog(data)
        UI->>UI: openExifDialog with summary/full data
        User->>UI: Click "Show All" button
        UI->>UI: toggle showFull, refresh display text
    end

    rect rgb(255, 220, 200)
        Note over User,Decoder: Batch Delete Confirmation Flow
        User->>UI: select multiple images, press delete
        UI->>Controller: on delete action triggered
        Controller->>UI: show_delete_batch_dialog(batch_count)
        UI->>UI: open DeleteBatchDialog(count=N)
        User->>UI: click "Delete All" button
        UI->>Controller: delete_batch_images()
        Controller->>ImageEditor: apply to each selected image
        Controller->>UI: update status/refresh UI
    end

    rect rgb(200, 255, 220)
        Note over User,Decoder: JPEG Decode with TurboJPEG Path
        Controller->>Cache: request decoded image for path
        Cache->>Decoder: decode_jpeg_resized(jpeg_bytes, width, height, fast_dct=True)
        alt TurboJPEG Available
            Decoder->>Decoder: compute scaling_factor from dimensions
            Decoder->>Decoder: decode via TurboJPEG with fast_dct flag
            Decoder->>Cache: return decoded RGB array (fast path)
        else TurboJPEG Unavailable or Fails
            Decoder->>Decoder: fallback to Pillow resize (BILINEAR)
            Decoder->>Cache: return decoded RGB array (fallback path)
        end
        Cache->>Cache: cache result with build_cache_key(path, generation)
        Cache->>Controller: return image
        Controller->>UI: update displayed image
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • imaging/jpeg.py – TurboJPEG integration logic with multiple fallback paths; verify scaling factor computation and edge cases (very small/large images, dimension constraints).
  • imaging/prefetch.py – Return type change in _decode_and_cache (now returns Optional[tuple[Path, int]] instead of Optional[tuple[int, int]]); verify callers correctly handle Path-based cache keys and ICC transform caching semantics.
  • imaging/editor.py – Dense changes including multiplicative white balance gains, texture enhancement with Gaussian/midtone masking, auto_levels histogram computation, straightening angle before crop, and file timestamp restoration; verify image processing logic and test coverage.
  • ui/provider.py – Significant UIState expansion with many new signals/properties; verify signal-slot connections in QML, property binding correctness, and camelCase vs snake_case consistency.
  • qml/Components.qml – Complex zoom/pan implementation with cursor-relative scaling, crop rotation handling, and aspect ratio constraints; verify coordinate mapping between screen and image space, and crop overlay synchronization.
  • qml/Main.qml, qml/ImageEditorDialog.qml, qml/SettingsDialog.qml – New dialogs and controls wired to backend; verify signal/property bindings match controller expectations and dialog data passing is correct.
  • Tests – New test modules mock complex dependencies (PIL, TurboJPEG); verify mocks accurately simulate real behavior and test assertions cover edge cases.

Possibly related PRs

  • PR #12 – Overlapping JPEG decode signature and prefetcher cache/decode return type changes; verify no conflicting modifications to the same functions.
  • PR #14 – Related cache utility functions and TurboJPEG scaling/ICC caching overlap; coordinate on cache key structure and ICC transform caching strategy.
  • PR #20 – Overlapping Components.qml crop/overlay logic, HistogramWindow.qml, and histogram/crop UIState properties; verify UI state signal/property consistency.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.05% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'New features and bug fixes' is vague and generic, using non-descriptive terms that don't convey specific information about the changeset despite substantial changes including crop rotation, auto levels, cache optimization, EXIF support, and more. Revise the title to be more specific, e.g., 'Add crop rotation, auto levels, and EXIF support' or highlight the primary functional change most important to the codebase.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

🧹 Nitpick comments (34)
faststack/faststack/models.py (1)

41-41: Optional: Consider a more specific type hint for the format field (if applicable).

The format field is typed as object with a comment noting it should be QImage.Format. If PyQt imports are available, consider a more specific type hint (e.g., Optional[QImage.Format]) to improve IDE support and type checking. If this is intentionally loose to avoid hard dependencies, the current approach is fine.

faststack/faststack/io/helicon.py (2)

63-72: Safe argument parsing with shlex; minor logging redundancy.

The use of shlex.split with OS-aware posix parameter is a solid security practice for handling user-provided arguments. However, on Line 71, logging.exception() automatically includes the exception traceback, making the {e} parameter redundant.

- log.exception(f"Invalid helicon args format: {e}")
+ log.exception("Invalid helicon args format")

Alternatively, if you need the exception message for clarity: use log.error with explicit exception info instead.


87-92: Exception handling covers appropriate cases; clean up redundant logging parameters.

The exception handling properly catches and logs errors, returning (False, None) in all failure paths. However, Lines 88 and 91 include redundant exception parameters in logging.exception() calls—the framework automatically includes exception info.

- log.exception(f"Failed to launch Helicon Focus: {e}")
+ log.exception("Failed to launch Helicon Focus")
- log.exception(f"Failed to create temporary file for Helicon Focus: {e}")
+ log.exception("Failed to create temporary file for Helicon Focus")
faststack/faststack/tests/test_sidecar.py (1)

14-14: Use explicit Optional or | None syntax for type hints.

Line 14 has an implicit Optional (parameter with default None but no type annotation for the None case). Per PEP 484, convert the type hint to explicitly indicate the optional nature:

- def _create(content: dict = None):
+ def _create(content: dict | None = None):

Verify that the project targets Python 3.10+ for the | union syntax. If targeting earlier versions, use Optional[dict] instead.

faststack/faststack/tests/test_executable_validator.py (1)

1-131: Formatting changes only—no functional review needed.

This file contains only whitespace/formatting adjustments with no logic changes.

Static analysis identified some minor cleanup opportunities (pre-existing, not introduced by this PR):

  • Lines 40, 62, 123: Lambda functions don't use self parameter. Consider removing or prefixing with _.
  • Lines 66, 127: Unpacked error variable is unused. Consider using _ instead: is_valid, _ = validate_executable_path(...)

These are low-priority style improvements.

faststack/faststack/io/indexer.py (1)

1-84: Formatting changes only—no functional review needed.

This file contains only whitespace/formatting adjustments with no logic changes.

Static analysis identified minor cleanup opportunities (pre-existing, not introduced by this PR):

  • Line 39: Exception variable e is captured but never used. Since log.exception() automatically includes exception details, you can use bare except OSError: instead.
  • Line 65: Function parameter jpg_path is declared but never used in _find_raw_pair(). Consider removing it if truly unnecessary.

These are low-priority style improvements.

faststack/faststack/AGENTS.md (1)

3-19: Repository guidelines doc looks solid

The structure, workflow, and security sections are coherent and match the layout implied by the code. As a small optional tweak, you could later add a note that once installed via the package metadata, users can also launch via the configured faststack console script, not just python -m faststack.app.

faststack/faststack/tests/check_turbo.py (1)

2-11: Guard diagnostics to avoid side effects on import

Right now, importing this module will immediately run the diagnostic and print to stdout. If this is meant as a standalone check script, consider wrapping the logic in a main() function and guarding it:

-try:
-    import turbojpeg
-    print("turbojpeg module found")
-    print(f"Dir: {dir(turbojpeg)}")
-    if hasattr(turbojpeg, 'TJFLAG_FASTDCT'):
-        print(f"TJFLAG_FASTDCT: {turbojpeg.TJFLAG_FASTDCT}")
-    else:
-        print("TJFLAG_FASTDCT not found in module")
-except ImportError:
-    print("turbojpeg module not found")
+def main() -> None:
+    try:
+        import turbojpeg
+        print("turbojpeg module found")
+        print(f"Dir: {dir(turbojpeg)}")
+        if hasattr(turbojpeg, "TJFLAG_FASTDCT"):
+            print(f"TJFLAG_FASTDCT: {turbojpeg.TJFLAG_FASTDCT}")
+        else:
+            print("TJFLAG_FASTDCT not found in module")
+    except ImportError:
+        print("turbojpeg module not found")
+
+
+if __name__ == "__main__":
+    main()
faststack/faststack/io/sidecar.py (2)

28-60: Log full traceback when sidecar load fails

Since you already treat parse failures as “start fresh”, capturing the traceback will help diagnose corrupted/old sidecar issues without changing behavior. You can switch to logging.exception:

-        except (json.JSONDecodeError, TypeError) as e:
-            log.error(f"Failed to load or parse sidecar file {self.path}: {e}")
+        except (json.JSONDecodeError, TypeError):
+            log.exception("Failed to load or parse sidecar file %s", self.path)

61-90: Likewise, use logging.exception on save errors

For symmetry with load and better observability, consider logging the full traceback when persisting fails:

-        except (IOError, TypeError) as e:
-            log.error(f"Failed to save sidecar file {self.path}: {e}")
+        except (IOError, TypeError):
+            log.exception("Failed to save sidecar file %s", self.path)
faststack/faststack/tests/test_pairing.py (1)

55-71: Split multiple statements per line for clarity

Lines 55, 58, 63, 68, and 69 chain assignments with ;, which is harder to read and is typically flagged by linters. Consider one statement per line:

-    jpg_stat = MagicMock(); jpg_stat.st_mtime = 1000.0
+    jpg_stat = MagicMock()
+    jpg_stat.st_mtime = 1000.0
@@
-    raw1_path = Path("IMG_01.CR3"); raw1_stat = MagicMock(); raw1_stat.st_mtime = 1000.1
+    raw1_path = Path("IMG_01.CR3")
+    raw1_stat = MagicMock()
+    raw1_stat.st_mtime = 1000.1
@@
-    raw2_path = Path("IMG_01.CR3"); raw2_stat = MagicMock(); raw2_stat.st_mtime = 1003.0
+    raw2_path = Path("IMG_01.CR3")
+    raw2_stat = MagicMock()
+    raw2_stat.st_mtime = 1003.0
@@
-    raw3_path = Path("IMG_01_A.CR3"); raw3_stat = MagicMock(); raw3_stat.st_mtime = 1000.5
+    raw3_path = Path("IMG_01_A.CR3")
+    raw3_stat = MagicMock()
+    raw3_stat.st_mtime = 1000.5
@@
-    raw4_path = Path("IMG_01_B.CR3"); raw4_stat = MagicMock(); raw4_stat.st_mtime = 1001.8
+    raw4_path = Path("IMG_01_B.CR3")
+    raw4_stat = MagicMock()
+    raw4_stat.st_mtime = 1001.8
faststack/README.md (2)

3-3: Optional: Consider adding comma after year for clarity.

Some style guides recommend a comma after the year in month-day-year format: "December 1, 2025," for improved readability.


16-16: Optional: Consider hyphenating compound adjective.

"high-performance decoding" (with hyphen) follows conventional style for compound adjectives modifying a noun.

faststack/faststack/debug_output.txt (1)

1-13: Consider excluding debug output from version control.

This file appears to be a generated test artifact. Debug output files like this should typically be excluded from version control via .gitignore to prevent test artifacts from polluting the repository.

faststack/faststack/config.py (1)

82-82: Use logging.exception for better error diagnostics.

Replace log.error with log.exception to automatically include the traceback, which aids debugging when configuration saves fail.

-            log.error(f"Failed to save config to {self.config_path}: {e}")
+            log.exception(f"Failed to save config to {self.config_path}: {e}")
faststack/faststack/tests/debug_metadata.py (1)

54-54: Optional: Remove unused exception variable.

The exception variable e is captured but never used. You can simplify to except Exception: since the traceback is printed via traceback.print_exc.

-        except Exception as e:
+        except Exception:
             f.write("Test FAILED\n")
             import traceback
             traceback.print_exc(file=f)
faststack/faststack/verify_wb.py (1)

7-65: Consider converting to a proper unittest and improving test assertions.

This test file uses print() statements for pass/fail reporting instead of proper test assertions, which means failures won't be caught by test runners. Additionally, the file is located in faststack/faststack/ rather than faststack/faststack/tests/ where other tests reside.

Consider refactoring to use unittest like other test files in this PR:

+import unittest
 import numpy as np
 from PIL import Image
 from faststack.imaging.editor import ImageEditor
 import os

-def test_white_balance():
+class TestWhiteBalance(unittest.TestCase):
+    def setUp(self):
+        self.editor = ImageEditor()
+        self.black_path = "test_black.jpg"
+        self.grey_path = "test_grey.jpg"
+
+    def tearDown(self):
+        for path in [self.black_path, self.grey_path]:
+            if os.path.exists(path):
+                os.remove(path)
+
+    def test_black_preservation(self):
+        black_img = Image.new('RGB', (100, 100), (0, 0, 0))
+        black_img.save(self.black_path)
+        self.editor.load_image(self.black_path)
+        self.editor.set_edit_param('white_balance_by', 1.0)
+        self.editor.set_edit_param('white_balance_mg', 1.0)
+        processed_img = self.editor._apply_edits(self.editor.original_image.copy())
+        arr = np.array(processed_img)
+        self.assertEqual(arr.max(), 0, "Black level not preserved")
faststack/faststack/tests/test_prefetch_logic.py (2)

9-64: Remove redundant try/except wrapper; unittest handles exceptions.

The try/except block that prints traceback and re-raises is unnecessary since unittest already captures exceptions and displays full tracebacks on test failures. The print statements also clutter test output.

     def test_submit_task_priority_cancellation(self):
-        try:
-            # Mock dependencies
-            mock_cache_put = MagicMock()
-            ...
-            print("Test passed!")
-        except Exception:
-            import traceback
-            traceback.print_exc()
-            raise
+        # Mock dependencies
+        mock_cache_put = MagicMock()
+        mock_get_display_info = MagicMock(return_value=(100, 100, 1))
+        
+        # ... rest of test without try/except wrapper ...
+        
+        # Assertions (no print statements needed)
+        self.assertIn(4, prefetcher.futures, "Task 4 was not added to futures")
+        f0.cancel.assert_called()
+        f5.cancel.assert_not_called()

46-48: Use self.assertIn instead of raising a generic Exception.

Replace the manual check with a proper unittest assertion for better error messages and consistency.

-            # Check if task 4 was added
-            if 4 not in prefetcher.futures:
-                raise Exception("Task 4 was not added to futures!")
+            # Check if task 4 was added
+            self.assertIn(4, prefetcher.futures, "Task 4 was not added to futures")
faststack/faststack/tests/test_metadata.py (1)

66-69: Use bare raise to preserve the original traceback.

Using raise e instead of raise resets the traceback to this line, losing the original context. As flagged by Ruff TRY201.

         except Exception as e:
             import traceback
             traceback.print_exc()
-            raise e
+            raise

Or better yet, remove the try/except entirely since unittest captures exceptions and displays tracebacks automatically (similar to the pattern in test_prefetch_logic.py).

faststack/faststack/tests/benchmark_decode_bilinear.py (2)

1-6: Remove leading blank line.

Line 1 has an unnecessary blank line at the start of the file. This is a minor style issue.

-
 import time

8-51: Consider extracting resampling mode as a parameter to avoid code duplication.

This function largely duplicates decode_jpeg_resized from jpeg.py, differing only in the resampling method (BILINEAR vs LANCZOS). Consider adding a resampling parameter to the existing function in jpeg.py rather than maintaining two nearly identical implementations.

Additionally, the exception handling uses print() while the main jpeg.py uses log.exception(). For consistency, consider using logging here as well, or since this is a benchmark script, the print statements may be acceptable for quick debugging.

faststack/faststack/tests/test_new_features.py (2)

1-1: Remove unused import.

The sys module is imported but never used in this file.

-import sys
 import unittest

10-11: Redundant type conversion.

The astype(np.uint8) call at the end is redundant since np.arange(256, dtype=np.uint8) already produces a uint8 array, and np.tile preserves the dtype.

-        self.img = Image.fromarray(np.tile(np.arange(256, dtype=np.uint8), (10, 1)).astype(np.uint8))
+        self.img = Image.fromarray(np.tile(np.arange(256, dtype=np.uint8), (10, 1)))
faststack/faststack/qml/SettingsDialog.qml (2)

1-3: Inconsistent import versioning.

Line 1 uses an unversioned import (import QtQuick) while lines 2-3 use versioned imports. For consistency, consider using the same style throughout.

-import QtQuick
+import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.15

27-35: Inconsistent initialization pattern for auto level settings.

Line 33 initializes autoLevelThresholdField from settingsDialog.autoLevelClippingThreshold, but line 34 assigns autoLevelStrength directly from uiState.autoLevelStrength. For consistency, consider initializing both from the same source (either the dialog property or uiState).

         autoLevelThresholdField.text = settingsDialog.autoLevelClippingThreshold.toFixed(4)
-        settingsDialog.autoLevelStrength = uiState.autoLevelStrength
+        autoLevelStrengthSlider.value = settingsDialog.autoLevelStrength
faststack/faststack/imaging/editor.py (1)

143-157: Update step numbering comments.

The cropping step is labeled "3. Cropping" on line 148, but it should now be step 3 after rotation (step 1) and straighten (step 2). Also, line 158 labels exposure as "3. Exposure" but it should be step 4. Consider updating all step comments for accuracy.

-        # 3. Cropping
+        # 3. Cropping (after rotation and straighten)
         crop_box = self.current_edits.get('crop_box')
faststack/faststack/imaging/jpeg.py (2)

42-43: Redundant exception in log.exception calls.

log.exception() automatically includes exception info from the current context. The explicit {e} in the f-string is redundant and adds noise to the log message.

Apply this pattern throughout the file:

-            log.exception(f"PyTurboJPEG failed to decode image: {e}. Trying Pillow.")
+            log.exception("PyTurboJPEG failed to decode image. Trying Pillow.")

37-40: Use the exported TJFLAG_FASTDCT constant from turbojpeg instead of the hardcoded magic number.

The turbojpeg library exports TJFLAG_FASTDCT for exactly this purpose. Import it alongside TJPF_RGB and replace the hardcoded 2048 value to improve maintainability.

-    from turbojpeg import TurboJPEG, TJPF_RGB
+    from turbojpeg import TurboJPEG, TJPF_RGB, TJFLAG_FASTDCT

Then use flags |= TJFLAG_FASTDCT instead of flags |= 2048.

faststack/faststack/imaging/prefetch.py (2)

22-22: Move import to top of file.

The import threading on line 22 should be at the top of the file with other imports for consistency and PEP 8 compliance.

 import logging
 import os
 import io
 import hashlib
+import threading
 from pathlib import Path
 from concurrent.futures import ThreadPoolExecutor, Future
 from typing import List, Dict, Optional, Callable
 import mmap

Then remove line 22.


295-518: Consider extracting duplicated decode logic.

The decode-and-resize logic is repeated in multiple places:

  • Lines 320-332 (ICC path)
  • Lines 391-402 (ICC fallback)
  • Lines 421-433 (ICC no profile fallback)
  • Lines 451-463 (standard path)

A helper function would reduce duplication and maintenance burden.

Example extraction:

def _decode_image_from_file(self, image_path: Path, display_width: int, display_height: int, 
                            use_resized: bool, fast_dct: bool) -> Optional[np.ndarray]:
    """Decode image using mmap with optional resize."""
    with open(image_path, "rb") as f:
        with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mmapped:
            if use_resized:
                return decode_jpeg_resized(mmapped, display_width, display_height, fast_dct=fast_dct)
            else:
                buffer = decode_jpeg_rgb(mmapped, fast_dct=fast_dct)
                if buffer is not None:
                    img = PILImage.fromarray(buffer)
                    img.thumbnail((display_width, display_height), PILImage.Resampling.LANCZOS)
                    return np.array(img)
                return None
faststack/faststack/qml/Components.qml (2)

198-204: Redundant variable re-declaration.

cropCenterX and cropCenterY are already declared at lines 182-183 and can be reused here, avoiding redundant declarations.

                 else if (mainMouseArea.isRotating) {
                     cropDragMode = "rotate"
-                    var cropCenterX = cropRect.x + cropRect.width / 2
-                    var cropCenterY = cropRect.y + cropRect.height / 2
                     cropStartAngle = Math.atan2(mouse.y - cropCenterY, mouse.x - cropCenterX) * 180 / Math.PI
                     cropStartRotation = cropRotation
                 }

228-228: Variable box is already declared at line 174.

Re-declaring box shadows the earlier declaration. Since they reference the same data, this works but is confusing.

Remove the var keyword to reuse the existing variable:

-                    var box = uiState.currentCropBox
+                    box = uiState.currentCropBox

Or rename for clarity if distinct semantics are intended.

faststack/faststack/ui/provider.py (1)

63-65: Use log.exception for error with traceback.

When catching an exception, log.exception provides the full traceback which is more useful for debugging than log.error.

         except (ValueError, IndexError) as e:
-            log.error(f"Invalid image ID requested from QML: {id}. Error: {e}")
+            log.exception("Invalid image ID requested from QML: %s", id)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32c6216 and 81c9ba6.

📒 Files selected for processing (46)
  • faststack/ChangeLog.md (1 hunks)
  • faststack/README.md (1 hunks)
  • faststack/faststack.egg-info/PKG-INFO (1 hunks)
  • faststack/faststack/AGENTS.md (1 hunks)
  • faststack/faststack/config.py (1 hunks)
  • faststack/faststack/debug_output.txt (1 hunks)
  • faststack/faststack/faststack.json (1 hunks)
  • faststack/faststack/imaging/cache.py (1 hunks)
  • faststack/faststack/imaging/editor.py (7 hunks)
  • faststack/faststack/imaging/jpeg.py (1 hunks)
  • faststack/faststack/imaging/metadata.py (1 hunks)
  • faststack/faststack/imaging/prefetch.py (1 hunks)
  • faststack/faststack/io/executable_validator.py (1 hunks)
  • faststack/faststack/io/helicon.py (1 hunks)
  • faststack/faststack/io/indexer.py (1 hunks)
  • faststack/faststack/io/sidecar.py (1 hunks)
  • faststack/faststack/io/watcher.py (1 hunks)
  • faststack/faststack/logging_setup.py (1 hunks)
  • faststack/faststack/models.py (1 hunks)
  • faststack/faststack/next.prompt (1 hunks)
  • faststack/faststack/qml/Components.qml (1 hunks)
  • faststack/faststack/qml/DeleteBatchDialog.qml (1 hunks)
  • faststack/faststack/qml/ExifDialog.qml (1 hunks)
  • faststack/faststack/qml/FilterDialog.qml (1 hunks)
  • faststack/faststack/qml/HistogramWindow.qml (6 hunks)
  • faststack/faststack/qml/ImageEditorDialog.qml (8 hunks)
  • faststack/faststack/qml/JumpToImageDialog.qml (1 hunks)
  • faststack/faststack/qml/Main.qml (1 hunks)
  • faststack/faststack/qml/SettingsDialog.qml (1 hunks)
  • faststack/faststack/tests/benchmark_decode.py (1 hunks)
  • faststack/faststack/tests/benchmark_decode_bilinear.py (1 hunks)
  • faststack/faststack/tests/check_turbo.py (1 hunks)
  • faststack/faststack/tests/debug_metadata.py (1 hunks)
  • faststack/faststack/tests/test_cache.py (1 hunks)
  • faststack/faststack/tests/test_editor.py (1 hunks)
  • faststack/faststack/tests/test_executable_validator.py (1 hunks)
  • faststack/faststack/tests/test_metadata.py (1 hunks)
  • faststack/faststack/tests/test_new_features.py (1 hunks)
  • faststack/faststack/tests/test_pairing.py (1 hunks)
  • faststack/faststack/tests/test_prefetch_logic.py (1 hunks)
  • faststack/faststack/tests/test_sidecar.py (1 hunks)
  • faststack/faststack/ui/keystrokes.py (1 hunks)
  • faststack/faststack/ui/provider.py (1 hunks)
  • faststack/faststack/verify_wb.py (1 hunks)
  • faststack/pyproject.toml (1 hunks)
  • faststack/requirements.txt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (16)
faststack/faststack/tests/debug_metadata.py (1)
faststack/faststack/imaging/metadata.py (1)
  • get_exif_data (39-195)
faststack/faststack/imaging/editor.py (2)
faststack/faststack/ui/provider.py (6)
  • highlights (755-756)
  • highlights (759-762)
  • blacks (795-796)
  • blacks (799-802)
  • whites (805-806)
  • whites (809-812)
faststack/faststack/app.py (3)
  • auto_levels (2673-2713)
  • rotate_image_cw (2233-2237)
  • rotate_image_ccw (2240-2246)
faststack/faststack/io/indexer.py (2)
faststack/faststack/models.py (1)
  • ImageFile (8-12)
faststack/faststack/config.py (1)
  • get (84-85)
faststack/faststack/tests/benchmark_decode.py (1)
faststack/faststack/imaging/jpeg.py (1)
  • decode_jpeg_resized (114-177)
faststack/faststack/io/sidecar.py (2)
faststack/faststack/models.py (2)
  • Sidecar (27-32)
  • EntryMetadata (15-23)
faststack/faststack/app.py (1)
  • load (308-321)
faststack/faststack/tests/test_editor.py (1)
faststack/faststack/imaging/editor.py (1)
  • save_image (376-458)
faststack/faststack/tests/test_pairing.py (1)
faststack/faststack/io/indexer.py (2)
  • find_images (20-62)
  • _find_raw_pair (64-84)
faststack/faststack/tests/test_cache.py (1)
faststack/faststack/imaging/cache.py (1)
  • ByteLRUCache (12-49)
faststack/faststack/tests/test_new_features.py (1)
faststack/faststack/imaging/editor.py (2)
  • ImageEditor (56-477)
  • _apply_edits (132-300)
faststack/faststack/tests/test_sidecar.py (2)
faststack/faststack/io/sidecar.py (4)
  • SidecarManager (13-97)
  • set_last_index (96-97)
  • get_metadata (92-94)
  • save (61-90)
faststack/faststack/models.py (1)
  • EntryMetadata (15-23)
faststack/faststack/tests/test_metadata.py (1)
faststack/faststack/imaging/metadata.py (2)
  • get_exif_data (39-195)
  • clean_exif_value (9-37)
faststack/faststack/io/helicon.py (2)
faststack/faststack/io/executable_validator.py (1)
  • validate_executable_path (23-95)
faststack/faststack/config.py (1)
  • get (84-85)
faststack/faststack/config.py (1)
faststack/faststack/logging_setup.py (1)
  • get_app_data_dir (8-13)
faststack/faststack/imaging/prefetch.py (5)
faststack/faststack/models.py (2)
  • ImageFile (8-12)
  • DecodedImage (35-44)
faststack/faststack/imaging/jpeg.py (2)
  • decode_jpeg_rgb (31-53)
  • decode_jpeg_resized (114-177)
faststack/faststack/imaging/cache.py (1)
  • build_cache_key (72-78)
faststack/faststack/config.py (1)
  • get (84-85)
faststack/faststack/app.py (1)
  • get_display_info (210-213)
faststack/faststack/tests/benchmark_decode_bilinear.py (2)
faststack/faststack/imaging/jpeg.py (2)
  • decode_jpeg_rgb (31-53)
  • _get_turbojpeg_scaling_factor (94-111)
faststack/faststack/tests/benchmark_decode.py (2)
  • create_test_jpeg (8-16)
  • benchmark (18-37)
faststack/faststack/ui/provider.py (3)
faststack/faststack/models.py (1)
  • DecodedImage (35-44)
faststack/faststack/app.py (1)
  • get_decoded_image (352-450)
faststack/faststack/config.py (1)
  • get (84-85)
🪛 LanguageTool
faststack/ChangeLog.md

[grammar] ~18-~18: Ensure spelling is correct
Context: ...n the cache is full so you can consider increassing the cache size in settings.

[1....

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[style] ~24-~24: The wording of this phrase can be improved.
Context: ... cool.

  • Updated auto white balance to make it better, and put some controls for it in the se...

(MAKE_STYLE_BETTER)


[style] ~149-~149: Consider using a different verb for a more formal wording.
Context: ...e JPEG scaling factor calculation.

  • Fixed an issue where panning the image was no...

(FIX_RESOLVE)


[grammar] ~164-~164: Use a hyphen to join words.
Context: ... 0.4

Todo

Make it use the full res image when zooming in
When multiple...

(QB_NEW_EN_HYPHEN)

faststack/README.md

[style] ~3-~3: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ...astStack

Version 1.4 - December 1, 2025

By Alan Rockefeller

Ultra-fast, c...

(MISSING_COMMA_AFTER_YEAR)


[grammar] ~16-~16: Use a hyphen to join words.
Context: ...10ms next/previous image switching, high performance decoding via PyTurboJPEG.
...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.18.1)
faststack/ChangeLog.md

17-17: Bare URL used

(MD034, no-bare-urls)

🪛 Ruff (0.14.7)
faststack/faststack/imaging/metadata.py

56-56: Do not catch blind exception: Exception

(BLE001)


81-81: Multiple statements on one line (colon)

(E701)


82-82: Multiple statements on one line (colon)

(E701)


114-114: Do not catch blind exception: Exception

(BLE001)


130-130: Do not catch blind exception: Exception

(BLE001)


142-142: Do not catch blind exception: Exception

(BLE001)


180-180: Do not catch blind exception: Exception

(BLE001)

faststack/faststack/tests/debug_metadata.py

54-54: Do not catch blind exception: Exception

(BLE001)


54-54: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

faststack/faststack/imaging/editor.py

455-455: Consider moving this statement to an else block

(TRY300)


456-456: Do not catch blind exception: Exception

(BLE001)

faststack/faststack/io/executable_validator.py

110-110: Consider moving this statement to an else block

(TRY300)

faststack/faststack/io/indexer.py

39-39: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


65-65: Unused function argument: jpg_path

(ARG001)

faststack/faststack/imaging/jpeg.py

43-43: Redundant exception object included in logging.exception call

(TRY401)


52-52: Redundant exception object included in logging.exception call

(TRY401)


79-79: Consider moving this statement to an else block

(TRY300)


81-81: Redundant exception object included in logging.exception call

(TRY401)


90-90: Redundant exception object included in logging.exception call

(TRY401)


158-158: Redundant exception object included in logging.exception call

(TRY401)


176-176: Redundant exception object included in logging.exception call

(TRY401)

faststack/faststack/io/sidecar.py

57-57: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


87-87: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

faststack/faststack/tests/test_prefetch_logic.py

48-48: Abstract raise to an inner function

(TRY301)


48-48: Create your own exception

(TRY002)


48-48: Avoid specifying long messages outside the exception class

(TRY003)

faststack/faststack/verify_wb.py

62-62: Do not use bare except

(E722)


62-63: try-except-pass detected, consider logging the exception

(S110)

faststack/faststack/tests/test_executable_validator.py

40-40: Unused lambda argument: self

(ARG005)


62-62: Unused lambda argument: self

(ARG005)


66-66: Unpacked variable error is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


123-123: Unused lambda argument: self

(ARG005)


127-127: Unpacked variable error is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

faststack/faststack/tests/test_pairing.py

55-55: Multiple statements on one line (semicolon)

(E702)


58-58: Multiple statements on one line (semicolon)

(E702)


58-58: Multiple statements on one line (semicolon)

(E702)


63-63: Multiple statements on one line (semicolon)

(E702)


63-63: Multiple statements on one line (semicolon)

(E702)


68-68: Multiple statements on one line (semicolon)

(E702)


68-68: Multiple statements on one line (semicolon)

(E702)


69-69: Multiple statements on one line (semicolon)

(E702)


69-69: Multiple statements on one line (semicolon)

(E702)

faststack/faststack/imaging/cache.py

19-19: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

faststack/faststack/tests/test_sidecar.py

14-14: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

faststack/faststack/tests/test_metadata.py

11-11: Unused method argument: mock_exists

(ARG002)


69-69: Use raise without specifying exception name

Remove exception name

(TRY201)

faststack/faststack/io/helicon.py

71-71: Redundant exception object included in logging.exception call

(TRY401)


78-78: subprocess call: check for execution of untrusted input

(S603)


86-86: Consider moving this statement to an else block

(TRY300)


88-88: Redundant exception object included in logging.exception call

(TRY401)


91-91: Redundant exception object included in logging.exception call

(TRY401)

faststack/faststack/config.py

82-82: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

faststack/faststack/imaging/prefetch.py

513-513: Consider moving this statement to an else block

(TRY300)

faststack/faststack/tests/benchmark_decode_bilinear.py

41-41: Do not catch blind exception: Exception

(BLE001)


49-49: Do not catch blind exception: Exception

(BLE001)

faststack/faststack/ui/provider.py

28-28: Unused method argument: size

(ARG002)


28-28: Unused method argument: requestedSize

(ARG002)


64-64: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


705-705: Comment contains ambiguous (NON-BREAKING HYPHEN). Did you mean - (HYPHEN-MINUS)?

(RUF003)

🔇 Additional comments (63)
faststack/faststack/models.py (1)

1-44: LGTM! Well-structured dataclass definitions with appropriate type hints.

The module provides clean, correct data structures for image metadata and image buffers. Type hints are sensible, and the use of dataclasses.field(default_factory=...) for mutable defaults is the right pattern. The __sizeof__ implementation in DecodedImage properly reports buffer memory usage.

faststack/faststack/io/helicon.py (4)

1-15: Import structure looks good.

The addition of the executable validator import properly supports the security hardening in the function.


26-40: Strong security validation of executable path.

The use of validate_executable_path before execution is a solid security improvement. The early-return pattern on validation failure is appropriate.


77-86: Subprocess hardening is sound; clarify temp file lifecycle.

The subprocess call correctly uses shell=False, validates the executable beforehand, and closes file descriptors—mitigating the S603 linter concern. However, the function returns the tmp_path on success but creates the file with delete=False (line 47). Verify that the caller is responsible for cleanup, or document the expected lifecycle. If not handled by the caller, this becomes a resource leak.

Please confirm: who is responsible for deleting the temporary file after Helicon Focus consumes it? Is cleanup handled by the caller or should this function provide cleanup logic?


1-92: AI summary inconsistency: claims formatting-only changes but code includes functional security improvements.

The AI-generated summary states this is a "formatting/reflow only" change with "no functional changes," but the code demonstrates substantive security improvements: executable path validation via validate_executable_path, safe argument parsing using shlex.split, and hardened subprocess execution with shell=False and close_fds=True. These are functional enhancements, not mere formatting adjustments.

faststack/faststack/logging_setup.py (1)

1-46: Formatting changes only—no functional review needed.

This file contains only whitespace/formatting adjustments with no logic changes.

faststack/faststack/faststack.json (1)

1-6: Formatting changes only—no functional review needed.

JSON formatting adjustment with no semantic changes.

faststack/requirements.txt (1)

1-8: Formatting changes only—no functional review needed.

Cosmetic formatting adjustment with no changes to package versions or dependencies.

faststack/faststack/io/watcher.py (1)

1-74: Formatting changes only—no functional review needed.

This file contains only whitespace/formatting adjustments with no logic changes.

faststack/faststack/qml/JumpToImageDialog.qml (1)

1-90: Formatting changes only—no functional review needed.

This file contains only whitespace/formatting adjustments with no logic changes.

faststack/faststack/io/executable_validator.py (1)

1-112: Formatting changes only—no functional review needed.

This file contains only whitespace/formatting adjustments with no logic changes. The static analysis suggestion about moving Line 110 to an else block is a stylistic preference; the current try/return/except/return pattern is clear and idiomatic.

faststack/pyproject.toml (1)

2-34: Metadata and script declaration look consistent

The version bump and script entry wire cleanly into this configuration; nothing here looks problematic from a packaging perspective.

faststack/faststack/imaging/metadata.py (1)

9-37: EXIF value cleaning looks robust for display/JSON use

Handling bytes, strings, and nested lists/tuples via clean_exif_value should keep both the summary and full maps printable and JSON-serializable; this is a good balance between robustness and not over-normalizing the raw values.

faststack/faststack/tests/test_cache.py (1)

7-61: Cache tests adequately cover core behavior

The tests validate initialization, size accounting, eviction of least-recently-used entries, and size updates on overwrite using a simple MockItem; this is a nice focused unit test suite for ByteLRUCache.

faststack/faststack/qml/FilterDialog.qml (1)

1-87: Filter dialog wiring and imports look good

The dialog’s imports, bindings to filterDialog.filterString, and coordination with the controller on open/close are consistent and self-contained; nothing concerning here.

faststack/README.md (1)

29-29: LGTM! Clear feature documentation.

The RGB histogram feature is well-documented with clear usage instructions and functional details about clipping detection and zoom responsiveness.

faststack/faststack/tests/test_editor.py (1)

9-27: LGTM! Well-structured test for mtime preservation.

The test correctly validates that save_image preserves the original file's modification time after applying edits. The use of pytest.approx with tight tolerance ensures precision while accommodating filesystem timestamp limitations.

faststack/ChangeLog.md (1)

5-11: LGTM! Clear release notes for version 1.4.

The changelog entry accurately documents the new features and improvements in this release.

faststack/faststack/config.py (1)

17-19: LGTM! Well-defined configuration defaults.

The new configuration options for optimization mode and auto-levels functionality are clearly defined with sensible defaults.

faststack/faststack/qml/HistogramWindow.qml (2)

15-26: LGTM! Good keyboard focus management.

Adding a FocusScope with H-key handling allows users to close the histogram window using the same key that opened it, improving UX consistency.


197-197: No issues found. The histogram data property names are correct and consistent. The uiState.histogramData structure is created with keys 'r', 'g', 'b', 'r_clip', 'g_clip', 'b_clip', 'r_preclip', 'g_preclip', 'b_preclip' (faststack/faststack/app.py, lines 2361-2371), which match exactly the property accesses in HistogramWindow.qml at lines 197, 220, and 242.

faststack/faststack/tests/debug_metadata.py (1)

13-57: LGTM! Effective debug harness for EXIF extraction.

The test script appropriately mocks PIL components and validates EXIF data extraction. The broad exception handling is suitable for a debug script that logs all errors to a file.

faststack/faststack/ui/keystrokes.py (1)

42-42: Both methods are properly implemented.

The methods show_exif_dialog (line 524) and quick_auto_levels (line 2716) are implemented in faststack/faststack/app.py with complete implementations and docstrings.

faststack/faststack/tests/benchmark_decode.py (1)

1-40: LGTM!

The benchmark implementation follows good practices: uses time.perf_counter() for accurate timing, includes a warmup iteration, and generates appropriately sized test data. The iteration count and timing methodology are suitable for measuring decode performance.

faststack/faststack/tests/test_metadata.py (2)

71-89: LGTM!

Thorough test coverage for clean_exif_value covering strings, bytes, binary fallback, numbers, and lists. The assertions correctly verify the expected behavior for each input type.


91-105: LGTM!

Good edge case coverage testing both the "no EXIF data" scenario and non-existent file handling, ensuring the function gracefully returns empty dictionaries.

faststack/faststack/qml/ExifDialog.qml (1)

81-113: LGTM!

The getDisplayText() function correctly handles both summary (ordered keys) and full (alphabetically sorted) views with appropriate fallback for empty data. The implementation cleanly formats EXIF data for display.

Minor note: Lines 88 and 94 both declare var i, and lines 95 and 102 use var key. While JavaScript's function-level scoping makes this work, using distinct variable names (e.g., j for the second loop) would improve readability.

faststack/faststack/next.prompt (1)

1-1: This appears to be an internal requirements/prompt file.

This file contains natural language descriptions of features and bugs rather than code. It may have been accidentally included in the PR. Consider whether this should be:

  1. Removed from the PR
  2. Converted to a proper issue/task tracker
  3. Moved to a documentation directory if intentional
faststack/faststack/qml/Main.qml (5)

38-42: LGTM!

The openExifDialog helper cleanly separates summary and full data population from dialog opening, providing a clear API for displaying EXIF information.


650-678: LGTM!

The keyboard handling for the editor toggle (E key) and save (Ctrl+S) is well implemented with proper guards for uiState and controller availability, and correctly sets event.accepted to prevent event bubbling.


682-692: Good fix for layout stability.

Using a fixed footer height prevents the main image area from resizing when status labels appear/disappear, which avoids unnecessary cache invalidations. This is a thoughtful optimization.


966-969: LGTM!

The show_delete_batch_dialog function properly sets the batch count before opening the dialog, ensuring the confirmation dialog displays the correct number of images to be deleted.


971-975: LGTM!

The ExifDialog is properly instantiated with theme-aware colors bound from the root window properties, ensuring consistent styling with the rest of the application.

faststack/faststack/tests/benchmark_decode_bilinear.py (2)

53-60: LGTM!

The create_test_jpeg function is consistent with the implementation in benchmark_decode.py and correctly generates a random test image for benchmarking.


62-84: LGTM!

The benchmark function follows the same pattern as benchmark_decode.py with appropriate warmup and iteration-based timing. The main entry point is correctly structured.

faststack/faststack/tests/test_new_features.py (2)

42-67: LGTM!

The test_highlights_recovery test correctly validates the negative highlights (recovery) behavior by checking that bright pixels (255) are significantly darkened while midtone pixels (128) remain largely unchanged. The expected calculations match the implementation in _apply_edits.


69-81: LGTM!

The test_straighten_angle test verifies that applying a 45-degree rotation with expand=True changes the image dimensions, which is the expected behavior for free rotation.

faststack/faststack/qml/DeleteBatchDialog.qml (3)

5-23: LGTM!

The dialog setup is well-structured with appropriate modal behavior, configurable colors, and clean background styling.


49-88: Good UX: Destructive action is visually distinguished.

The "Delete All" button appropriately uses red coloring to indicate a destructive action, and the "Delete Current Image" button uses neutral styling. The controller method calls are properly guarded.


109-121: LGTM!

The onOpened and onClosed handlers correctly notify the Python controller about dialog state, which is consistent with patterns used by other dialogs (e.g., ExifDialog).

faststack/faststack/qml/SettingsDialog.qml (2)

56-75: LGTM!

The onAccepted handler correctly persists all settings including the new optimizeFor, auto-level, and AWB settings to uiState.


224-253: LGTM!

The Auto Levels UI section is well-implemented with appropriate validation on the threshold field (0.0-10.0 range) and a slider for strength (0.0-1.0). The percentage display for strength is a nice touch for user clarity.

faststack/faststack/imaging/editor.py (7)

93-95: LGTM!

New edit parameters texture and straighten_angle are properly initialized with sensible defaults.


187-196: LGTM!

The new negative highlights (recovery) branch correctly implements the recovery algorithm by applying a multiplicative factor that darkens bright pixels while leaving midtones unchanged. The mask calculation and factor application align with the test expectations in test_new_features.py.


242-261: LGTM!

The reworked white balance using multiplicative gains is a significant improvement over additive shifts. This approach correctly preserves black levels (since 0 × gain = 0) while adjusting color balance proportionally across the tonal range. The comments clearly explain the temperature and tint adjustments.


283-299: LGTM!

The texture enhancement implementation follows the same pattern as clarity but with a smaller blur radius (2.0 vs 20) to target fine details. The midtone masking approach is appropriate for this use case.


302-344: LGTM!

The auto_levels implementation correctly computes percentile-based black and white points from the luminance histogram. The conversion formulas for blacks and whites are consistent with the level mapping in _apply_edits.


389-394: Good defensive coding for timestamp capture.

Properly handling the case where reading file timestamps fails, with a clear warning message. This prevents crashes on edge cases like permission issues.


460-466: LGTM!

The _restore_file_times helper is well-designed with best-effort semantics and appropriate error handling.

faststack/faststack/imaging/prefetch.py (3)

37-52: LGTM on ICC transform caching.

The caching strategy using stable keys (SHA-256 digest for embedded profiles, file path for monitor profiles) is sound and thread-safe with proper lock usage.


96-136: LGTM on saturation compensation.

The in-place RGB saturation adjustment is correctly implemented with proper bounds checking, early return optimization for factor=1.0, and vectorized numpy operations.


541-545: LGTM on shutdown handling.

The shutdown method correctly cancels all pending tasks before shutting down the executor with wait=False for responsive shutdown.

faststack/faststack/qml/ImageEditorDialog.qml (5)

43-53: LGTM on keyboard shortcuts.

The E and Escape keys for closing the editor provide good UX for quick dismissal, and event.accepted = true properly prevents event propagation.


122-131: LGTM on Auto Levels button.

The button correctly calls controller.auto_levels() and triggers a UI refresh via updatePulse++, following the same pattern as the Auto White Balance button.


200-222: LGTM on direct value input.

The TextInput with validation, clamping, and proper normalization for backend values provides a clean UX for precise parameter adjustment.


255-263: LGTM on double-tap reset.

The TapHandler for double-tap to reset provides convenient UX for returning sliders to their neutral value.


160-174: LGTM on button renaming and behavior.

The renamed buttons clearly communicate their actions, and the "Save and Close" button correctly chains save_edited_image() with closing the editor.

faststack/faststack/qml/Components.qml (3)

54-70: LGTM on zoom state management.

The updateZoomState() function correctly manages zoom threshold detection (1.1 for zoomed in, 1.0 for zoomed out) and triggers histogram updates when visible.


545-630: LGTM on aspect ratio constraint logic.

The function correctly handles all drag modes, maintains aspect ratio after boundary clamping, and properly anchors to the correct edge/corner based on drag direction.


715-751: Overlay rectangles don't account for crop rotation.

The four semi-transparent overlay rectangles use cropRect.x, cropRect.y, etc. directly without accounting for the rotation applied to cropRect at line 759. When the crop box is rotated, these overlays won't align correctly with the rotated crop area.

Is the rotation feature expected to show aligned overlays, or is this a known limitation? If alignment is expected, the overlay rendering would need to use a more complex approach (e.g., canvas-based masking or rotated clip paths).

faststack/faststack/ui/provider.py (3)

46-61: LGTM on color space handling.

The conditional color space assignment correctly handles ICC mode (where pixels are already in monitor space), non-ICC mode (sets sRGB), and gracefully degrades when QColorSpace is unavailable. The buffer attachment at line 60 correctly prevents premature garbage collection.


637-652: LGTM on snake_case aliases for QML.

The snake_case property aliases for white_balance_by and white_balance_mg provide convenient QML bracket notation access while maintaining the camelCase primary properties.


28-28: Unused parameters are required by interface.

The size and requestedSize parameters flagged by static analysis are required by the QQuickImageProvider.requestImage interface signature, even if not used. No change needed.

Comment thread faststack/ChangeLog.md
Comment thread faststack/faststack.egg-info/PKG-INFO
Comment thread faststack/faststack.egg-info/PKG-INFO
Comment thread faststack/faststack.egg-info/PKG-INFO
Comment thread faststack/faststack/imaging/cache.py
Comment thread faststack/faststack/tests/test_new_features.py
Comment thread faststack/faststack/tests/test_sidecar.py
Comment thread faststack/faststack/tests/test_sidecar.py
Comment thread faststack/faststack/ui/provider.py
Comment thread faststack/faststack/verify_wb.py
@AlanRockefeller AlanRockefeller merged commit 8782406 into main Dec 8, 2025
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Jan 2, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Feb 5, 2026
Merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant