Skip to content

Update image editor#26

Merged
AlanRockefeller merged 1 commit intomainfrom
test
Jan 2, 2026
Merged

Update image editor#26
AlanRockefeller merged 1 commit intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Jan 2, 2026

Summary by CodeRabbit

Release Notes

  • Performance

    • Asynchronous histogram and preview rendering prevents UI freezing during heavy editing operations.
  • Bug Fixes

    • Improved input validation for crop operations with bounds clamping and ordering.
    • Added safeguards against division by zero in crop aspect ratio calculation.
    • Enhanced image format handling and buffer management for safer editing.
  • UI/UX Improvements

    • Parameter slider changes now throttled for smoother, more responsive editing.
    • Console logging added for improved debugging visibility.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 2, 2026

Walkthrough

The PR introduces asynchronous rendering for histograms and previews using background thread pools with token-based result matching to prevent stale data application. It enhances ImageEditor with thread-safety, caching, and new public methods for preview retrieval. Input validation, logging, and UI slider throttling are refined. Image buffer management is optimized for live editing, and crop box handling gains defensive copies and bounds checking.

Changes

Cohort / File(s) Summary
Async Rendering Infrastructure
faststack/faststack/app.py
Introduced histogramReady and previewReady signals with corresponding background ThreadPoolExecutor workers. Added state management (token, in-flight, pending, locks) for concurrent task orchestration and token-based result deduplication. Integrated graceful executor shutdown on app exit. Refactored histogram and preview updates from direct calls to signal-based handlers (_apply_histogram_result, _apply_preview_result). Added throttled preview refresh via _preview_refresh_pending flag and timer. Enhanced execute_crop with robust crop_box_raw validation (4-tuple normalization, bounds clamping, format handling).
Image Processing & Caching
faststack/faststack/imaging/editor.py
Added re-entrant lock and revision/caching counters for thread-safe state management. Introduced get_preview_data_cached(allow_compute) and get_edit_value(key, default) public methods. Modified _apply_edits signature to accept optional external edits snapshot. Updated rotate_image_cw/rotate_image_ccw to operate under lock and increment revision counter. Enhanced exception logging.
Input Validation
faststack/faststack/imaging/jpeg.py
Tightened decode_jpeg_resized validation from == 0 to <= 0 for width and height, rejecting negative or zero dimensions.
Logging & Instrumentation
faststack/faststack/imaging/prefetch.py
Added explicit copy phase duration (copy=) timing to ICC and non-ICC decode paths, improving performance metric granularity.
Logging Setup
faststack/faststack/logging_setup.py
Introduced console handler (StreamHandler) alongside existing rotating file handler; both attached to root logger with shared formatter.
Documentation
faststack/faststack/config.py
Added extensive inline documentation for Auto Levels feature (behavior, algorithm, tuning guidance); configuration keys and defaults unchanged.
QML Crop Handling
faststack/faststack/qml/Components.qml
Introduced defensive copy of crop box (array to local variable) to prevent in-place mutations. Added guard for division by zero in cropStartAspect calculation (boxH > 0 check). Replaced repeated currentCropBox fetches with consistent local box variable usage.
QML Slider Throttling & Headers
faststack/faststack/qml/ImageEditorDialog.qml
Removed headerText property from SectionHeader; now assigned via onLoaded handlers in Loader instances with emoji-label strings. Introduced slider auto-synchronization with internal state (_pendingValue, _lastSentValue) and throttle timer (sendTimer) to batch parameter updates. Enhanced onMoved and onPressedChanged to queue changes and defer backend updates until timer fires or slider release.
Image Provider Buffer Management
faststack/faststack/ui/provider.py
Added specialized image fetch path for live editing: uses last rendered preview if available, otherwise falls back to get_decoded_image. Conditional buffer management: deep-copies QImage when editing (detach from shared memory), attaches buffer reference when not editing (prevent GC). Guarded QImage creation with format detection (None → RGB888).

Sequence Diagrams

sequenceDiagram
    participant UI as UI Thread
    participant AC as AppController
    participant Pool as Thread Pool
    participant IE as ImageEditor
    
    rect rgb(230, 245, 250)
    Note over UI,IE: Histogram Async Flow
    UI->>AC: set_edit_parameter(key, value)
    AC->>AC: _kick_histogram_worker()
    AC->>Pool: submit(_compute_histogram_worker)
    
    Pool->>IE: get_preview_data_cached()
    IE-->>Pool: DecodedImage (cached)
    Pool->>Pool: compute histogram from image
    
    Pool-->>AC: _on_histogram_done(future)
    AC->>AC: extract result (token-guarded)
    AC->>UI: emit histogramReady(payload)
    UI->>UI: _apply_histogram_result(payload)
    end
    
    rect rgb(245, 230, 250)
    Note over UI,IE: Preview Async Flow
    UI->>AC: load_image_for_editing() / enter crop mode
    AC->>AC: _kick_preview_worker()
    AC->>Pool: submit(_render_preview_worker)
    
    Pool->>IE: render preview with current edits
    IE-->>Pool: rendered DecodedImage
    Pool-->>AC: _on_preview_done(future)
    
    AC->>AC: token match check
    AC->>AC: _last_rendered_preview = result
    AC->>UI: emit previewReady(payload)
    UI->>UI: _apply_preview_result(payload)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #24: Directly related—modifies the same AppController crop/zoom handling, ImageEditor rotation/autocrop, and UI provider crop wiring that this PR refactors.
  • PR #23: Directly related—updates ImageEditor._apply_edits contract to accept optional external edits snapshot, mirroring changes in this PR's caching/preview logic.
  • PR #25: Directly related—overlaps with histogram and execute_crop behavior modifications in AppController.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Update image editor' is vague and generic, failing to convey the specific nature of the substantial changes made across the codebase. Consider using a more descriptive title that captures the primary change, such as 'Add asynchronous offloading for histogram and preview rendering' or 'Implement background thread pools for image editor operations'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings

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: 0

🧹 Nitpick comments (3)
faststack/faststack/imaging/editor.py (1)

58-58: Minor: Redundant exception object in log.exception() calls.

Per static analysis (TRY401), log.exception() automatically includes the current exception, so passing e is redundant. This is a cosmetic issue and doesn't affect functionality.

🔎 Proposed fix
-        log.exception(f"Failed to create backup: {e}")
+        log.exception("Failed to create backup")

Similar changes can be applied to lines 258, 722, and 731.

Also applies to: 258-258, 722-722, 731-731

faststack/faststack/app.py (2)

3027-3049: Robust crop box normalization with good defensive coding.

The validation handles QJSValue wrappers, list-to-tuple conversion, bounds clamping, and coordinate ordering. This prevents edge cases from causing crashes.

Minor: Static analysis flags l (line 3041) as an ambiguous variable name. Consider using left for clarity.

🔎 Proposed fix for variable naming
-            l, t, r, b = [max(0, min(1000, int(x))) for x in crop_box_raw]
-            
-            # Ensure correct order (left <= right, top <= bottom)
-            crop_box_raw = (min(l, r), min(t, b), max(l, r), max(t, b))
+            left, top, right, bottom = [max(0, min(1000, int(x))) for x in crop_box_raw]
+            
+            # Ensure correct order (left <= right, top <= bottom)
+            crop_box_raw = (min(left, right), min(top, bottom), max(left, right), max(top, bottom))

2504-2528: Broad exception handling is appropriate here for UI stability.

Catching Exception in set_edit_parameter prevents slider interactions from crashing the app. The error is logged for debugging. Per static analysis, consider using log.exception() instead of log.error() to include the stack trace.

🔎 Proposed fix
         except Exception as e:
-            log.error("Error setting edit parameter %s=%s: %s", key, value, e)
+            log.exception("Error setting edit parameter %s=%s", key, value)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fda086c and ca65fc0.

📒 Files selected for processing (9)
  • faststack/faststack/app.py
  • faststack/faststack/config.py
  • faststack/faststack/imaging/editor.py
  • faststack/faststack/imaging/jpeg.py
  • faststack/faststack/imaging/prefetch.py
  • faststack/faststack/logging_setup.py
  • faststack/faststack/qml/Components.qml
  • faststack/faststack/qml/ImageEditorDialog.qml
  • faststack/faststack/ui/provider.py
🧰 Additional context used
📓 Path-based instructions (5)
faststack/faststack/**/*.py

📄 CodeRabbit inference engine (faststack/faststack/AGENTS.md)

faststack/faststack/**/*.py: Use four-space indentation in Python files
Use trailing commas for multi-line structures in Python
Use descriptive snake_case identifiers for variables and functions in Python
Use CamelCase exclusively for class names and Qt signal names in Python
Include type hints in all Python code as seen in models.py and AppController
Every new Python module should expose a top-level log = logging.getLogger(__name__) logger
Format Python code with Black before opening a pull request
Lint Python code with Ruff before opening a pull request
Prefer relative paths in configuration and file handling

Files:

  • faststack/faststack/config.py
  • faststack/faststack/imaging/prefetch.py
  • faststack/faststack/logging_setup.py
  • faststack/faststack/imaging/jpeg.py
  • faststack/faststack/imaging/editor.py
  • faststack/faststack/app.py
  • faststack/faststack/ui/provider.py
faststack/faststack/**/config.py

📄 CodeRabbit inference engine (faststack/faststack/AGENTS.md)

Document any new INI key in config.py

Files:

  • faststack/faststack/config.py
faststack/faststack/imaging/**/*.py

📄 CodeRabbit inference engine (faststack/faststack/AGENTS.md)

Cover every new branch in imaging/ or io/ modules with focused pytest test cases

Files:

  • faststack/faststack/imaging/prefetch.py
  • faststack/faststack/imaging/jpeg.py
  • faststack/faststack/imaging/editor.py
faststack/faststack/**/app.py

📄 CodeRabbit inference engine (faststack/faststack/AGENTS.md)

app.py is the PySide6 entrypoint that coordinates image indexing, caching, and QML screens

Files:

  • faststack/faststack/app.py
faststack/faststack/ui/**/*.py

📄 CodeRabbit inference engine (faststack/faststack/AGENTS.md)

Treat drag-and-drop helpers like make_hdrop as security-sensitive and validate all user-supplied paths before invoking external tools

Files:

  • faststack/faststack/ui/provider.py
🧠 Learnings (4)
📚 Learning: 2025-12-08T04:46:41.632Z
Learnt from: CR
Repo: AlanRockefeller/faststack PR: 0
File: faststack/faststack/AGENTS.md:0-0
Timestamp: 2025-12-08T04:46:41.632Z
Learning: Applies to faststack/faststack/**/config.py : Document any new INI key in `config.py`

Applied to files:

  • faststack/faststack/config.py
📚 Learning: 2025-12-08T04:46:41.632Z
Learnt from: CR
Repo: AlanRockefeller/faststack PR: 0
File: faststack/faststack/AGENTS.md:0-0
Timestamp: 2025-12-08T04:46:41.632Z
Learning: `imaging/` module manages decode workflow, metadata, editing, and ICC caches

Applied to files:

  • faststack/faststack/imaging/prefetch.py
  • faststack/faststack/imaging/editor.py
📚 Learning: 2025-12-08T04:46:41.632Z
Learnt from: CR
Repo: AlanRockefeller/faststack PR: 0
File: faststack/faststack/AGENTS.md:0-0
Timestamp: 2025-12-08T04:46:41.632Z
Learning: Applies to faststack/faststack/**/*.py : Every new Python module should expose a top-level `log = logging.getLogger(__name__)` logger

Applied to files:

  • faststack/faststack/logging_setup.py
📚 Learning: 2025-12-08T04:46:41.632Z
Learnt from: CR
Repo: AlanRockefeller/faststack PR: 0
File: faststack/faststack/AGENTS.md:0-0
Timestamp: 2025-12-08T04:46:41.632Z
Learning: Applies to faststack/faststack/**/app.py : `app.py` is the PySide6 entrypoint that coordinates image indexing, caching, and QML screens

Applied to files:

  • faststack/faststack/imaging/editor.py
  • faststack/faststack/app.py
  • faststack/faststack/ui/provider.py
🧬 Code graph analysis (2)
faststack/faststack/imaging/editor.py (2)
faststack/faststack/models.py (1)
  • DecodedImage (37-46)
faststack/faststack/app.py (1)
  • get_preview_data (2493-2495)
faststack/faststack/ui/provider.py (1)
faststack/faststack/app.py (1)
  • get_decoded_image (436-550)
🪛 Ruff (0.14.10)
faststack/faststack/imaging/editor.py

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

(TRY401)


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

(TRY300)


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

(TRY401)


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

(TRY300)


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

(TRY401)


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

(TRY401)

faststack/faststack/app.py

2527-2527: Do not catch blind exception: Exception

(BLE001)


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

Replace with exception

(TRY400)


2664-2664: Redefinition of unused np from line 55

Remove definition: np

(F811)


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

(TRY300)


2718-2718: Do not catch blind exception: Exception

(BLE001)


2727-2727: Do not catch blind exception: Exception

(BLE001)


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

(TRY300)


2786-2786: Do not catch blind exception: Exception

(BLE001)


3038-3038: Abstract raise to an inner function

(TRY301)


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

(TRY003)


3041-3041: Ambiguous variable name: l

(E741)

🔇 Additional comments (22)
faststack/faststack/config.py (1)

18-43: Excellent documentation for Auto Levels configuration.

The expanded inline documentation block clearly explains the behavior, algorithm, and practical tuning guidelines for the Auto Levels feature. The examples (0.05, 0.001) are well-chosen and the distinction between fraction and percentage is explicit. Default values are sensible and align with the documented guidance.

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

118-119: LGTM: Improved input validation.

The boundary check now handles negative dimensions in addition to zero, making the validation more robust. This prevents potential issues downstream if negative dimensions are passed.

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

422-426: LGTM: Enhanced timing metrics.

The explicit copy phase duration logging provides useful performance insights for the ICC fallback path, consistent with the timing structure in other decode paths.


456-459: LGTM: Consistent timing metrics.

The copy phase timing addition aligns with the ICC fallback path and provides consistent observability across all decode paths.

faststack/faststack/logging_setup.py (1)

25-43: LGTM: Console logging addition.

Adding a console handler alongside the file handler improves developer experience by making logs visible in the terminal. Both handlers share the same formatter and respect the debug flag, which is the correct approach.

faststack/faststack/qml/Components.qml (2)

618-618: LGTM: Defensive copy prevents mutations.

Creating a defensive copy of the crop box array prevents unintended mutations of the shared uiState.currentCropBox object, which is good practice for avoiding side effects.


662-676: LGTM: Division-by-zero guard and consistent data handling.

The addition of the boxH > 0 check on Line 665 prevents division by zero when calculating the crop start aspect ratio. Using the defensive copy throughout ensures consistent behavior and prevents reference-related bugs.

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

36-43: LGTM: Correct preview routing.

The logic correctly restricts the editor preview to only the currently edited index (index == self.app_controller.current_index), preventing thumbnails or prefetch requests for other images from receiving the editor's live preview buffer. The fallback to get_decoded_image(index) ensures a valid image is always returned.


46-57: LGTM: Defensive format handling.

The code safely handles the case where image_data.format is None or missing (from the prefetcher) by defaulting to Format_RGB888. This prevents crashes while maintaining correct behavior.


59-69: LGTM: Correct buffer ownership strategy.

The two-path buffer management approach is correct:

  1. Editor path (copy): The deep copy detaches from shared memory that may be mutated during live editing, preventing texture corruption. This is necessary despite the performance cost.

  2. Browsing path (reference): Keeping original_buffer as a Python reference prevents garbage collection while Qt holds the QImage data pointer. This is essential because QImage created from bytes does not own or copy the underlying buffer.

This implementation correctly handles the ownership semantics of QImage in Python bindings.

faststack/faststack/qml/ImageEditorDialog.qml (3)

400-421: Well-designed slider throttling mechanism.

The throttle Timer pattern at ~30fps (32ms interval) is appropriate for smooth UI updates while preventing backend overload. The delta check (> 0.001) prevents redundant updates. The Binding with when: !slider.pressed correctly allows backend-driven updates when the user isn't actively dragging.


457-468: Solid cleanup on slider release.

Stopping the timer and sending the final value immediately on release ensures the backend always receives the user's intended final value. Triggering update_histogram() only on release is appropriate since histogram computation can be expensive.


123-123: Section header initialization via onLoaded is acceptable.

Using onLoaded to set the text property works correctly with Loader components. This is a minor refactor from property binding.

Also applies to: 140-143, 220-220, 257-260, 270-273

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

176-180: Good thread-safety foundation with RLock and revision tracking.

Using threading.RLock() allows reentrant calls, and the revision counter pattern (_edits_rev, _cached_rev) is a clean way to invalidate cached previews. This design correctly separates mutation tracking from the actual preview computation.


524-564: Well-implemented cached preview with snapshot pattern.

The implementation correctly:

  1. Acquires lock only to check cache validity and snapshot data
  2. Performs heavy computation (_apply_edits) outside the lock
  3. Re-checks revision before caching to avoid stale updates

The allow_compute parameter is a nice touch for non-blocking callers.


599-617: Float comparison prevents unnecessary cache invalidation.

Using math.isclose() with appropriate tolerances prevents cache thrashing from floating-point noise. The fallback to direct equality for non-numeric types is sensible.

faststack/faststack/app.py (6)

97-113: Proper background executor setup with clean shutdown handling.

Single-worker ThreadPoolExecutors for histogram and preview rendering prevent thread explosion. The in-flight/pending flags and token-based matching are correct patterns for coalescing rapid requests.


2157-2162: Clean executor shutdown prevents hanging on exit.

Setting _shutting_down = True before shutdown and using wait=False, cancel_futures=True ensures the application doesn't hang. The flag is correctly checked in all callback methods.


2655-2719: Histogram computation correctly offloaded to background thread.

The static method pattern prevents accidental QObject access. Token-based result gating in _apply_histogram_result correctly discards stale results.

One note: line 2664 re-imports numpy as np inside the static method, shadowing the module-level import. This is harmless but unnecessary since the module-level import is already available.


2750-2812: Preview rendering with token-based stale rejection is well-implemented.

The pattern correctly:

  1. Guards against re-entry with _preview_inflight
  2. Uses token matching to discard outdated renders
  3. Uses QTimer.singleShot(0, ...) to chain pending requests on the UI thread

One minor observation: _apply_preview_result holds _preview_lock while emitting signals and modifying UI state. Since the slot runs on the UI thread (via signal-slot queuing), this is safe but slightly unusual. Consider moving signal emission outside the lock if you encounter deadlocks in the future.


464-467: Preview fallback correctly uses background-rendered result.

Returning _last_rendered_preview when the editor is open ensures the UI displays the most recent edit preview rather than stale cached data.


2766-2778: Thread safety is correctly implemented.

All modifications to _preview_image and original_image are properly protected by self._lock. The get_preview_data_cached() method correctly uses the snapshot pattern: it copies _preview_image and current_edits under the lock, then releases the lock before performing heavy computation with _apply_edits(). The static method _render_preview_worker safely receives image_editor and calls get_preview_data_cached(allow_compute=True) without race conditions.

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