Files
Thing/src-tauri/src/quickpanel/icon_extractor.rs
T
2026-08-06 10:33:16 +08:00

352 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 应用图标提取: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<Option<(HashMap<String, String>, VecDeque<String>)>> = Mutex::new(None);
const MEM_CACHE_MAX: usize = 512;
fn mem_get(path: &str) -> Option<String> {
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<String> {
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<Vec<u8>> {
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<u16> = 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::<SHFILEINFOW>() 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<Vec<u8>> {
None
}
/// HICON → PNG bytes
#[cfg(windows)]
fn hicon_to_png(hicon: windows_sys::Win32::UI::WindowsAndMessaging::HICON) -> Option<Vec<u8>> {
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<u8>, 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 位图为 RGBAtop-down),BGRA→RGBA
#[cfg(windows)]
fn bitmap_to_rgba32(
hbm: windows_sys::Win32::Graphics::Gdi::HBITMAP,
) -> Option<(Vec<u8>, 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::<BITMAP>() 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::<BITMAPINFOHEADER>() 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 设置 alphamask 白(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::<BITMAPINFOHEADER>() 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<Vec<u8>> {
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())
}