//! 应用图标提取:Windows SHGetFileInfo → HICON → RGBA → PNG,带磁盘 + 内存缓存。 //! //! 流程: //! 1. 内存缓存命中 → 直接返回 data URL //! 2. 磁盘缓存命中({app_data_dir}/quickpanel/icons/{hash}.png) → 读取并缓存 //! 3. 调用 SHGetFileInfoW 提取 HICON → GetDIBits 取 32bit BGRA → 转 RGBA → PNG //! 4. 写入磁盘缓存 + 内存缓存,返回 data URL //! //! 设计取舍: //! - 返回 base64 data URL 而非文件路径,避免独立弹窗窗口的 asset 协议配置问题 //! - 磁盘缓存避免重复 Windows API 调用(昂贵),内存缓存避免重复磁盘读取 + 编码 use std::collections::{HashMap, VecDeque}; use std::path::PathBuf; use std::sync::Mutex; use base64::Engine as _; use tauri::{AppHandle, Manager}; // ===== 内存缓存 ===== /// (path → data URL) + FIFO 淘汰队列(队头最旧,超限时先淘汰) static MEM_CACHE: Mutex, VecDeque)>> = Mutex::new(None); const MEM_CACHE_MAX: usize = 512; fn mem_get(path: &str) -> Option { let cache = MEM_CACHE.lock().ok()?; cache.as_ref()?.0.get(path).cloned() } fn mem_put(path: String, url: String) { if let Ok(mut guard) = MEM_CACHE.lock() { let slot = guard.get_or_insert_with(|| (HashMap::new(), VecDeque::new())); let (map, order) = &mut *slot; if map.contains_key(&path) { // 已存在:仅更新值,不重复入队 map.insert(path, url); return; } if map.len() >= MEM_CACHE_MAX { // FIFO 淘汰最旧条目(O(1)),避免无序淘汰把刚插入的常用图标清掉 if let Some(oldest) = order.pop_front() { map.remove(&oldest); } } map.insert(path.clone(), url); order.push_back(path); } } // ===== 磁盘缓存路径 ===== fn cache_dir(app: &AppHandle) -> PathBuf { app.path() .app_data_dir() .unwrap_or_else(|_| PathBuf::from(".")) .join("quickpanel") .join("icons") } fn path_hash(path: &str) -> String { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; // 统一小写 + 正斜杠,避免大小写/分隔符差异导致缓存未命中 let normalized = path.to_lowercase().replace('\\', "/"); let mut hasher = DefaultHasher::new(); normalized.hash(&mut hasher); format!("{:016x}", hasher.finish()) } // ===== 公共 API ===== /// 获取应用图标 data URL。命中缓存则零开销;未命中则提取 + 编码 + 落盘。 /// 返回 Ok(None) 表示提取失败或不支持的平台。 pub fn get_icon_data_url(app: &AppHandle, path: &str) -> Option { if path.is_empty() { return None; } // 规范化路径分隔符:混合 / 和 \ 会导致 SHGetFileInfoW 失败 let normalized = path.replace('/', "\\"); let path = normalized.as_str(); // 1. 内存缓存 if let Some(url) = mem_get(path) { return Some(url); } let dir = cache_dir(app); let hash = path_hash(path); let cache_path = dir.join(format!("{}.png", hash)); // 2. 磁盘缓存 if cache_path.exists() { if let Ok(bytes) = std::fs::read(&cache_path) { let url = png_to_data_url(&bytes); mem_put(path.to_string(), url.clone()); return Some(url); } } // 3. 提取 let png = extract_icon_png(path)?; // 4. 落盘(失败不影响返回) std::fs::create_dir_all(&dir).ok(); std::fs::write(&cache_path, &png).ok(); // 5. 缓存 + 返回 let url = png_to_data_url(&png); mem_put(path.to_string(), url.clone()); Some(url) } /// 清理整个图标磁盘缓存(设置页可调用) pub fn clear_cache(app: &AppHandle) { let dir = cache_dir(app); if dir.exists() { std::fs::remove_dir_all(&dir).ok(); std::fs::create_dir_all(&dir).ok(); } if let Ok(mut guard) = MEM_CACHE.lock() { *guard = None; } } fn png_to_data_url(png: &[u8]) -> String { let b64 = base64::engine::general_purpose::STANDARD.encode(png); format!("data:image/png;base64,{}", b64) } // ===== Windows 图标提取 ===== #[cfg(windows)] fn extract_icon_png(path: &str) -> Option> { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; use windows_sys::Win32::UI::Shell::{ SHGetFileInfoW, SHFILEINFOW, SHGFI_ICON, SHGFI_LARGEICON, }; use windows_sys::Win32::UI::WindowsAndMessaging::DestroyIcon; unsafe { let wide: Vec = OsStr::new(path) .encode_wide() .chain(std::iter::once(0)) .collect(); let mut shfi: SHFILEINFOW = std::mem::zeroed(); let _ = SHGetFileInfoW( wide.as_ptr(), 0, &mut shfi, std::mem::size_of::() as u32, SHGFI_ICON | SHGFI_LARGEICON, ); // hIcon 为 0 表示无图标 if shfi.hIcon == 0 { return None; } let result = hicon_to_png(shfi.hIcon); let _ = DestroyIcon(shfi.hIcon); result } } #[cfg(not(windows))] fn extract_icon_png(_path: &str) -> Option> { None } /// HICON → PNG bytes #[cfg(windows)] fn hicon_to_png(hicon: windows_sys::Win32::UI::WindowsAndMessaging::HICON) -> Option> { use windows_sys::Win32::Graphics::Gdi::DeleteObject; use windows_sys::Win32::UI::WindowsAndMessaging::{GetIconInfo, ICONINFO}; unsafe { let mut icon_info: ICONINFO = std::mem::zeroed(); if GetIconInfo(hicon, &mut icon_info) == 0 { return None; } let has_color = icon_info.hbmColor != 0; let has_mask = icon_info.hbmMask != 0; let result: Option<(Vec, u32, u32)> = if has_color { // 32-bit BGRA → RGBA let (mut rgba, w, h) = bitmap_to_rgba32(icon_info.hbmColor)?; // 检查 alpha 是否全 0(旧式无 alpha 通道图标) let alpha_any = rgba.chunks_exact(4).any(|c| c[3] != 0); if !alpha_any { if has_mask { // 用 mask 补 alpha(白=透明,黑=不透明) let _ = apply_mask_alpha(&mut rgba, w, h, icon_info.hbmMask); } else { // 无 mask,设为全不透明 for c in rgba.chunks_exact_mut(4) { c[3] = 255; } } } Some((rgba, w, h)) } else { // 无颜色位图:monochrome 图标,罕见且无色,跳过 None }; // 清理 GDI 对象 if has_color { let _ = DeleteObject(icon_info.hbmColor); } if has_mask { let _ = DeleteObject(icon_info.hbmMask); } let (rgba, w, h) = result?; encode_png(&rgba, w, h) } } /// 读取 32-bit 位图为 RGBA(top-down),BGRA→RGBA #[cfg(windows)] fn bitmap_to_rgba32( hbm: windows_sys::Win32::Graphics::Gdi::HBITMAP, ) -> Option<(Vec, u32, u32)> { use windows_sys::Win32::Foundation::HWND; use windows_sys::Win32::Graphics::Gdi::{ GetDC, GetDIBits, GetObjectW, BITMAP, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, ReleaseDC, }; unsafe { // 取尺寸 let mut bmp: BITMAP = std::mem::zeroed(); let got = GetObjectW( hbm, std::mem::size_of::() as i32, &mut bmp as *mut _ as *mut _, ); if got == 0 { return None; } let w = bmp.bmWidth as u32; let h = bmp.bmHeight as u32; if w == 0 || h == 0 { return None; } // 32-bit top-down DIB let mut bi: BITMAPINFO = std::mem::zeroed(); bi.bmiHeader.biSize = std::mem::size_of::() as u32; bi.bmiHeader.biWidth = w as i32; bi.bmiHeader.biHeight = -(h as i32); // 负值 = top-down bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biBitCount = 32; bi.bmiHeader.biCompression = BI_RGB; let mut pixels = vec![0u8; (w * h * 4) as usize]; let hdc = GetDC(0 as HWND); if hdc == 0 { return None; } let ret = GetDIBits( hdc, hbm, 0, h, pixels.as_mut_ptr() as *mut _, &mut bi, DIB_RGB_COLORS, ); let _ = ReleaseDC(0 as HWND, hdc); if ret == 0 { return None; } // BGRA → RGBA for chunk in pixels.chunks_exact_mut(4) { chunk.swap(0, 2); } Some((pixels, w, h)) } } /// 用 1bpp mask 设置 alpha:mask 白(1)=透明,黑(0)=不透明 #[cfg(windows)] fn apply_mask_alpha( rgba: &mut [u8], w: u32, h: u32, hbm_mask: windows_sys::Win32::Graphics::Gdi::HBITMAP, ) -> Result<(), ()> { use windows_sys::Win32::Foundation::HWND; use windows_sys::Win32::Graphics::Gdi::{ GetDC, GetDIBits, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, ReleaseDC, }; unsafe { let mut bi: BITMAPINFO = std::mem::zeroed(); bi.bmiHeader.biSize = std::mem::size_of::() as u32; bi.bmiHeader.biWidth = w as i32; bi.bmiHeader.biHeight = -(h as i32); bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biBitCount = 1; bi.bmiHeader.biCompression = BI_RGB; // 1bpp,每行 4 字节对齐 let row_bytes = ((w + 31) / 32 * 4) as usize; let mut mask = vec![0u8; row_bytes * h as usize]; let hdc = GetDC(0 as HWND); if hdc == 0 { return Err(()); } let ret = GetDIBits( hdc, hbm_mask, 0, h, mask.as_mut_ptr() as *mut _, &mut bi, DIB_RGB_COLORS, ); let _ = ReleaseDC(0 as HWND, hdc); if ret == 0 { return Err(()); } for y in 0..h as usize { for x in 0..w as usize { let byte_idx = y * row_bytes + x / 8; let bit = (mask[byte_idx] >> (7 - (x % 8))) & 1; let alpha = if bit == 1 { 0 } else { 255 }; rgba[(y * w as usize + x) * 4 + 3] = alpha; } } Ok(()) } } /// RGBA → PNG fn encode_png(rgba: &[u8], w: u32, h: u32) -> Option> { use image::{ImageBuffer, RgbaImage}; let img: RgbaImage = ImageBuffer::from_raw(w, h, rgba.to_vec())?; let mut buf = std::io::Cursor::new(Vec::new()); image::DynamicImage::ImageRgba8(img) .write_to(&mut buf, image::ImageFormat::Png) .ok()?; Some(buf.into_inner()) }