diff --git a/.gitignore b/.gitignore index 9a66800..ae93fd6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__/ dist/ build/ *.spec +faststack.egg-info/ # Logs *.log @@ -22,17 +23,15 @@ Thumbs.db .vscode/ .idea/ +# Documentation/Generated prompt.md WARP.md AGENTS.md +ARCHITECTURE.md + +# Caches faststack/.mypy_cache/ .mypy_cache/ -var/ -faststack.egg-info/ - -# Local-only docs -ARCHITECTURE.md -# Local test/debug outputs -out.txt -test_out*.txt +# Runtime/Data +var/ diff --git a/faststack/app.py b/faststack/app.py index e97ff70..a812d30 100644 --- a/faststack/app.py +++ b/faststack/app.py @@ -2648,6 +2648,11 @@ def _kick_histogram_worker(self): # But histogram is mostly for edits. If preview_data is None, we likely can't compute anyway. # We can try to peek at the image editor if _last_rendered_preview is unset. preview_data = self.image_editor.get_preview_data_cached(allow_compute=False) + + # Fallback: If still no preview data (e.g. editor not open), use the main image + if not preview_data and 0 <= self.current_index < len(self.image_files): + # This ensures histogram works even if we haven't opened the editor + preview_data = self.get_decoded_image(self.current_index) # If still no data, we cannot compute the histogram. # Ensure we don't drop the request: keep _hist_pending set (it was cleared above, restore it?) @@ -2826,6 +2831,9 @@ def _apply_preview_result(self, payload): # Ensure QML/provider URL changes so we don't get a cached frame self.ui_refresh_generation += 1 self.ui_state.currentImageSourceChanged.emit() + + # Trigger histogram update (always call, let update_histogram handle visibility guards) + self.update_histogram() # If new requests arrived while we were rendering, start the next one immediately if self._preview_pending: diff --git a/faststack/imaging/cache.py b/faststack/imaging/cache.py index 75553b3..f37f078 100644 --- a/faststack/imaging/cache.py +++ b/faststack/imaging/cache.py @@ -46,6 +46,16 @@ def popitem(self): # and we would explicitly free the GPU texture here. return key, value + def clear(self): + """Clear cache without triggering eviction callbacks.""" + # Temporarily disable callback to prevent "thrashing" warnings during mass clear + callback = self.on_evict + self.on_evict = None + try: + super().clear() + finally: + self.on_evict = callback + def get_decoded_image_size(item) -> int: """Calculates the size of a decoded image tuple (buffer, qimage).""" diff --git a/faststack/imaging/editor.py b/faststack/imaging/editor.py index 15dfc62..ddeeac5 100644 --- a/faststack/imaging/editor.py +++ b/faststack/imaging/editor.py @@ -13,7 +13,7 @@ from faststack.models import DecodedImage try: from PySide6.QtGui import QImage -except Exception: +except ImportError: QImage = None import threading @@ -275,7 +275,7 @@ def load_image(self, filepath: str, cached_preview: Optional[DecodedImage] = Non return False - def _apply_edits(self, img: Image.Image, edits: Optional[Dict[str, Any]] = None, *, for_export: bool = True) -> Image.Image: + def _apply_edits(self, img: Image.Image, edits: Optional[Dict[str, Any]] = None, *, for_export: bool = False) -> Image.Image: """Applies all current edits to the provided PIL Image.""" if edits is None: @@ -353,7 +353,7 @@ def _apply_edits(self, img: Image.Image, edits: Optional[Dict[str, Any]] = None, if abs(blacks) > 0.001 or abs(whites) > 0.001: arr = np.array(img, dtype=np.float32) black_point = -blacks * 40 - white_point = 255 + whites * 40 + white_point = 255 - whites * 40 # Prevent division by zero if abs(white_point - black_point) < 0.001: white_point = black_point + 0.001 @@ -520,8 +520,8 @@ def auto_levels(self, threshold_percent: float = 0.1) -> Tuple[float, float]: blacks = -float(p_low) / 40.0 # We want white_point to be p_high - # p_high = 255 + whites * 40 => whites = (float(p_high) - 255) / 40.0 - whites = (float(p_high) - 255.0) / 40.0 + # p_high = 255 - whites * 40 => whites = (255.0 - float(p_high)) / 40.0 + whites = (255.0 - float(p_high)) / 40.0 # Update state with self._lock: diff --git a/faststack/imaging/prefetch.py b/faststack/imaging/prefetch.py index 722fd71..a43d8c9 100644 --- a/faststack/imaging/prefetch.py +++ b/faststack/imaging/prefetch.py @@ -14,7 +14,7 @@ try: from PySide6.QtCore import QTimer from PySide6.QtGui import QImage -except Exception: +except ImportError: QTimer = None QImage = None diff --git a/faststack/logging_setup.py b/faststack/logging_setup.py index 74356a2..58cad4c 100644 --- a/faststack/logging_setup.py +++ b/faststack/logging_setup.py @@ -16,7 +16,7 @@ def setup_logging(debug: bool = False): """Sets up logging to a rotating file in the app data directory. Args: - debug: If True, sets log level to DEBUG. Otherwise, sets to INFO to reduce noise. + debug: If True, sets log level to DEBUG. Otherwise, sets to WARNING to reduce noise. """ log_dir = get_app_data_dir() / "logs" log_dir.mkdir(parents=True, exist_ok=True) diff --git a/faststack/qml/ImageEditorDialog.qml b/faststack/qml/ImageEditorDialog.qml index 75b7fa0..0ec7880 100644 --- a/faststack/qml/ImageEditorDialog.qml +++ b/faststack/qml/ImageEditorDialog.qml @@ -449,6 +449,8 @@ Window { controller.set_edit_parameter(model.key, 0.0) imageEditorDialog.updatePulse++ value = 0.0 + _pendingValue = 0.0 + slider._lastSentValue = 0.0 } lastPressTime = now lastPressValue = value diff --git a/faststack/qml/Main.qml b/faststack/qml/Main.qml index 73f86fe..95dd106 100644 --- a/faststack/qml/Main.qml +++ b/faststack/qml/Main.qml @@ -22,7 +22,7 @@ ApplicationWindow { Material.accent: "#4fb360" property bool isDarkTheme: uiState ? uiState.theme === 0 : true - property color currentBackgroundColor: isDarkTheme ? "#2b2b2b" : "#ffffff" + property color currentBackgroundColor: isDarkTheme ? "#000000" : "#ffffff" property color currentTextColor: isDarkTheme ? "white" : "black" property color hoverColor: isDarkTheme ? Qt.lighter(currentBackgroundColor, 1.5) : Qt.darker(currentBackgroundColor, 1.1) diff --git a/faststack/tests/test_new_features.py b/faststack/tests/test_new_features.py index dc6be7d..a2ddcc3 100644 --- a/faststack/tests/test_new_features.py +++ b/faststack/tests/test_new_features.py @@ -30,7 +30,7 @@ def test_auto_levels_strength(self): self.assertNotEqual(blacks, 0.0) self.assertNotEqual(whites, 0.0) self.assertLess(blacks, 0.0) - self.assertLess(whites, 0.0) + self.assertGreater(whites, 0.0) # Mock strength application matching app.py logic strength = 0.5 @@ -76,7 +76,7 @@ def test_straighten_angle(self): self.editor.current_edits['straighten_angle'] = 45.0 # Apply - res = self.editor._apply_edits(self.img.copy()) + res = self.editor._apply_edits(self.img.copy(), for_export=True) # Image should be rotated and larger (expand=True) # Original width 256. 45 deg rotation of valid rect makes it wider? diff --git a/faststack/tests/test_sidecar.py b/faststack/tests/test_sidecar.py index 901551c..ea2862d 100644 --- a/faststack/tests/test_sidecar.py +++ b/faststack/tests/test_sidecar.py @@ -38,8 +38,6 @@ def test_sidecar_load_existing(mock_sidecar_dir): d = mock_sidecar_dir(content) sm = SidecarManager(d, None) - assert sm.data.last_index == 42 - assert len(sm.data.entries) == 2 assert sm.data.last_index == 42 assert len(sm.data.entries) == 2 diff --git a/pyproject.toml b/pyproject.toml index 62bf411..ad86184 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ ] [project.optional-dependencies] + dev = [ "pytest>=8.0,<9.0", ] @@ -41,6 +42,6 @@ packages = ["faststack"] [tool.pytest.ini_options] testpaths = ["faststack/tests"] python_files = ["test_*.py"] -addopts = "-p no:cacheprovider -p no:doctest" +addopts = "-p no:cacheprovider -p no:doctest --basetemp=./var/pytest-temp" norecursedirs = ["var", ".venv", "cache", "faststack.egg-info", "__pycache__"] diff --git a/requirements.txt b/requirements.txt index 975c930..4016687 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,3 @@ cachetools==5.* watchdog==4.* Pillow==10.* # fallback decode; keep it - -[project.optional-dependencies] -gui = ["PySide6>=6.0,<7.0"] -dev = ["pytest>=8.0,<9.0"] diff --git a/tools/reproduce_issue.py b/tools/reproduce_issue.py deleted file mode 100644 index 899b7a3..0000000 --- a/tools/reproduce_issue.py +++ /dev/null @@ -1,39 +0,0 @@ - -import logging -import sys -import os - -# Mock the parts of the app needed for setup_logging -# We need to make sure we can import faststack modules -sys.path.append(os.getcwd()) - -from faststack.logging_setup import setup_logging - -def test_logging(debug_mode): - print(f"\n--- Testing with debug={debug_mode} ---") - # Reset logging - root = logging.getLogger() - for h in root.handlers[:]: - root.removeHandler(h) - h.close() - - setup_logging(debug=debug_mode) - - logger = logging.getLogger("test_logger") - - # We want to capture stderr/stdout to check if it printed - # But for a simple script run by the agent, just seeing the output is enough - # or we can check the effective level - - effective_level = logger.getEffectiveLevel() - print(f"Effective level: {logging.getLevelName(effective_level)}") - - if logger.isEnabledFor(logging.INFO): - print("INFO logs are ENABLED") - else: - print("INFO logs are DISABLED") - -if __name__ == "__main__": - print("Reproduction Script Starting...") - test_logging(debug_mode=False) - test_logging(debug_mode=True)