1use crate::EmbedResourcesKind;
5use crate::diagnostics::BuildDiagnostics;
6use crate::embedded_resources::*;
7use crate::expression_tree::{Expression, ImageReference};
8use crate::object_tree::*;
9#[cfg(feature = "software-renderer")]
10use image::GenericImageView;
11use smol_str::SmolStr;
12use std::cell::RefCell;
13use std::collections::HashMap;
14use std::future::Future;
15use std::pin::Pin;
16use std::rc::Rc;
17use typed_index_collections::TiVec;
18
19#[cfg(feature = "software-renderer")]
22pub(crate) type SharedFontCollection = super::embed_glyphs::SharedFontCollection;
23#[cfg(not(feature = "software-renderer"))]
24pub(crate) type SharedFontCollection = ();
25
26pub async fn embed_images(
27 doc: &Document,
28 embed_files: EmbedResourcesKind,
29 scale_factor: f32,
30 resource_url_mapper: &Option<Rc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = Option<String>>>>>>,
31 font_collection: Option<&SharedFontCollection>,
32 diag: &mut BuildDiagnostics,
33) {
34 if embed_files == EmbedResourcesKind::Nothing && resource_url_mapper.is_none() {
35 return;
36 }
37
38 let global_embedded_resources = &doc.embedded_file_resources;
39 let mut path_to_id = HashMap::<SmolStr, EmbeddedResourcesIdx>::new();
40
41 let mut all_components = Vec::new();
42 doc.visit_all_used_components(|c| all_components.push(c.clone()));
43 let all_components = all_components;
44
45 let mapped_urls = {
46 let mut urls = HashMap::<SmolStr, Option<SmolStr>>::new();
47
48 if let Some(mapper) = resource_url_mapper {
49 for component in &all_components {
51 visit_all_expressions(component, |e, _| {
52 collect_image_urls_from_expression(e, &mut urls)
53 });
54 }
55
56 for i in urls.iter_mut() {
58 *i.1 = (*mapper)(i.0).await.map(SmolStr::new);
59 }
60 }
61
62 urls
63 };
64
65 for component in &all_components {
67 visit_all_expressions(component, |e, _| {
68 embed_images_from_expression(
69 e,
70 &mapped_urls,
71 global_embedded_resources,
72 &mut path_to_id,
73 embed_files,
74 scale_factor,
75 diag,
76 font_collection,
77 )
78 });
79 }
80}
81
82fn image_reference_url(resource: &str) -> SmolStr {
88 if crate::pathutils::is_url(std::path::Path::new(resource)) {
89 return resource.into();
90 }
91 #[cfg(not(target_arch = "wasm32"))]
94 {
95 url::Url::from_file_path(resource)
96 .map(|url| SmolStr::from(url.as_str()))
97 .unwrap_or_else(|()| resource.into())
98 }
99 #[cfg(target_arch = "wasm32")]
100 {
101 resource.into()
102 }
103}
104
105fn collect_image_urls_from_expression(
106 e: &Expression,
107 urls: &mut HashMap<SmolStr, Option<SmolStr>>,
108) {
109 if let Expression::ImageReference { resource_ref, .. } = e
110 && let ImageReference::AbsolutePath(path) = resource_ref
111 && !path.starts_with("data:")
114 {
115 urls.insert(image_reference_url(path), None);
116 };
117
118 e.visit(|e| collect_image_urls_from_expression(e, urls));
119}
120
121fn embed_images_from_expression(
122 e: &mut Expression,
123 urls: &HashMap<SmolStr, Option<SmolStr>>,
124 global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
125 path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
126 embed_files: EmbedResourcesKind,
127 scale_factor: f32,
128 diag: &mut BuildDiagnostics,
129 font_collection: Option<&SharedFontCollection>,
130) {
131 if let Expression::ImageReference { resource_ref, source_location, nine_slice: _ } = e
132 && let ImageReference::AbsolutePath(path) = resource_ref
133 {
134 if path.starts_with("data:") {
135 if !matches!(
138 embed_files,
139 EmbedResourcesKind::Nothing | EmbedResourcesKind::ListAllResources
140 ) {
141 let image_ref = embed_data_uri(
142 global_embedded_resources,
143 path_to_id,
144 path,
145 embed_files,
146 scale_factor,
147 diag,
148 source_location,
149 font_collection,
150 );
151 *resource_ref = image_ref;
152 }
153 return;
154 }
155
156 let mapped_path =
158 urls.get(&image_reference_url(path)).cloned().flatten().unwrap_or_else(|| path.clone());
159 *path = mapped_path;
160 if embed_files != EmbedResourcesKind::Nothing
161 && (embed_files != EmbedResourcesKind::OnlyBuiltinResources
162 || path.starts_with("builtin:/"))
163 {
164 let image_ref = embed_image(
165 global_embedded_resources,
166 path_to_id,
167 embed_files,
168 path,
169 scale_factor,
170 diag,
171 source_location,
172 font_collection,
173 );
174 if embed_files != EmbedResourcesKind::ListAllResources {
175 *resource_ref = image_ref;
176 }
177 }
178 };
179
180 e.visit_mut(|e| {
181 embed_images_from_expression(
182 e,
183 urls,
184 global_embedded_resources,
185 path_to_id,
186 embed_files,
187 scale_factor,
188 diag,
189 font_collection,
190 )
191 });
192}
193
194fn embed_image(
195 global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
196 path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
197 embed_files: EmbedResourcesKind,
198 path: &str,
199 _scale_factor: f32,
200 diag: &mut BuildDiagnostics,
201 source_location: &Option<crate::diagnostics::SourceLocation>,
202 _font_collection: Option<&SharedFontCollection>,
203) -> ImageReference {
204 let extension = || {
205 std::path::Path::new(path)
206 .extension()
207 .and_then(|e| e.to_str())
208 .map(|x| x.to_string())
209 .unwrap_or_default()
210 };
211
212 if let Some(&resource_id) = path_to_id.get(path) {
213 return match global_embedded_resources.borrow()[resource_id].kind {
214 #[cfg(feature = "software-renderer")]
215 EmbeddedResourcesKind::TextureData { .. } => {
216 ImageReference::EmbeddedTexture { resource_id }
217 }
218 _ => ImageReference::EmbeddedData { resource_id, extension: extension() },
219 };
220 }
221
222 let mut resources = global_embedded_resources.borrow_mut();
223 let mut push = |kind| {
224 let id = resources.push_and_get_key(EmbeddedResources { path: Some(path.into()), kind });
225 path_to_id.insert(path.into(), id);
226 id
227 };
228
229 if embed_files == EmbedResourcesKind::ListAllResources {
230 push(EmbeddedResourcesKind::ListOnly);
231 return ImageReference::None;
232 }
233
234 let Some(_file) = crate::fileaccess::load_file(std::path::Path::new(path)) else {
235 diag.push_error(format!("Cannot find image file {path}"), source_location);
236 return ImageReference::None;
237 };
238
239 #[cfg(feature = "software-renderer")]
240 if embed_files == EmbedResourcesKind::EmbedTextures {
241 return match load_image(_file, _scale_factor, _font_collection) {
242 Ok((img, source_format, original_size)) => {
243 let resource_id = push(EmbeddedResourcesKind::TextureData(generate_texture(
244 img,
245 source_format,
246 original_size,
247 )));
248 ImageReference::EmbeddedTexture { resource_id }
249 }
250 Err(err) => {
251 diag.push_error(format!("Cannot load image file {path}: {err}"), source_location);
252 ImageReference::None
253 }
254 };
255 }
256
257 let resource_id = push(EmbeddedResourcesKind::FileData);
258 ImageReference::EmbeddedData { resource_id, extension: extension() }
259}
260
261#[cfg(feature = "software-renderer")]
262trait Pixel {
263 fn is_transparent(&self) -> bool;
266}
267#[cfg(feature = "software-renderer")]
268impl Pixel for image::Rgba<u8> {
269 fn is_transparent(&self) -> bool {
272 self[3] <= 1
273 }
274}
275
276#[cfg(feature = "software-renderer")]
277fn generate_texture(
278 image: image::RgbaImage,
279 source_format: SourceFormat,
280 original_size: Size,
281) -> Texture {
282 let mut top = 0;
284 let is_line_transparent = |y| {
285 for x in 0..image.width() {
286 if !image.get_pixel(x, y).is_transparent() {
287 return false;
288 }
289 }
290 true
291 };
292 while top < image.height() && is_line_transparent(top) {
293 top += 1;
294 }
295 if top == image.height() {
296 return Texture::new_empty();
297 }
298 let mut bottom = image.height() - 1;
299 while is_line_transparent(bottom) {
300 bottom -= 1;
301 assert!(bottom > top); }
303 let is_column_transparent = |x| {
304 for y in top..=bottom {
305 if !image.get_pixel(x, y).is_transparent() {
306 return false;
307 }
308 }
309 true
310 };
311 let mut left = 0;
312 while is_column_transparent(left) {
313 left += 1;
314 assert!(left < image.width()); }
316 let mut right = image.width() - 1;
317 while is_column_transparent(right) {
318 right -= 1;
319 assert!(right > left); }
321 let mut is_opaque = true;
322 enum ColorState {
323 Unset,
324 Different,
325 Rgb([u8; 3]),
326 }
327 let mut color = ColorState::Unset;
328 'outer: for y in top..=bottom {
329 for x in left..=right {
330 let p = image.get_pixel(x, y);
331 let alpha = p[3];
332 if alpha != 255 {
333 is_opaque = false;
334 }
335 if alpha == 0 {
336 continue;
337 }
338 let get_pixel = || match source_format {
339 SourceFormat::RgbaPremultiplied => <[u8; 3]>::try_from(&p.0[0..3])
340 .unwrap()
341 .map(|v| (v as u16 * 255 / alpha as u16) as u8),
342 SourceFormat::Rgba => p.0[0..3].try_into().unwrap(),
343 };
344 match color {
345 ColorState::Unset => {
346 color = ColorState::Rgb(get_pixel());
347 }
348 ColorState::Different => {
349 if !is_opaque {
350 break 'outer;
351 }
352 }
353 ColorState::Rgb([a, b, c]) => {
354 let px = get_pixel();
355 if a.abs_diff(px[0]) > 2 || b.abs_diff(px[1]) > 2 || c.abs_diff(px[2]) > 2 {
356 color = ColorState::Different
357 }
358 }
359 }
360 }
361 }
362
363 let format = if let ColorState::Rgb(c) = color {
364 PixelFormat::AlphaMap(c)
365 } else if is_opaque {
366 PixelFormat::Rgb
367 } else {
368 PixelFormat::RgbaPremultiplied
369 };
370
371 let rect = Rect::from_ltrb(left as _, top as _, (right + 1) as _, (bottom + 1) as _).unwrap();
372 Texture {
373 total_size: Size { width: image.width(), height: image.height() },
374 original_size,
375 rect,
376 data: convert_image(image, source_format, format, rect),
377 format,
378 }
379}
380
381#[cfg(feature = "software-renderer")]
382fn convert_image(
383 image: image::RgbaImage,
384 source_format: SourceFormat,
385 format: PixelFormat,
386 rect: Rect,
387) -> Vec<u8> {
388 let i = image::SubImage::new(&image, rect.x() as _, rect.y() as _, rect.width(), rect.height());
389 match (source_format, format) {
390 (_, PixelFormat::Rgb) => {
391 i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0).take(3)).collect()
392 }
393 (SourceFormat::RgbaPremultiplied, PixelFormat::RgbaPremultiplied)
394 | (SourceFormat::Rgba, PixelFormat::Rgba) => {
395 i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0)).collect()
396 }
397 (SourceFormat::Rgba, PixelFormat::RgbaPremultiplied) => i
398 .pixels()
399 .flat_map(|(_, _, p)| {
400 let a = p.0[3] as u32;
401 IntoIterator::into_iter(p.0)
402 .take(3)
403 .map(move |x| (x as u32 * a / 255) as u8)
404 .chain(std::iter::once(a as u8))
405 })
406 .collect(),
407 (SourceFormat::RgbaPremultiplied, PixelFormat::Rgba) => i
408 .pixels()
409 .flat_map(|(_, _, p)| {
410 let a = p.0[3] as u32;
411 IntoIterator::into_iter(p.0)
412 .take(3)
413 .map(move |x| (x as u32 * 255 / a) as u8)
414 .chain(std::iter::once(a as u8))
415 })
416 .collect(),
417 (_, PixelFormat::AlphaMap(_)) => i.pixels().map(|(_, _, p)| p[3]).collect(),
418 }
419}
420
421#[cfg(feature = "software-renderer")]
422enum SourceFormat {
423 RgbaPremultiplied,
424 Rgba,
425}
426
427#[cfg(feature = "software-renderer")]
431fn svg_font_options(
432 font_collection: Option<&SharedFontCollection>,
433) -> resvg::usvg::Options<'static> {
434 use i_slint_common::sharedfontique::svg as svg_fonts;
435
436 let Some(font_collection) = font_collection.cloned() else {
437 return resvg::usvg::Options::default();
438 };
439 svg_fonts::options(move |families, attributes, require_char| {
440 let mut fonts = font_collection.lock().ok()?;
441 let collection = &mut fonts.collection;
442 svg_fonts::query_font(
443 &mut collection.inner,
444 &mut collection.source_cache,
445 families,
446 attributes,
447 require_char,
448 )
449 })
450}
451
452#[cfg(feature = "software-renderer")]
453fn load_image_from_bytes(
454 data: &[u8],
455 extension: Option<&str>,
456 scale_factor: f32,
457 font_collection: Option<&SharedFontCollection>,
458) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
459 use resvg::{tiny_skia, usvg};
460
461 let is_svg = matches!(extension, Some("svg") | Some("svgz"));
462
463 if is_svg {
464 let tree = {
465 usvg::Tree::from_data(data, &svg_font_options(font_collection)).map_err(|e| {
466 image::ImageError::Decoding(image::error::DecodingError::new(
467 image::error::ImageFormatHint::Name("svg".into()),
468 e,
469 ))
470 })?
471 };
472
473 let original_size = tree.size();
474 let width = (original_size.width() * scale_factor) as u32;
475 let height = (original_size.height() * scale_factor) as u32;
476
477 let mut buffer = vec![0u8; width as usize * height as usize * 4];
478
479 let size_error = || {
480 image::ImageError::Limits(image::error::LimitError::from_kind(
481 image::error::LimitErrorKind::DimensionError,
482 ))
483 };
484
485 let mut skia_buffer =
486 tiny_skia::PixmapMut::from_bytes(buffer.as_mut_slice(), width, height)
487 .ok_or_else(size_error)?;
488
489 resvg::render(
490 &tree,
491 tiny_skia::Transform::from_scale(scale_factor, scale_factor),
492 &mut skia_buffer,
493 );
494
495 return image::RgbaImage::from_raw(width, height, buffer).ok_or_else(size_error).map(
496 |img| {
497 (
498 img,
499 SourceFormat::RgbaPremultiplied,
500 Size { width: original_size.width() as _, height: original_size.height() as _ },
501 )
502 },
503 );
504 }
505
506 image::load_from_memory(data).map(|mut image| {
507 let (original_width, original_height) = image.dimensions();
508
509 if scale_factor < 1.0 {
510 image = image.resize_exact(
511 (original_width as f32 * scale_factor) as u32,
512 (original_height as f32 * scale_factor) as u32,
513 image::imageops::FilterType::Gaussian,
514 );
515 }
516
517 (
518 image.to_rgba8(),
519 SourceFormat::Rgba,
520 Size { width: original_width, height: original_height },
521 )
522 })
523}
524
525#[cfg(feature = "software-renderer")]
526fn load_image(
527 file: crate::fileaccess::VirtualFile,
528 scale_factor: f32,
529 font_collection: Option<&SharedFontCollection>,
530) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
531 use std::ffi::OsStr;
532
533 let extension = file.canon_path.extension().and_then(OsStr::to_str);
534
535 let data = if let Some(buffer) = file.builtin_contents {
536 buffer.to_vec()
537 } else {
538 std::fs::read(&file.canon_path)?
539 };
540
541 load_image_from_bytes(&data, extension, scale_factor, font_collection)
542}
543
544fn embed_data_uri(
545 global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
546 path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
547 data_uri: &str,
548 _embed_files: EmbedResourcesKind,
549 _scale_factor: f32,
550 diag: &mut BuildDiagnostics,
551 source_location: &Option<crate::diagnostics::SourceLocation>,
552 _font_collection: Option<&SharedFontCollection>,
553) -> ImageReference {
554 if let Some(&resource_id) = path_to_id.get(data_uri) {
555 let resources = global_embedded_resources.borrow();
556 return match &resources[resource_id].kind {
557 #[cfg(feature = "software-renderer")]
558 EmbeddedResourcesKind::TextureData { .. } => {
559 ImageReference::EmbeddedTexture { resource_id }
560 }
561 EmbeddedResourcesKind::DataUriPayload(_, ext) => {
562 ImageReference::EmbeddedData { resource_id, extension: ext.clone() }
563 }
564 _ => ImageReference::None,
565 };
566 }
567
568 let (decoded_data, extension) = match crate::data_uri::decode_data_uri(data_uri) {
569 Ok(result) => result,
570 Err(e) => {
571 diag.push_error(e, source_location);
572 return ImageReference::None;
573 }
574 };
575
576 let mut resources = global_embedded_resources.borrow_mut();
577 let mut push = |kind| {
578 let id = resources.push_and_get_key(EmbeddedResources { path: None, kind });
579 path_to_id.insert(data_uri.into(), id);
580 id
581 };
582
583 #[cfg(feature = "software-renderer")]
584 if _embed_files == EmbedResourcesKind::EmbedTextures {
585 match load_image_from_bytes(
586 &decoded_data,
587 Some(&extension),
588 _scale_factor,
589 _font_collection,
590 )
591 .map_err(|e| e.to_string())
592 {
593 Ok((img, source_format, original_size)) => {
594 let resource_id = push(EmbeddedResourcesKind::TextureData(generate_texture(
595 img,
596 source_format,
597 original_size,
598 )));
599 return ImageReference::EmbeddedTexture { resource_id };
600 }
601 Err(err) => {
602 diag.push_error(format!("Cannot load data URI image: {err}"), source_location);
603 return ImageReference::None;
604 }
605 }
606 }
607
608 let resource_id = push(EmbeddedResourcesKind::DataUriPayload(decoded_data, extension.clone()));
609
610 ImageReference::EmbeddedData { resource_id, extension }
611}