From 7f095ad7b8d0aa069d3691d12a114345bc226e16 Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Fri, 2 Jan 2026 18:25:52 -0800 Subject: [PATCH 1/3] Improved histogram updating --- .gitignore | 15 ++- faststack.egg-info/PKG-INFO | 99 ++++++++++++++++++ faststack.egg-info/SOURCES.txt | 15 +++ faststack.egg-info/dependency_links.txt | 1 + faststack.egg-info/entry_points.txt | 2 + faststack.egg-info/requires.txt | 12 +++ faststack.egg-info/top_level.txt | 1 + faststack/app.py | 3 + faststack/imaging/cache.py | 10 ++ faststack/imaging/editor.py | 10 +- faststack/imaging/prefetch.py | 2 +- faststack/logging_setup.py | 2 +- faststack/qml/ImageEditorDialog.qml | 2 + faststack/qml/Main.qml | 2 +- faststack/tests/test_new_features.py | 4 +- faststack/tests/test_sidecar.py | 2 - pyproject.toml | 5 +- requirements.txt | 4 - tools/reproduce_issue.py | 5 +- .../test_find_images0/IMG_0001.CR3 | 0 .../test_find_images0/IMG_0001.JPG | 0 .../test_find_images0/IMG_0002.CR3 | 0 .../test_find_images0/IMG_0002.jpg | 0 .../test_find_images0/IMG_0003.jpeg | 0 .../test_find_images0/IMG_0004.CR3 | 0 .../sample-backup.jpg | Bin 0 -> 632 bytes .../sample.jpg | Bin 0 -> 633 bytes .../faststack.json | 1 + .../test_sidecar_save0/faststack.json | 18 ++++ var/reproduction_output.txt | Bin 0 -> 404 bytes var/test_output.txt | Bin 0 -> 788 bytes 31 files changed, 188 insertions(+), 27 deletions(-) create mode 100644 faststack.egg-info/PKG-INFO create mode 100644 faststack.egg-info/SOURCES.txt create mode 100644 faststack.egg-info/dependency_links.txt create mode 100644 faststack.egg-info/entry_points.txt create mode 100644 faststack.egg-info/requires.txt create mode 100644 faststack.egg-info/top_level.txt create mode 100644 var/pytest-temp/test_find_images0/IMG_0001.CR3 create mode 100644 var/pytest-temp/test_find_images0/IMG_0001.JPG create mode 100644 var/pytest-temp/test_find_images0/IMG_0002.CR3 create mode 100644 var/pytest-temp/test_find_images0/IMG_0002.jpg create mode 100644 var/pytest-temp/test_find_images0/IMG_0003.jpeg create mode 100644 var/pytest-temp/test_find_images0/IMG_0004.CR3 create mode 100644 var/pytest-temp/test_save_image_preserves_mtim0/sample-backup.jpg create mode 100644 var/pytest-temp/test_save_image_preserves_mtim0/sample.jpg create mode 100644 var/pytest-temp/test_sidecar_load_existing0/faststack.json create mode 100644 var/pytest-temp/test_sidecar_save0/faststack.json create mode 100644 var/reproduction_output.txt create mode 100644 var/test_output.txt diff --git a/.gitignore b/.gitignore index 9a66800..0c18c3c 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.egg-info/PKG-INFO b/faststack.egg-info/PKG-INFO new file mode 100644 index 0000000..d4c1274 --- /dev/null +++ b/faststack.egg-info/PKG-INFO @@ -0,0 +1,99 @@ +Metadata-Version: 2.4 +Name: faststack +Version: 1.4 +Summary: Ultra-fast JPG Viewer for Focus Stacking Selection +Author: Alan Rockefeller +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: Microsoft :: Windows +Requires-Python: >=3.11 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: PySide6<7.0,>=6.0 +Requires-Dist: PyTurboJPEG<2.0,>=1.8 +Requires-Dist: numpy<3.0,>=2.0 +Requires-Dist: cachetools<6.0,>=5.0 +Requires-Dist: watchdog<5.0,>=4.0 +Requires-Dist: Pillow<11.0,>=10.0 +Provides-Extra: gui +Requires-Dist: PySide6<7.0,>=6.0; extra == "gui" +Provides-Extra: dev +Requires-Dist: pytest<9.0,>=8.0; extra == "dev" +Dynamic: license-file + +# FastStack + +# Version 1.5 - January 1, 2026 +# By Alan Rockefeller + +Ultra-fast, caching JPG viewer designed for culling and selecting RAW or JPG files for focus stacking and website upload. + +This tool is optimized for speed, using `libjpeg-turbo` for decoding, aggressive prefetching, and byte-aware LRU caches to provide a fluid experience when reviewing thousands of images. + +## Features + +- **Crop:** Added the ability to crop and rotate images via the cr(O)p hotkey (or right mouse click). It can be a freeform crop, or constrained to several popular aspect ratios. +- **Zoom & Pan:** Smooth zooming and panning. +- **Stack Selection:** Group images into stacks (`[`, `]`) and select them for processing (`S`). +- **Helicon Focus Integration:** Launch Helicon Focus with your selected RAW files with a single keypress (`Enter`). +- **Instant Navigation:** Sub-10ms next/previous image switching, high performance decoding via `PyTurboJPEG`. +- **Image Editor:** Built-in editor with exposure, contrast, white balance, sharpness, and more (E key) +- **Quick Auto White Balance:** Press A to apply auto white balance and save automatically with undo support (Ctrl+Z). For better white balance, load the raw into Photoshop with the P key.- **Photoshop Integration:** Edit current image in Photoshop (P key) - always uses RAW files when available. +- **Clipboard Support:** Copy image path to clipboard (Ctrl+C) +- **Image Filtering:** Filter images by filename +- **Drag & Drop:** Drag images to external applications. Press { and } to batch files to drag & drop multiple images. +- **Theme Support:** Toggle between light and dark themes +- **Delete & Undo:** Move images to recycle bin (Delete/Backspace) with undo support (Ctrl+Z) +- **Has Memory:** Starts where you left off, tells you which images have been edited, stacked and uploaded. +- **RAW Pairing:** Automatically maps JPGs to their corresponding RAW files (`.CR3`, `.ARW`, `.NEF`, etc.). +- **Configurable:** Adjust cache sizes, prefetch behavior, and Helicon Focus / Photoshop paths via a settings dialog and a persistent `.ini` file. +- **Accurate Colors:** Uses monitor ICC profile to display colors correctly. +- **RGB Histogram:** Pressing H brings up a RGB histogram which is designed to show even a little bit of highlight clipping and updates as you zoom in. + +## Installation & Usage + +1. **Install Dependencies:** + ```bash + pip install -r requirements.txt + ``` + +2. **Run the App:** + ```bash + python -m faststack.app "C:\path\to\your\images" + ``` + +## Keyboard Shortcuts + +- `J` / `Right Arrow`: Next Image +- `K` / `Left Arrow`: Previous Image +- `G`: Jump to Image Number +- `I`: Show EXIF Data +- `S`: Toggle current image in/out of stack +- `X`: Remove current image from batch/stack +- `B`: Toggle current image in/out of batch +- `[`: Begin new stack group +- `]`: End current stack group +- `C`: Clear all stacks +- `{`: Begin new drag & drop batch +- `}`: End current drag & drop batch +- `\`: Clear drag & drop batch +- `U`: Toggle uploaded flag +- `Ctrl+E`: Toggle edited flag +- `Ctrl+S`: Toggle stacked flag +- `Enter`: Launch Helicon Focus with selected RAWs +- `P`: Edit in Photoshop (uses RAW file if available) +- `O` (or Right-Click): Toggle crop mode (Enter to execute, Esc to cancel) +- `Delete` / `Backspace`: Move image to recycle bin +- `Ctrl+Z`: Undo last action (delete, auto white balance, or crop) +- `A`: Quick auto white balance (saves automatically) +- `Ctrl+Shift+B`: Quick auto white balance (alternate) +- `L`: Quick auto levels (saves automatically) +- `E`: Toggle Image Editor +- `Esc`: Close active dialog, editor, or cancel crop +- `H`: Toggle histogram window +- `Ctrl+C`: Copy image path to clipboard +- `Ctrl+0`: Reset zoom and pan to fit window +- `Ctrl+1`: Zoom to 100% +- `Ctrl+2`: Zoom to 200% +- `Ctrl+3`: Zoom to 300% +- `Ctrl+4`: Zoom to 400% diff --git a/faststack.egg-info/SOURCES.txt b/faststack.egg-info/SOURCES.txt new file mode 100644 index 0000000..aa262fd --- /dev/null +++ b/faststack.egg-info/SOURCES.txt @@ -0,0 +1,15 @@ +LICENSE +README.md +pyproject.toml +faststack/__init__.py +faststack/app.py +faststack/config.py +faststack/logging_setup.py +faststack/models.py +faststack/verify_wb.py +faststack.egg-info/PKG-INFO +faststack.egg-info/SOURCES.txt +faststack.egg-info/dependency_links.txt +faststack.egg-info/entry_points.txt +faststack.egg-info/requires.txt +faststack.egg-info/top_level.txt \ No newline at end of file diff --git a/faststack.egg-info/dependency_links.txt b/faststack.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/faststack.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/faststack.egg-info/entry_points.txt b/faststack.egg-info/entry_points.txt new file mode 100644 index 0000000..e2facd6 --- /dev/null +++ b/faststack.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +faststack = faststack.app:cli diff --git a/faststack.egg-info/requires.txt b/faststack.egg-info/requires.txt new file mode 100644 index 0000000..e199e8f --- /dev/null +++ b/faststack.egg-info/requires.txt @@ -0,0 +1,12 @@ +PySide6<7.0,>=6.0 +PyTurboJPEG<2.0,>=1.8 +numpy<3.0,>=2.0 +cachetools<6.0,>=5.0 +watchdog<5.0,>=4.0 +Pillow<11.0,>=10.0 + +[dev] +pytest<9.0,>=8.0 + +[gui] +PySide6<7.0,>=6.0 diff --git a/faststack.egg-info/top_level.txt b/faststack.egg-info/top_level.txt new file mode 100644 index 0000000..81352aa --- /dev/null +++ b/faststack.egg-info/top_level.txt @@ -0,0 +1 @@ +faststack diff --git a/faststack/app.py b/faststack/app.py index e97ff70..70a3f78 100644 --- a/faststack/app.py +++ b/faststack/app.py @@ -2826,6 +2826,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..6e424c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,9 @@ dependencies = [ ] [project.optional-dependencies] +gui = [ + "PySide6>=6.0,<7.0", +] dev = [ "pytest>=8.0,<9.0", ] @@ -41,6 +44,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 index 899b7a3..270a80b 100644 --- a/tools/reproduce_issue.py +++ b/tools/reproduce_issue.py @@ -1,11 +1,12 @@ import logging import sys -import os + +log = logging.getLogger(__name__) # 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()) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from faststack.logging_setup import setup_logging diff --git a/var/pytest-temp/test_find_images0/IMG_0001.CR3 b/var/pytest-temp/test_find_images0/IMG_0001.CR3 new file mode 100644 index 0000000..e69de29 diff --git a/var/pytest-temp/test_find_images0/IMG_0001.JPG b/var/pytest-temp/test_find_images0/IMG_0001.JPG new file mode 100644 index 0000000..e69de29 diff --git a/var/pytest-temp/test_find_images0/IMG_0002.CR3 b/var/pytest-temp/test_find_images0/IMG_0002.CR3 new file mode 100644 index 0000000..e69de29 diff --git a/var/pytest-temp/test_find_images0/IMG_0002.jpg b/var/pytest-temp/test_find_images0/IMG_0002.jpg new file mode 100644 index 0000000..e69de29 diff --git a/var/pytest-temp/test_find_images0/IMG_0003.jpeg b/var/pytest-temp/test_find_images0/IMG_0003.jpeg new file mode 100644 index 0000000..e69de29 diff --git a/var/pytest-temp/test_find_images0/IMG_0004.CR3 b/var/pytest-temp/test_find_images0/IMG_0004.CR3 new file mode 100644 index 0000000..e69de29 diff --git a/var/pytest-temp/test_save_image_preserves_mtim0/sample-backup.jpg b/var/pytest-temp/test_save_image_preserves_mtim0/sample-backup.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c981cab1ef9af59ae43650277f88af8d009454e2 GIT binary patch literal 632 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!$&ENMca(*|K9`vy5Y_X literal 0 HcmV?d00001 diff --git a/var/pytest-temp/test_save_image_preserves_mtim0/sample.jpg b/var/pytest-temp/test_save_image_preserves_mtim0/sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c023c192d52346f637ef668b52d136a73da70eea GIT binary patch literal 633 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO`~r0}C^w5(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA3g+0lH%rKp*-R#r=Odw@ zrr|Iy?D)Ckr3bEst}?D`cYV;@5XxE%Q~WKz3+IflE~Eho+A>g=_lWJ)Y0 zD_)`h_oUN^I4X*dhnw$Oy{@uM#N9F@VZ}l@_j_FxvsTZ_btz5bT{`7Zyi^kbOKFFm Z-`{Jdn>+8NkJC^a12%uSjC8@T#}|9+Kal_c literal 0 HcmV?d00001 diff --git a/var/test_output.txt b/var/test_output.txt new file mode 100644 index 0000000000000000000000000000000000000000..ac13cb85de2b747d4f00a6ccef245e91c39c1bba GIT binary patch literal 788 zcmbu7K}*9x5QX1a@IUOun+2+-s$lT*qlFL!on>sZj*0*Av*ScHn)$Qz_S+{R9iT z*Pa_M1ADGaGx8aJNw!JZgVu>|h+pCJAs6+oXKWABjQBM^P0THLHMnc|THS(l$GT8S zbWO(#G_B~+UAo^=Zzp5!!Zj85w=j8))Em*-8qjOK5-GvS@R~kj(%`z7ZacSuH_$-< z!KWR}Mz^2Ra{NObx{mv0LUFy{bD6lUz6aA9v!p5T{cQo(zu5!tImIsY_@i?CEEGfM XO?q=roZrHe5a96t%>OxUhk5%3<+^g* literal 0 HcmV?d00001 From 6d75df68b2a73b11660258e264eeed6d1f23dff2 Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Fri, 2 Jan 2026 18:27:51 -0800 Subject: [PATCH 2/3] rm files --- .gitignore | 2 +- faststack.egg-info/PKG-INFO | 99 ------------------ faststack.egg-info/SOURCES.txt | 15 --- faststack.egg-info/dependency_links.txt | 1 - faststack.egg-info/entry_points.txt | 2 - faststack.egg-info/requires.txt | 12 --- faststack.egg-info/top_level.txt | 1 - .../test_find_images0/IMG_0001.CR3 | 0 .../test_find_images0/IMG_0001.JPG | 0 .../test_find_images0/IMG_0002.CR3 | 0 .../test_find_images0/IMG_0002.jpg | 0 .../test_find_images0/IMG_0003.jpeg | 0 .../test_find_images0/IMG_0004.CR3 | 0 .../sample-backup.jpg | Bin 632 -> 0 bytes .../sample.jpg | Bin 633 -> 0 bytes .../faststack.json | 1 - .../test_sidecar_save0/faststack.json | 18 ---- var/reproduction_output.txt | Bin 404 -> 0 bytes var/test_output.txt | Bin 788 -> 0 bytes 19 files changed, 1 insertion(+), 150 deletions(-) delete mode 100644 faststack.egg-info/PKG-INFO delete mode 100644 faststack.egg-info/SOURCES.txt delete mode 100644 faststack.egg-info/dependency_links.txt delete mode 100644 faststack.egg-info/entry_points.txt delete mode 100644 faststack.egg-info/requires.txt delete mode 100644 faststack.egg-info/top_level.txt delete mode 100644 var/pytest-temp/test_find_images0/IMG_0001.CR3 delete mode 100644 var/pytest-temp/test_find_images0/IMG_0001.JPG delete mode 100644 var/pytest-temp/test_find_images0/IMG_0002.CR3 delete mode 100644 var/pytest-temp/test_find_images0/IMG_0002.jpg delete mode 100644 var/pytest-temp/test_find_images0/IMG_0003.jpeg delete mode 100644 var/pytest-temp/test_find_images0/IMG_0004.CR3 delete mode 100644 var/pytest-temp/test_save_image_preserves_mtim0/sample-backup.jpg delete mode 100644 var/pytest-temp/test_save_image_preserves_mtim0/sample.jpg delete mode 100644 var/pytest-temp/test_sidecar_load_existing0/faststack.json delete mode 100644 var/pytest-temp/test_sidecar_save0/faststack.json delete mode 100644 var/reproduction_output.txt delete mode 100644 var/test_output.txt diff --git a/.gitignore b/.gitignore index 0c18c3c..f07a847 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ __pycache__/ dist/ build/ *.spec -+faststack.egg-info/ +faststack.egg-info/ # Logs *.log diff --git a/faststack.egg-info/PKG-INFO b/faststack.egg-info/PKG-INFO deleted file mode 100644 index d4c1274..0000000 --- a/faststack.egg-info/PKG-INFO +++ /dev/null @@ -1,99 +0,0 @@ -Metadata-Version: 2.4 -Name: faststack -Version: 1.4 -Summary: Ultra-fast JPG Viewer for Focus Stacking Selection -Author: Alan Rockefeller -Classifier: Programming Language :: Python :: 3 -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: Microsoft :: Windows -Requires-Python: >=3.11 -Description-Content-Type: text/markdown -License-File: LICENSE -Requires-Dist: PySide6<7.0,>=6.0 -Requires-Dist: PyTurboJPEG<2.0,>=1.8 -Requires-Dist: numpy<3.0,>=2.0 -Requires-Dist: cachetools<6.0,>=5.0 -Requires-Dist: watchdog<5.0,>=4.0 -Requires-Dist: Pillow<11.0,>=10.0 -Provides-Extra: gui -Requires-Dist: PySide6<7.0,>=6.0; extra == "gui" -Provides-Extra: dev -Requires-Dist: pytest<9.0,>=8.0; extra == "dev" -Dynamic: license-file - -# FastStack - -# Version 1.5 - January 1, 2026 -# By Alan Rockefeller - -Ultra-fast, caching JPG viewer designed for culling and selecting RAW or JPG files for focus stacking and website upload. - -This tool is optimized for speed, using `libjpeg-turbo` for decoding, aggressive prefetching, and byte-aware LRU caches to provide a fluid experience when reviewing thousands of images. - -## Features - -- **Crop:** Added the ability to crop and rotate images via the cr(O)p hotkey (or right mouse click). It can be a freeform crop, or constrained to several popular aspect ratios. -- **Zoom & Pan:** Smooth zooming and panning. -- **Stack Selection:** Group images into stacks (`[`, `]`) and select them for processing (`S`). -- **Helicon Focus Integration:** Launch Helicon Focus with your selected RAW files with a single keypress (`Enter`). -- **Instant Navigation:** Sub-10ms next/previous image switching, high performance decoding via `PyTurboJPEG`. -- **Image Editor:** Built-in editor with exposure, contrast, white balance, sharpness, and more (E key) -- **Quick Auto White Balance:** Press A to apply auto white balance and save automatically with undo support (Ctrl+Z). For better white balance, load the raw into Photoshop with the P key.- **Photoshop Integration:** Edit current image in Photoshop (P key) - always uses RAW files when available. -- **Clipboard Support:** Copy image path to clipboard (Ctrl+C) -- **Image Filtering:** Filter images by filename -- **Drag & Drop:** Drag images to external applications. Press { and } to batch files to drag & drop multiple images. -- **Theme Support:** Toggle between light and dark themes -- **Delete & Undo:** Move images to recycle bin (Delete/Backspace) with undo support (Ctrl+Z) -- **Has Memory:** Starts where you left off, tells you which images have been edited, stacked and uploaded. -- **RAW Pairing:** Automatically maps JPGs to their corresponding RAW files (`.CR3`, `.ARW`, `.NEF`, etc.). -- **Configurable:** Adjust cache sizes, prefetch behavior, and Helicon Focus / Photoshop paths via a settings dialog and a persistent `.ini` file. -- **Accurate Colors:** Uses monitor ICC profile to display colors correctly. -- **RGB Histogram:** Pressing H brings up a RGB histogram which is designed to show even a little bit of highlight clipping and updates as you zoom in. - -## Installation & Usage - -1. **Install Dependencies:** - ```bash - pip install -r requirements.txt - ``` - -2. **Run the App:** - ```bash - python -m faststack.app "C:\path\to\your\images" - ``` - -## Keyboard Shortcuts - -- `J` / `Right Arrow`: Next Image -- `K` / `Left Arrow`: Previous Image -- `G`: Jump to Image Number -- `I`: Show EXIF Data -- `S`: Toggle current image in/out of stack -- `X`: Remove current image from batch/stack -- `B`: Toggle current image in/out of batch -- `[`: Begin new stack group -- `]`: End current stack group -- `C`: Clear all stacks -- `{`: Begin new drag & drop batch -- `}`: End current drag & drop batch -- `\`: Clear drag & drop batch -- `U`: Toggle uploaded flag -- `Ctrl+E`: Toggle edited flag -- `Ctrl+S`: Toggle stacked flag -- `Enter`: Launch Helicon Focus with selected RAWs -- `P`: Edit in Photoshop (uses RAW file if available) -- `O` (or Right-Click): Toggle crop mode (Enter to execute, Esc to cancel) -- `Delete` / `Backspace`: Move image to recycle bin -- `Ctrl+Z`: Undo last action (delete, auto white balance, or crop) -- `A`: Quick auto white balance (saves automatically) -- `Ctrl+Shift+B`: Quick auto white balance (alternate) -- `L`: Quick auto levels (saves automatically) -- `E`: Toggle Image Editor -- `Esc`: Close active dialog, editor, or cancel crop -- `H`: Toggle histogram window -- `Ctrl+C`: Copy image path to clipboard -- `Ctrl+0`: Reset zoom and pan to fit window -- `Ctrl+1`: Zoom to 100% -- `Ctrl+2`: Zoom to 200% -- `Ctrl+3`: Zoom to 300% -- `Ctrl+4`: Zoom to 400% diff --git a/faststack.egg-info/SOURCES.txt b/faststack.egg-info/SOURCES.txt deleted file mode 100644 index aa262fd..0000000 --- a/faststack.egg-info/SOURCES.txt +++ /dev/null @@ -1,15 +0,0 @@ -LICENSE -README.md -pyproject.toml -faststack/__init__.py -faststack/app.py -faststack/config.py -faststack/logging_setup.py -faststack/models.py -faststack/verify_wb.py -faststack.egg-info/PKG-INFO -faststack.egg-info/SOURCES.txt -faststack.egg-info/dependency_links.txt -faststack.egg-info/entry_points.txt -faststack.egg-info/requires.txt -faststack.egg-info/top_level.txt \ No newline at end of file diff --git a/faststack.egg-info/dependency_links.txt b/faststack.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/faststack.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/faststack.egg-info/entry_points.txt b/faststack.egg-info/entry_points.txt deleted file mode 100644 index e2facd6..0000000 --- a/faststack.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[console_scripts] -faststack = faststack.app:cli diff --git a/faststack.egg-info/requires.txt b/faststack.egg-info/requires.txt deleted file mode 100644 index e199e8f..0000000 --- a/faststack.egg-info/requires.txt +++ /dev/null @@ -1,12 +0,0 @@ -PySide6<7.0,>=6.0 -PyTurboJPEG<2.0,>=1.8 -numpy<3.0,>=2.0 -cachetools<6.0,>=5.0 -watchdog<5.0,>=4.0 -Pillow<11.0,>=10.0 - -[dev] -pytest<9.0,>=8.0 - -[gui] -PySide6<7.0,>=6.0 diff --git a/faststack.egg-info/top_level.txt b/faststack.egg-info/top_level.txt deleted file mode 100644 index 81352aa..0000000 --- a/faststack.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -faststack diff --git a/var/pytest-temp/test_find_images0/IMG_0001.CR3 b/var/pytest-temp/test_find_images0/IMG_0001.CR3 deleted file mode 100644 index e69de29..0000000 diff --git a/var/pytest-temp/test_find_images0/IMG_0001.JPG b/var/pytest-temp/test_find_images0/IMG_0001.JPG deleted file mode 100644 index e69de29..0000000 diff --git a/var/pytest-temp/test_find_images0/IMG_0002.CR3 b/var/pytest-temp/test_find_images0/IMG_0002.CR3 deleted file mode 100644 index e69de29..0000000 diff --git a/var/pytest-temp/test_find_images0/IMG_0002.jpg b/var/pytest-temp/test_find_images0/IMG_0002.jpg deleted file mode 100644 index e69de29..0000000 diff --git a/var/pytest-temp/test_find_images0/IMG_0003.jpeg b/var/pytest-temp/test_find_images0/IMG_0003.jpeg deleted file mode 100644 index e69de29..0000000 diff --git a/var/pytest-temp/test_find_images0/IMG_0004.CR3 b/var/pytest-temp/test_find_images0/IMG_0004.CR3 deleted file mode 100644 index e69de29..0000000 diff --git a/var/pytest-temp/test_save_image_preserves_mtim0/sample-backup.jpg b/var/pytest-temp/test_save_image_preserves_mtim0/sample-backup.jpg deleted file mode 100644 index c981cab1ef9af59ae43650277f88af8d009454e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 632 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!$&ENMca(*|K9`vy5Y_X diff --git a/var/pytest-temp/test_save_image_preserves_mtim0/sample.jpg b/var/pytest-temp/test_save_image_preserves_mtim0/sample.jpg deleted file mode 100644 index c023c192d52346f637ef668b52d136a73da70eea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 633 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAO`~r0}C^w5(ASU zBeNjm|04|YKzFi&odL?6mQqXwbzED#l4gO`Kd};u4Zls%q*Qnp!5NX66=_R?aT2 zZtfnQUcn)uVc`*xQOPN(Y3Ui6S;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8 z+42=DS8dw7W$U)>J9h3mboj{8W5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2 z`tAFVpT9u3cne5k^_L*fUreAlU=!0ld(M2vX6_bamA3g+0lH%rKp*-R#r=Odw@ zrr|Iy?D)Ckr3bEst}?D`cYV;@5XxE%Q~WKz3+IflE~Eho+A>g=_lWJ)Y0 zD_)`h_oUN^I4X*dhnw$Oy{@uM#N9F@VZ}l@_j_FxvsTZ_btz5bT{`7Zyi^kbOKFFm Z-`{Jdn>+8NkJC^a12%uSjC8@T#}|9+Kal_c diff --git a/var/test_output.txt b/var/test_output.txt deleted file mode 100644 index ac13cb85de2b747d4f00a6ccef245e91c39c1bba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 788 zcmbu7K}*9x5QX1a@IUOun+2+-s$lT*qlFL!on>sZj*0*Av*ScHn)$Qz_S+{R9iT z*Pa_M1ADGaGx8aJNw!JZgVu>|h+pCJAs6+oXKWABjQBM^P0THLHMnc|THS(l$GT8S zbWO(#G_B~+UAo^=Zzp5!!Zj85w=j8))Em*-8qjOK5-GvS@R~kj(%`z7ZacSuH_$-< z!KWR}Mz^2Ra{NObx{mv0LUFy{bD6lUz6aA9v!p5T{cQo(zu5!tImIsY_@i?CEEGfM XO?q=roZrHe5a96t%>OxUhk5%3<+^g* From 5ac176ba6a11184570d0dc63a99d509ae7aaa30f Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Fri, 2 Jan 2026 18:49:31 -0800 Subject: [PATCH 3/3] update --- .gitignore | 2 +- faststack/app.py | 5 +++++ pyproject.toml | 4 +--- tools/reproduce_issue.py | 40 ---------------------------------------- 4 files changed, 7 insertions(+), 44 deletions(-) delete mode 100644 tools/reproduce_issue.py diff --git a/.gitignore b/.gitignore index f07a847..ae93fd6 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ faststack/.mypy_cache/ .mypy_cache/ # Runtime/Data -+var/ +var/ diff --git a/faststack/app.py b/faststack/app.py index 70a3f78..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?) diff --git a/pyproject.toml b/pyproject.toml index 6e424c6..ad86184 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,9 +27,7 @@ dependencies = [ ] [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 270a80b..0000000 --- a/tools/reproduce_issue.py +++ /dev/null @@ -1,40 +0,0 @@ - -import logging -import sys - -log = logging.getLogger(__name__) - -# Mock the parts of the app needed for setup_logging -# We need to make sure we can import faststack modules -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -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)