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
15 changes: 7 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ __pycache__/
dist/
build/
*.spec
faststack.egg-info/

# Logs
*.log
Expand All @@ -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/
8 changes: 8 additions & 0 deletions faststack/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?)
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions faststack/imaging/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."""
Expand Down
10 changes: 5 additions & 5 deletions faststack/imaging/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from faststack.models import DecodedImage
try:
from PySide6.QtGui import QImage
except Exception:
except ImportError:
QImage = None

import threading
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion faststack/imaging/prefetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
try:
from PySide6.QtCore import QTimer
from PySide6.QtGui import QImage
except Exception:
except ImportError:
QTimer = None
QImage = None

Expand Down
2 changes: 1 addition & 1 deletion faststack/logging_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions faststack/qml/ImageEditorDialog.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion faststack/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions faststack/tests/test_new_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand Down
2 changes: 0 additions & 2 deletions faststack/tests/test_sidecar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies = [
]

[project.optional-dependencies]

dev = [
"pytest>=8.0,<9.0",
]
Expand All @@ -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__"]

4 changes: 0 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
39 changes: 0 additions & 39 deletions tools/reproduce_issue.py

This file was deleted.