zerolaunch_plugin_api/services/icon/
icon_extractor.rs1use 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#[async_trait]
12pub trait IconExtractor: Send + Sync {
13 async fn extract_from_path(&self, path: &str) -> Result<Vec<u8>, HostApiError>;
19
20 async fn extract_from_url(&self, url: &str) -> Result<Vec<u8>, HostApiError>;
24
25 async fn extract_from_extension(&self, ext: &str) -> Result<Vec<u8>, HostApiError>;
29
30 fn default_app_icon_path(&self) -> &str;
34
35 fn default_web_icon_path(&self) -> &str;
39
40 fn is_network_available(&self) -> bool;
44
45 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 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 let trimmed = ImageUtils::trim_transparent_white_border(data.clone()).unwrap_or(data);
68 let resized = ImageUtils::resize_image(trimmed.clone(), MAX_ICON_SIZE, MAX_ICON_SIZE)
70 .await
71 .unwrap_or(trimmed);
72 Ok(ImageUtils::to_webp(resized.clone()).unwrap_or(resized))
74 }
75
76 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 ImageUtils::to_webp(png.clone()).unwrap_or(png)
91 }
92
93 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 if level != CacheLevel::SkipAll {
107 if level == CacheLevel::Full {
109 if let Some(data) = cache.get_l1(&hash_key) {
110 return Ok(data);
111 }
112 }
113
114 if cache.contains_l2(&hash_key) {
116 if let Some(data) = cache.get_l2(&hash_key).await {
117 if level == CacheLevel::Full {
119 cache.set_l1(&hash_key, data.clone());
120 }
121 return Ok(data);
122 }
123 }
124 }
125
126 let data = match self.extract_and_process(request).await {
128 Ok(d) if !d.is_empty() => d,
129 _ => {
130 return Ok(self.load_default_icon(request).await);
132 }
133 };
134
135 write_back_cache(cache, &hash_key, &data, level).await;
137
138 Ok(data)
139 }
140
141 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
158async 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
175fn 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}