Skip to main content

zerolaunch_plugin_api/services/icon/
icon_extractor.rs

1use crate::common::image_utils::ImageUtils;
2use crate::host::cache_level::CacheLevel;
3use crate::host::error::HostApiError;
4use crate::services::icon::icon_cache::IconCacheService;
5use crate::services::icon_request::IconRequest;
6use async_trait::async_trait;
7
8/// 图标提取器 trait,定义平台原语与跨平台业务默认实现。
9/// 平台实现者只需实现 6 个原语方法,业务逻辑由默认实现提供。
10/// 默认实现可按需覆盖。
11#[async_trait]
12pub trait IconExtractor: Send + Sync {
13    // ===== 平台原语(各平台必须实现)=====
14
15    /// 从本地文件路径提取图标,返回 PNG 格式字节数据。
16    /// 参数:path - 文件路径(exe, lnk, url, ico, png 等)。
17    /// 返回:PNG 格式图标字节数据,失败返回 HostApiError。
18    async fn extract_from_path(&self, path: &str) -> Result<Vec<u8>, HostApiError>;
19
20    /// 从网址提取图标(favicon),返回 PNG 格式字节数据。
21    /// 参数:url - 网址。
22    /// 返回:PNG 格式图标字节数据,失败返回 HostApiError。
23    async fn extract_from_url(&self, url: &str) -> Result<Vec<u8>, HostApiError>;
24
25    /// 从文件扩展名提取系统关联图标,返回 PNG 格式字节数据。
26    /// 参数:ext - 文件扩展名(如 ".txt", ".doc")。
27    /// 返回:PNG 格式图标字节数据,失败返回 HostApiError。
28    async fn extract_from_extension(&self, ext: &str) -> Result<Vec<u8>, HostApiError>;
29
30    /// 获取默认应用图标的文件路径。
31    /// 参数:无。
32    /// 返回:默认应用图标路径字符串。
33    fn default_app_icon_path(&self) -> &str;
34
35    /// 获取默认网址图标的文件路径。
36    /// 参数:无。
37    /// 返回:默认网址图标路径字符串。
38    fn default_web_icon_path(&self) -> &str;
39
40    /// 检测当前平台网络是否可用。
41    /// 参数:无。
42    /// 返回:网络可用返回 true。
43    fn is_network_available(&self) -> bool;
44
45    // ===== 跨平台业务逻辑(默认实现)=====
46
47    /// 根据 IconRequest 提取原始图标数据。
48    /// 默认实现根据请求类型分发到对应的平台原语方法。
49    /// 参数:request - 图标请求。
50    /// 返回:PNG 格式图标字节数据,失败返回 HostApiError。
51    async fn extract(&self, request: &IconRequest) -> Result<Vec<u8>, HostApiError> {
52        match request {
53            IconRequest::Path(p) => self.extract_from_path(p).await,
54            IconRequest::Url(u) => self.extract_from_url(u).await,
55            IconRequest::Extension(e) => self.extract_from_extension(e).await,
56            IconRequest::Data(data) => decode_data_url(data),
57        }
58    }
59
60    /// 提取图标并应用后处理:裁剪白边 → 等比缩放到 128×128 上限 → WebP 无损编码(VP8L)。
61    /// 参数:request - 图标请求。
62    /// 返回:处理后的 WebP 格式图标字节数据(失败回退 PNG,消费方按字节头嗅探),提取失败返回 HostApiError。
63    async fn extract_and_process(&self, request: &IconRequest) -> Result<Vec<u8>, HostApiError> {
64        const MAX_ICON_SIZE: u32 = 128;
65        let data = self.extract(request).await?;
66        // 1. 裁剪透明/白边(失败回退原数据)
67        let trimmed = ImageUtils::trim_transparent_white_border(data.clone()).unwrap_or(data);
68        // 2. 超过 128×128 时等比缩放到 128(失败回退裁剪产物)
69        let resized = ImageUtils::resize_image(trimmed.clone(), MAX_ICON_SIZE, MAX_ICON_SIZE)
70            .await
71            .unwrap_or(trimmed);
72        // 3. WebP 无损编码(失败回退 PNG,MIME 侧按字节头嗅探兜底)
73        Ok(ImageUtils::to_webp(resized.clone()).unwrap_or(resized))
74    }
75
76    /// 加载默认图标。
77    /// 默认实现:URL 类型加载默认网址图标,其他类型加载默认应用图标。
78    /// 参数:request - 图标请求(用于判断类型)。
79    /// 返回:默认图标的 WebP 字节数据,读取失败返回空 Vec。
80    async fn load_default_icon(&self, request: &IconRequest) -> Vec<u8> {
81        let default_path = match request {
82            IconRequest::Url(_) => self.default_web_icon_path(),
83            _ => self.default_app_icon_path(),
84        };
85        let png = tokio::fs::read(default_path).await.unwrap_or_default();
86        if png.is_empty() {
87            return png;
88        }
89        // 与提取路径一致编码为 WebP,统一返回字节格式
90        ImageUtils::to_webp(png.clone()).unwrap_or(png)
91    }
92
93    /// 完整的图标获取流程,包含缓存策略。
94    /// 默认实现:根据 CacheLevel 执行 L1 → L2 → 提取 → 写回缓存,提取失败返回默认图标。
95    /// 参数:cache - 图标缓存服务;request - 图标请求;level - 缓存等级。
96    /// 返回:WebP 格式图标字节数据(回退路径可能为 PNG,消费方按字节头嗅探 MIME),失败返回 HostApiError。
97    async fn get_icon(
98        &self,
99        cache: &IconCacheService,
100        request: &IconRequest,
101        level: CacheLevel,
102    ) -> Result<Vec<u8>, HostApiError> {
103        let hash_key = request.get_hash_string() + ".webp";
104
105        // 1. 根据缓存等级查缓存
106        if level != CacheLevel::SkipAll {
107            // L1 查询
108            if level == CacheLevel::Full {
109                if let Some(data) = cache.get_l1(&hash_key) {
110                    return Ok(data);
111                }
112            }
113
114            // L2 查询
115            if cache.contains_l2(&hash_key) {
116                if let Some(data) = cache.get_l2(&hash_key).await {
117                    // L2 命中时回填 L1
118                    if level == CacheLevel::Full {
119                        cache.set_l1(&hash_key, data.clone());
120                    }
121                    return Ok(data);
122                }
123            }
124        }
125
126        // 2. 缓存未命中 → 提取 + 后处理
127        let data = match self.extract_and_process(request).await {
128            Ok(d) if !d.is_empty() => d,
129            _ => {
130                // 3. 提取失败 → 默认图标
131                return Ok(self.load_default_icon(request).await);
132            }
133        };
134
135        // 4. 写回缓存
136        write_back_cache(cache, &hash_key, &data, level).await;
137
138        Ok(data)
139    }
140
141    /// 强制从磁盘提取图标并更新缓存(跳过缓存读取)。
142    /// 默认实现:直接提取 → 写回缓存。
143    /// 参数:cache - 图标缓存服务;request - 图标请求;level - 缓存等级。
144    /// 返回:WebP 格式图标字节数据,提取失败返回 HostApiError。
145    async fn get_icon_and_update_cache(
146        &self,
147        cache: &IconCacheService,
148        request: &IconRequest,
149        level: CacheLevel,
150    ) -> Result<Vec<u8>, HostApiError> {
151        let hash_key = request.get_hash_string() + ".webp";
152        let data = self.extract_and_process(request).await?;
153        write_back_cache(cache, &hash_key, &data, level).await;
154        Ok(data)
155    }
156}
157
158/// 根据缓存等级将图标数据写回缓存。
159/// Full: 写入 L1 + L2;SkipMemory: 只写 L2;SkipAll: 不写。
160async fn write_back_cache(
161    cache: &IconCacheService,
162    hash_key: &str,
163    icon_data: &[u8],
164    level: CacheLevel,
165) {
166    if level == CacheLevel::Full {
167        cache.set_l1(hash_key, icon_data.to_vec());
168    }
169
170    if level == CacheLevel::Full || level == CacheLevel::SkipMemory {
171        cache.set_l2(hash_key, icon_data.to_vec()).await;
172    }
173}
174
175/// 解码 data URL 或纯 base64 为原始字节(IconRequest::Data 直通路径)。
176/// 参数:data - "data:<mime>;base64,<payload>" 形式或纯 base64 字符串。
177/// 返回:解码后的字节;格式不合法返回 HostApiError。
178fn decode_data_url(data: &str) -> Result<Vec<u8>, HostApiError> {
179    let payload = data.rsplit_once(";base64,").map(|(_, p)| p).unwrap_or(data);
180    base64::Engine::decode(&base64::engine::general_purpose::STANDARD, payload).map_err(|e| {
181        HostApiError::IconExtractionFailed {
182            request: "data".to_string(),
183            reason: format!("data URL 解码失败: {}", e),
184        }
185    })
186}