352 lines
11 KiB
Rust
352 lines
11 KiB
Rust
//! 应用图标提取: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 位图为 RGBA(top-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 设置 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::<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())
|
||
}
|