快速面板模块
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
//! 应用图标提取: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;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use base64::Engine as _;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
// ===== 内存缓存 =====
|
||||
static MEM_CACHE: Mutex<Option<HashMap<String, 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()?.get(path).cloned()
|
||||
}
|
||||
|
||||
fn mem_put(path: String, url: String) {
|
||||
if let Ok(mut guard) = MEM_CACHE.lock() {
|
||||
let map = guard.get_or_insert_with(HashMap::new);
|
||||
if map.len() >= MEM_CACHE_MAX {
|
||||
// 简单清理:丢弃一半(最早插入的,HashMap 无序,近似随机)
|
||||
let keep = map.len() / 2;
|
||||
let keys: Vec<String> = map.keys().cloned().collect();
|
||||
for k in keys.iter().skip(keep) {
|
||||
map.remove(k);
|
||||
}
|
||||
}
|
||||
map.insert(path, url);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 磁盘缓存路径 =====
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user