From 64ec05aad0d0377a661ee4a81e3f73a2ece71e9d Mon Sep 17 00:00:00 2001 From: AlanRockefeller Date: Mon, 3 Nov 2025 03:04:10 -0800 Subject: [PATCH] =?UTF-8?q?Release=20v0.6=20=E2=80=94=20more=20improvement?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- faststack/ChangeLog.md | 10 ++++ faststack/faststack/app.py | 67 +++++++++++++++++++++----- faststack/faststack/imaging/jpeg.py | 12 ++++- faststack/faststack/qml/Components.qml | 4 +- faststack/pyproject.toml | 2 +- 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/faststack/ChangeLog.md b/faststack/ChangeLog.md index 54ea029..dd070ad 100644 --- a/faststack/ChangeLog.md +++ b/faststack/ChangeLog.md @@ -1,5 +1,15 @@ # ChangeLog +## [0.6.0] - 2025-11-03 + +### Fixed +- Resolved an issue where the prefetch range was not being applied correctly after changing the prefetch radius in settings. +- Corrected `decode_jpeg_thumb_rgb` to ensure that thumbnails generated by PyTurboJPEG do not exceed the `max_dim` by falling back to Pillow resizing when necessary. +- Addressed excessive metadata queries during application startup by deferring UI synchronization until after images are loaded. +- Fixed a bug where the zoom state callback was not firing, leading to low-resolution images being served when zoomed in. +- Resolved a QML error "Cannot assign to non-existent property 'scaleTransform'" by correctly placing the scale change handlers within the `Scale` transform. +- Handled the empty image files case in preloading to prevent unnecessary processing and correctly update the UI. + ## [0.5.0] - 2025-11-03 ### Added diff --git a/faststack/faststack/app.py b/faststack/faststack/app.py index 8526463..a4d7f84 100644 --- a/faststack/faststack/app.py +++ b/faststack/faststack/app.py @@ -72,21 +72,38 @@ def __init__(self, image_dir: Path, engine: QQmlApplicationEngine): self.stacks: List[List[int]] = [] self.selected_raws: set[Path] = set() + self._metadata_cache = {} + self._metadata_cache_index = -1 + + self.resize_timer = QTimer() + self.resize_timer.setSingleShot(True) + self.resize_timer.timeout.connect(self._handle_resize) + self.pending_width = None + self.pending_height = None + def get_display_info(self): if self.is_zoomed: return 0, 0, self.display_generation return self.display_width, self.display_height, self.display_generation def on_display_size_changed(self, width: int, height: int): + """Debounces display size change events to prevent spamming resizes.""" if self.display_width == width and self.display_height == height: - return # No change - - log.info(f"Display size changed to: {width}x{height}") - self.display_width = width - self.display_height = height + return + + # Debounce resize events + self.pending_width = width + self.pending_height = height + self.resize_timer.start(150) # 150ms debounce + + def _handle_resize(self): + """Actual resize handler, called after debounce period.""" + log.info(f"Display size changed to: {self.pending_width}x{self.pending_height}") + self.display_width = self.pending_width + self.display_height = self.pending_height self.display_generation += 1 self.image_cache.clear() - self.prefetcher.cancel_all() # Clear existing prefetch tasks + self.prefetcher.cancel_all() self.prefetcher.update_prefetch(self.current_index) self.sync_ui_state() # To refresh the image @@ -119,6 +136,8 @@ def load(self): self.stacks = self.sidecar.data.stacks # Load stacks from sidecar self.watcher.start() self.prefetcher.update_prefetch(self.current_index) + + # Defer initial UI sync until after images are loaded self.sync_ui_state() @@ -126,6 +145,7 @@ def refresh_image_list(self): """Rescans the directory for images.""" self.image_files = find_images(self.image_dir) self.prefetcher.set_image_files(self.image_files) + self._metadata_cache_index = (-1, -1) # Invalidate cache self.ui_state.imageCountChanged.emit() def get_decoded_image(self, index: int) -> Optional[DecodedImage]: @@ -161,6 +181,7 @@ def get_decoded_image(self, index: int) -> Optional[DecodedImage]: def sync_ui_state(self): """Forces the UI to update by emitting all state change signals.""" self.ui_refresh_generation += 1 + self._metadata_cache_index = (-1, -1) # Invalidate cache self.ui_state.currentIndexChanged.emit() self.ui_state.currentImageSourceChanged.emit() self.ui_state.metadataChanged.emit() @@ -189,25 +210,33 @@ def get_current_metadata(self) -> Dict: log.debug("get_current_metadata: image_files is empty, returning {}.") return {} + # Cache hit check + cache_key = (self.current_index, self.ui_refresh_generation) + if cache_key == self._metadata_cache_index: + return self._metadata_cache + + # Compute and cache stem = self.image_files[self.current_index].path.stem meta = self.sidecar.get_metadata(stem) - stack_info = self._get_stack_info(self.current_index) - - return { + + self._metadata_cache = { "filename": self.image_files[self.current_index].path.name, "flag": meta.flag, "reject": meta.reject, - "stack_info_text": stack_info, "stacked": meta.stacked, - "stacked_date": meta.stacked_date, + "stacked_date": meta.stacked_date or "", + "stack_info_text": stack_info } + self._metadata_cache_index = cache_key + return self._metadata_cache def toggle_current_flag(self): stem = self.image_files[self.current_index].path.stem meta = self.sidecar.get_metadata(stem) meta.flag = not meta.flag self.sidecar.save() + self._metadata_cache_index = (-1, -1) # Invalidate cache self.ui_state.metadataChanged.emit() def toggle_current_reject(self): @@ -215,11 +244,13 @@ def toggle_current_reject(self): meta = self.sidecar.get_metadata(stem) meta.reject = not meta.reject self.sidecar.save() + self._metadata_cache_index = (-1, -1) # Invalidate cache self.ui_state.metadataChanged.emit() def begin_new_stack(self): self.stack_start_index = self.current_index log.info(f"Stack start marked at index {self.stack_start_index}") + self._metadata_cache_index = (-1, -1) # Invalidate cache self.ui_state.metadataChanged.emit() # Update UI to show start marker def end_current_stack(self): @@ -233,6 +264,7 @@ def end_current_stack(self): self.sidecar.save() log.info(f"Defined new stack: [{start}, {end}]") self.stack_start_index = None + self._metadata_cache_index = (-1, -1) # Invalidate cache self.ui_state.metadataChanged.emit() else: log.warning("No stack start marked. Press '[' first.") @@ -305,6 +337,7 @@ def _launch_helicon_with_files(self, raw_files: List[Path]): meta.stacked_date = today break self.sidecar.save() + self._metadata_cache_index = (-1, -1) # Invalidate cache def _delete_temp_file(self, tmp_path: Path): if tmp_path.exists(): @@ -319,6 +352,7 @@ def clear_all_stacks(self): self.stacks = [] self.sidecar.data.stacks = self.stacks self.sidecar.save() + self._metadata_cache_index = (-1, -1) # Invalidate cache self.ui_state.metadataChanged.emit() # Refresh UI to show no stacks def get_helicon_path(self): @@ -352,6 +386,8 @@ def get_prefetch_radius(self): def set_prefetch_radius(self, radius): config.set('core', 'prefetch_radius', radius) config.save() + self.prefetcher.prefetch_radius = radius + self.prefetcher.update_prefetch(self.current_index) def get_theme(self): return 0 if config.get('core', 'theme') == 'dark' else 1 @@ -391,9 +427,16 @@ def preload_all_images(self): # Use existing prefetch executor (better resource utilization) total = len(self.image_files) + + if total == 0: + log.info("No images to preload.") + self.reporter.progress_updated.emit(100) # Or 0, depending on desired UX + self.reporter.finished.emit() + return + completed = 0 - def _on_done(future): + def _on_done(_future): nonlocal completed completed += 1 progress = int((completed / total) * 100) diff --git a/faststack/faststack/imaging/jpeg.py b/faststack/faststack/imaging/jpeg.py index a8faf21..049e884 100644 --- a/faststack/faststack/imaging/jpeg.py +++ b/faststack/faststack/imaging/jpeg.py @@ -51,7 +51,17 @@ def decode_jpeg_thumb_rgb( # Find the best scaling factor scaling_factor = _get_turbojpeg_scaling_factor(width, height, max_dim) - return jpeg_decoder.decode(jpeg_bytes, scaling_factor=scaling_factor, pixel_format=TJPF_RGB, flags=TJFLAG_FASTDCT) + decoded = jpeg_decoder.decode( + jpeg_bytes, + scaling_factor=scaling_factor, + pixel_format=TJPF_RGB, + flags=TJFLAG_FASTDCT, + ) + if decoded.shape[0] > max_dim or decoded.shape[1] > max_dim: + img = Image.fromarray(decoded) + img.thumbnail((max_dim, max_dim), Image.Resampling.LANCZOS) + return np.array(img) + return decoded except Exception as e: log.exception(f"PyTurboJPEG failed to decode thumbnail: {e}. Trying Pillow.") diff --git a/faststack/faststack/qml/Components.qml b/faststack/faststack/qml/Components.qml index 7e9f381..3edd2c8 100644 --- a/faststack/faststack/qml/Components.qml +++ b/faststack/faststack/qml/Components.qml @@ -33,7 +33,7 @@ Item { } } - onScaleChanged: { + function updateZoomState() { if (scaleTransform.xScale > 1.1 && !uiState.isZoomed) { uiState.setZoomed(true); } else if (scaleTransform.xScale <= 1.0 && uiState.isZoomed) { @@ -46,6 +46,8 @@ Item { id: scaleTransform origin.x: mainImage.width / 2 origin.y: mainImage.height / 2 + onXScaleChanged: mainImage.updateZoomState() + onYScaleChanged: mainImage.updateZoomState() }, Translate { id: panTransform diff --git a/faststack/pyproject.toml b/faststack/pyproject.toml index 4000db7..435bca8 100644 --- a/faststack/pyproject.toml +++ b/faststack/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "faststack" -version = "0.5" +version = "0.6" authors = [ { name="Alan Rockefeller", email="alanrockefeller@gmail.com" }, ]