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