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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions faststack/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
from faststack.imaging.mask_engine import inverse_transform
from faststack.imaging.metadata import get_exif_data
from faststack.thumbnail_view import (
DEFAULT_THUMBNAIL_CACHE_BYTES,
ThumbnailModel,
ThumbnailPrefetcher,
ThumbnailCache,
Expand Down Expand Up @@ -351,7 +352,7 @@ def __init__(
self._grid_model_dirty = True # Start dirty to ensure initial load
self._folder_loaded = False # Track whether the current folder scan is complete
self._thumbnail_cache = ThumbnailCache(
max_bytes=256 * 1024 * 1024, # 256 MB
max_bytes=DEFAULT_THUMBNAIL_CACHE_BYTES, # cache-ready thumbnail QImages
max_items=5000,
)
self._path_resolver = PathResolver()
Expand Down Expand Up @@ -8030,7 +8031,6 @@ def quick_auto_white_balance(self):

t_start = time.perf_counter()

image_file = self.image_files[self.current_index]
if self.view_override_path:
active_path = Path(self.view_override_path)
else:
Expand Down
7 changes: 3 additions & 4 deletions faststack/imaging/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
import numpy as np
from PIL import ExifTags, Image, ImageFilter, ImageOps

# Mask subsystem (lazy imports avoided — lightweight dataclasses)
from faststack.imaging.mask import MaskData
from faststack.imaging.mask_engine import MaskRasterCache
from faststack.imaging.math_utils import (
_analyze_highlight_state,
_apply_headroom_shoulder,
Expand All @@ -27,10 +30,6 @@
from faststack.imaging.orientation import apply_orientation_to_np, get_exif_orientation
from faststack.models import DecodedImage

# Mask subsystem (lazy imports avoided — lightweight dataclasses)
from faststack.imaging.mask import DarkenSettings, MaskData
from faststack.imaging.mask_engine import MaskRasterCache

try:
from PySide6.QtGui import QImage
except ImportError:
Expand Down
51 changes: 0 additions & 51 deletions faststack/repro.py

This file was deleted.

5 changes: 2 additions & 3 deletions faststack/tests/test_editor_reopening.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import unittest
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch

Expand Down Expand Up @@ -130,7 +130,6 @@ def test_reuse_returns_REUSED_not_True(self):
def test_prepare_darken_skips_reset_on_reuse(self):
"""_prepare_darken_image_state must NOT call _reset_darken_on_navigation
when load_image_for_editing returns _REUSED."""
target = Path("test.jpg")
self.controller.image_editor.current_filepath = None # Force a load
self.controller.image_editor.float_image = None # Force needs_load=True
self.controller.image_editor.current_edits = {}
Expand Down Expand Up @@ -184,7 +183,7 @@ def test_save_closes_ui_immediately_but_keeps_memory(self):
# Mock snapshot
self.controller.image_editor.snapshot_for_export.return_value = MagicMock()

with patch.object(self.controller, "_save_executor") as mock_executor:
with patch.object(self.controller, "_save_executor"):
# 2. CALL SAVE
self.controller.save_edited_image()

Expand Down
7 changes: 3 additions & 4 deletions faststack/tests/test_mask.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Tests for the reusable mask subsystem and background darkening tool."""

import math
import unittest

import numpy as np
Expand Down Expand Up @@ -427,7 +426,6 @@ def test_apply_edits_with_darken(self):
# Create a small test image
img = PILImage.new("RGB", (50, 50), color=(128, 128, 128))
import tempfile
from pathlib import Path

with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f:
img.save(f.name)
Expand Down Expand Up @@ -712,10 +710,11 @@ def test_mask_overlay_returns_transparent_when_no_overlay(self):
"""Verify that requesting mask_overlay with no image returns a
transparent QImage, not an opaque placeholder."""
try:
from unittest.mock import Mock

from PySide6.QtGui import QImage
from PySide6.QtCore import Qt

from faststack.ui.provider import ImageProvider
from unittest.mock import Mock
except ImportError:
self.skipTest("PySide6 not available")

Expand Down
6 changes: 3 additions & 3 deletions faststack/tests/thumbnail_view/test_folder_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def test_single_file_uploaded(self):
buckets = _compute_coverage_buckets(jpg_files, entries, num_buckets=1)

assert len(buckets) == 1
assert buckets[0] == (1.0, 0.0, 0.0) # uploaded, not stacked, not todo
assert buckets[0] == (1.0, 0.0, 0.0, 0.0)

def test_single_file_stacked(self):
"""Test with single stacked file."""
Expand All @@ -282,7 +282,7 @@ def test_single_file_stacked(self):
buckets = _compute_coverage_buckets(jpg_files, entries, num_buckets=1)

assert len(buckets) == 1
assert buckets[0] == (0.0, 1.0, 0.0) # not uploaded, stacked, not todo
assert buckets[0] == (0.0, 0.0, 1.0, 0.0)

def test_even_distribution(self):
"""Test even distribution across buckets."""
Expand Down Expand Up @@ -356,7 +356,7 @@ def test_coverage_buckets_support_path_keys(self, temp_folder):
stats = read_folder_stats(temp_folder)

assert stats is not None
assert stats.coverage_buckets == [(1.0, 0.0, 0.0)]
assert stats.coverage_buckets == [(1.0, 0.0, 0.0, 0.0)]


class TestCountImagesInFolder:
Expand Down
26 changes: 21 additions & 5 deletions faststack/tests/thumbnail_view/test_prefetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import pytest
from PIL import Image
from PySide6.QtGui import QImage

from faststack.io.utils import compute_path_hash
from faststack.thumbnail_view.prefetcher import ThumbnailCache, ThumbnailPrefetcher
Expand Down Expand Up @@ -185,6 +186,16 @@ def test_size_and_bytes_used(self, cache):
assert cache.size == 2
assert cache.bytes_used == 10

def test_qimage_size_accounting(self):
"""QImage entries should count their in-memory pixel footprint."""
cache = ThumbnailCache(max_bytes=1024 * 1024, max_items=10)
image = QImage(10, 10, QImage.Format.Format_RGB888)

cache.put("img", image)

assert cache.get("img") is image
assert cache.bytes_used == image.bytesPerLine() * image.height()

def test_update_existing_key(self, cache):
"""Test updating an existing key."""
cache.put("key1", b"old")
Expand Down Expand Up @@ -216,7 +227,9 @@ def test_submit_schedules_job(self, prefetcher, test_image, cache, qt_app):
lambda: cache.get(cache_key) is not None, timeout_s=2.0, qt_app=qt_app
)

assert cache.get(cache_key) is not None
cached_image = cache.get(cache_key)
assert isinstance(cached_image, QImage)
assert not cached_image.isNull()

def test_submit_skips_if_cached(self, prefetcher, test_image, cache):
"""Test that submit skips if already cached."""
Expand Down Expand Up @@ -328,9 +341,10 @@ def test_decode_applies_exif_orientation(self, cache, temp_folder, qt_app):
lambda: cache.get(cache_key) is not None, timeout_s=2.0, qt_app=qt_app
)

cached_bytes = cache.get(cache_key)
assert cached_bytes is not None
assert len(cached_bytes) > 0
cached_image = cache.get(cache_key)
assert isinstance(cached_image, QImage)
assert not cached_image.isNull()
assert cached_image.height() >= cached_image.width()
finally:
prefetcher.shutdown()

Expand All @@ -357,7 +371,9 @@ def test_decode_handles_png(self, cache, temp_folder, qt_app):
assert _wait_until(
lambda: cache.get(cache_key) is not None, timeout_s=2.0, qt_app=qt_app
)
assert cache.get(cache_key) is not None
cached_image = cache.get(cache_key)
assert isinstance(cached_image, QImage)
assert not cached_image.isNull()
finally:
prefetcher.shutdown()

Expand Down
18 changes: 18 additions & 0 deletions faststack/tests/thumbnail_view/test_provider_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import pytest
from PySide6.QtCore import QSize
from PySide6.QtGui import QColor, QImage

from faststack.thumbnail_view.prefetcher import ThumbnailCache
from faststack.thumbnail_view.provider import (
Expand Down Expand Up @@ -155,3 +156,20 @@ def test_bad_cached_bytes_evicts_and_submits(self, wired_provider):
assert args[1] == 999 # mtime_ns
assert args[2] == 200 # thumb_size
assert kwargs["priority"] == prefetcher.PRIO_HIGH

def test_qimage_cache_hit_returns_cached_image_without_submit(self, wired_provider):
provider, cache, prefetcher = wired_provider

cache_key = "200/abc123/999"
cached = QImage(20, 12, QImage.Format.Format_RGB888)
cached.fill(QColor(12, 34, 56))
cache.put(cache_key, cached)

out_size = QSize()
result = provider.requestImage(f"{cache_key}?r=1", out_size, QSize())

assert result is cached
pixel = result.pixelColor(0, 0)
assert (pixel.red(), pixel.green(), pixel.blue()) == (12, 34, 56)
assert (out_size.width(), out_size.height()) == (20, 12)
prefetcher.submit.assert_not_called()
7 changes: 6 additions & 1 deletion faststack/thumbnail_view/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

from .folder_stats import FolderStats, read_folder_stats
from .model import ThumbnailEntry, ThumbnailModel
from .prefetcher import ThumbnailCache, ThumbnailPrefetcher
from .prefetcher import (
DEFAULT_THUMBNAIL_CACHE_BYTES,
ThumbnailCache,
ThumbnailPrefetcher,
)
from .provider import PathResolver, ThumbnailProvider

__all__ = [
"FolderStats",
"DEFAULT_THUMBNAIL_CACHE_BYTES",
"PathResolver",
"read_folder_stats",
"ThumbnailCache",
Expand Down
Loading
Loading