From e70d4347737a05274644060a04b81c6a227f0e90 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:44:50 -0600 Subject: [PATCH] fix(theme): bound auto-theme image decode, spinner via event loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups: extract_palette decoded the full image before the 100x100 downsample with no limits — a decompression-bomb PNG expands to tens of GB, and this box hard-freezes on OOM; decode is now bounded at 12k x 12k / 512 MB (any real wallpaper fits). The generating indicator is now set through upgrade_in_event_loop so the select_image path (which runs on a tokio thread, where a plain upgrade() returns None) shows it too. Startup generation stays synchronous by design: with the decode bounded it costs milliseconds and the first paint comes up in the generated palette instead of flashing the default. cc --- crates/qbz-theme/src/auto/palette.rs | 14 +++++++++++++- crates/qbz/src/auto_theme.rs | 11 +++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/qbz-theme/src/auto/palette.rs b/crates/qbz-theme/src/auto/palette.rs index 907f10cdf..e0b0fb091 100644 --- a/crates/qbz-theme/src/auto/palette.rs +++ b/crates/qbz-theme/src/auto/palette.rs @@ -13,7 +13,19 @@ pub fn extract_palette(image_path: &str) -> Result { return Err(format!("Image not found: {}", image_path)); } - let img = image::open(path).map_err(|e| format!("Failed to open image: {}", e))?; + // Bounded decode: a decompression-bomb PNG (65k x 65k) would otherwise + // expand to tens of GB before the 100x100 downsample. 12k x 12k / 512 MB + // covers any real wallpaper while keeping the worst case survivable. + let mut reader = image::ImageReader::open(path) + .map_err(|e| format!("Failed to open image: {}", e))? + .with_guessed_format() + .map_err(|e| format!("Failed to read image: {}", e))?; + let mut limits = image::Limits::default(); + limits.max_image_width = Some(12_000); + limits.max_image_height = Some(12_000); + limits.max_alloc = Some(512 * 1024 * 1024); + reader.limits(limits); + let img = reader.decode().map_err(|e| format!("Failed to decode image: {}", e))?; // Downsample to 100x100 for fast processing. let thumb = img.resize_exact(100, 100, image::imageops::FilterType::Lanczos3); diff --git a/crates/qbz/src/auto_theme.rs b/crates/qbz/src/auto_theme.rs index 03e99129b..784fa953b 100644 --- a/crates/qbz/src/auto_theme.rs +++ b/crates/qbz/src/auto_theme.rs @@ -75,10 +75,13 @@ pub fn apply_startup(window: &AppWindow) { /// Regenerate the auto theme off-thread and push the result on the event loop. /// Toggles `auto-theme-generating` around the work and toasts on failure. pub fn regenerate(weak: slint::Weak, handle: tokio::runtime::Handle) { - if let Some(w) = weak.upgrade() { - w.global::() - .set_auto_theme_generating(true); - } + // Through the event loop, NOT a direct upgrade: regenerate() is also + // called from tokio contexts (select_image), where a plain upgrade() + // returns None and the generating indicator would silently be skipped. + let weak_flag = weak.clone(); + let _ = weak_flag.upgrade_in_event_loop(move |w| { + w.global::().set_auto_theme_generating(true); + }); handle.spawn(async move { let prefs = crate::ui_prefs::load(); let source = source_from_prefs(&prefs);