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 collect_image_urls_from_expression(
83 e: &Expression,
84 urls: &mut HashMap<SmolStr, Option<SmolStr>>,
85) {
86 if let Expression::ImageReference { resource_ref, .. } = e
87 && let ImageReference::AbsolutePath(path) = resource_ref
88 {
89 urls.insert(path.clone(), None);
90 };
91
92 e.visit(|e| collect_image_urls_from_expression(e, urls));
93}
94
95fn embed_images_from_expression(
96 e: &mut Expression,
97 urls: &HashMap<SmolStr, Option<SmolStr>>,
98 global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
99 path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
100 embed_files: EmbedResourcesKind,
101 scale_factor: f32,
102 diag: &mut BuildDiagnostics,
103 font_collection: Option<&SharedFontCollection>,
104) {
105 if let Expression::ImageReference { resource_ref, source_location, nine_slice: _ } = e
106 && let ImageReference::AbsolutePath(path) = resource_ref
107 {
108 if path.starts_with("data:") {
109 if !matches!(
112 embed_files,
113 EmbedResourcesKind::Nothing | EmbedResourcesKind::ListAllResources
114 ) {
115 let image_ref = embed_data_uri(
116 global_embedded_resources,
117 path_to_id,
118 path,
119 embed_files,
120 scale_factor,
121 diag,
122 source_location,
123 font_collection,
124 );
125 *resource_ref = image_ref;
126 }
127 return;
128 }
129
130 let mapped_path =
132 urls.get(path).unwrap_or(&Some(path.clone())).clone().unwrap_or(path.clone());
133 *path = mapped_path;
134 if embed_files != EmbedResourcesKind::Nothing
135 && (embed_files != EmbedResourcesKind::OnlyBuiltinResources
136 || path.starts_with("builtin:/"))
137 {
138 let image_ref = embed_image(
139 global_embedded_resources,
140 path_to_id,
141 embed_files,
142 path,
143 scale_factor,
144 diag,
145 source_location,
146 font_collection,
147 );
148 if embed_files != EmbedResourcesKind::ListAllResources {
149 *resource_ref = image_ref;
150 }
151 }
152 };
153
154 e.visit_mut(|e| {
155 embed_images_from_expression(
156 e,
157 urls,
158 global_embedded_resources,
159 path_to_id,
160 embed_files,
161 scale_factor,
162 diag,
163 font_collection,
164 )
165 });
166}
167
168fn embed_image(
169 global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
170 path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
171 embed_files: EmbedResourcesKind,
172 path: &str,
173 _scale_factor: f32,
174 diag: &mut BuildDiagnostics,
175 source_location: &Option<crate::diagnostics::SourceLocation>,
176 _font_collection: Option<&SharedFontCollection>,
177) -> ImageReference {
178 let extension = || {
179 std::path::Path::new(path)
180 .extension()
181 .and_then(|e| e.to_str())
182 .map(|x| x.to_string())
183 .unwrap_or_default()
184 };
185
186 if let Some(&resource_id) = path_to_id.get(path) {
187 return match global_embedded_resources.borrow()[resource_id].kind {
188 #[cfg(feature = "software-renderer")]
189 EmbeddedResourcesKind::TextureData { .. } => {
190 ImageReference::EmbeddedTexture { resource_id }
191 }
192 _ => ImageReference::EmbeddedData { resource_id, extension: extension() },
193 };
194 }
195
196 let mut resources = global_embedded_resources.borrow_mut();
197 let mut push = |kind| {
198 let id = resources.push_and_get_key(EmbeddedResources { path: Some(path.into()), kind });
199 path_to_id.insert(path.into(), id);
200 id
201 };
202
203 if embed_files == EmbedResourcesKind::ListAllResources {
204 push(EmbeddedResourcesKind::ListOnly);
205 return ImageReference::None;
206 }
207
208 let Some(_file) = crate::fileaccess::load_file(std::path::Path::new(path)) else {
209 diag.push_error(format!("Cannot find image file {path}"), source_location);
210 return ImageReference::None;
211 };
212
213 #[cfg(feature = "software-renderer")]
214 if embed_files == EmbedResourcesKind::EmbedTextures {
215 return match load_image(_file, _scale_factor, _font_collection) {
216 Ok((img, source_format, original_size)) => {
217 let resource_id = push(EmbeddedResourcesKind::TextureData(generate_texture(
218 img,
219 source_format,
220 original_size,
221 )));
222 ImageReference::EmbeddedTexture { resource_id }
223 }
224 Err(err) => {
225 diag.push_error(format!("Cannot load image file {path}: {err}"), source_location);
226 ImageReference::None
227 }
228 };
229 }
230
231 let resource_id = push(EmbeddedResourcesKind::FileData);
232 ImageReference::EmbeddedData { resource_id, extension: extension() }
233}
234
235#[cfg(feature = "software-renderer")]
236trait Pixel {
237 fn is_transparent(&self) -> bool;
240}
241#[cfg(feature = "software-renderer")]
242impl Pixel for image::Rgba<u8> {
243 fn is_transparent(&self) -> bool {
246 self[3] <= 1
247 }
248}
249
250#[cfg(feature = "software-renderer")]
251fn generate_texture(
252 image: image::RgbaImage,
253 source_format: SourceFormat,
254 original_size: Size,
255) -> Texture {
256 let mut top = 0;
258 let is_line_transparent = |y| {
259 for x in 0..image.width() {
260 if !image.get_pixel(x, y).is_transparent() {
261 return false;
262 }
263 }
264 true
265 };
266 while top < image.height() && is_line_transparent(top) {
267 top += 1;
268 }
269 if top == image.height() {
270 return Texture::new_empty();
271 }
272 let mut bottom = image.height() - 1;
273 while is_line_transparent(bottom) {
274 bottom -= 1;
275 assert!(bottom > top); }
277 let is_column_transparent = |x| {
278 for y in top..=bottom {
279 if !image.get_pixel(x, y).is_transparent() {
280 return false;
281 }
282 }
283 true
284 };
285 let mut left = 0;
286 while is_column_transparent(left) {
287 left += 1;
288 assert!(left < image.width()); }
290 let mut right = image.width() - 1;
291 while is_column_transparent(right) {
292 right -= 1;
293 assert!(right > left); }
295 let mut is_opaque = true;
296 enum ColorState {
297 Unset,
298 Different,
299 Rgb([u8; 3]),
300 }
301 let mut color = ColorState::Unset;
302 'outer: for y in top..=bottom {
303 for x in left..=right {
304 let p = image.get_pixel(x, y);
305 let alpha = p[3];
306 if alpha != 255 {
307 is_opaque = false;
308 }
309 if alpha == 0 {
310 continue;
311 }
312 let get_pixel = || match source_format {
313 SourceFormat::RgbaPremultiplied => <[u8; 3]>::try_from(&p.0[0..3])
314 .unwrap()
315 .map(|v| (v as u16 * 255 / alpha as u16) as u8),
316 SourceFormat::Rgba => p.0[0..3].try_into().unwrap(),
317 };
318 match color {
319 ColorState::Unset => {
320 color = ColorState::Rgb(get_pixel());
321 }
322 ColorState::Different => {
323 if !is_opaque {
324 break 'outer;
325 }
326 }
327 ColorState::Rgb([a, b, c]) => {
328 let px = get_pixel();
329 if a.abs_diff(px[0]) > 2 || b.abs_diff(px[1]) > 2 || c.abs_diff(px[2]) > 2 {
330 color = ColorState::Different
331 }
332 }
333 }
334 }
335 }
336
337 let format = if let ColorState::Rgb(c) = color {
338 PixelFormat::AlphaMap(c)
339 } else if is_opaque {
340 PixelFormat::Rgb
341 } else {
342 PixelFormat::RgbaPremultiplied
343 };
344
345 let rect = Rect::from_ltrb(left as _, top as _, (right + 1) as _, (bottom + 1) as _).unwrap();
346 Texture {
347 total_size: Size { width: image.width(), height: image.height() },
348 original_size,
349 rect,
350 data: convert_image(image, source_format, format, rect),
351 format,
352 }
353}
354
355#[cfg(feature = "software-renderer")]
356fn convert_image(
357 image: image::RgbaImage,
358 source_format: SourceFormat,
359 format: PixelFormat,
360 rect: Rect,
361) -> Vec<u8> {
362 let i = image::SubImage::new(&image, rect.x() as _, rect.y() as _, rect.width(), rect.height());
363 match (source_format, format) {
364 (_, PixelFormat::Rgb) => {
365 i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0).take(3)).collect()
366 }
367 (SourceFormat::RgbaPremultiplied, PixelFormat::RgbaPremultiplied)
368 | (SourceFormat::Rgba, PixelFormat::Rgba) => {
369 i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0)).collect()
370 }
371 (SourceFormat::Rgba, PixelFormat::RgbaPremultiplied) => i
372 .pixels()
373 .flat_map(|(_, _, p)| {
374 let a = p.0[3] as u32;
375 IntoIterator::into_iter(p.0)
376 .take(3)
377 .map(move |x| (x as u32 * a / 255) as u8)
378 .chain(std::iter::once(a as u8))
379 })
380 .collect(),
381 (SourceFormat::RgbaPremultiplied, PixelFormat::Rgba) => i
382 .pixels()
383 .flat_map(|(_, _, p)| {
384 let a = p.0[3] as u32;
385 IntoIterator::into_iter(p.0)
386 .take(3)
387 .map(move |x| (x as u32 * 255 / a) as u8)
388 .chain(std::iter::once(a as u8))
389 })
390 .collect(),
391 (_, PixelFormat::AlphaMap(_)) => i.pixels().map(|(_, _, p)| p[3]).collect(),
392 }
393}
394
395#[cfg(feature = "software-renderer")]
396enum SourceFormat {
397 RgbaPremultiplied,
398 Rgba,
399}
400
401#[cfg(feature = "software-renderer")]
405fn svg_font_options(
406 font_collection: Option<&SharedFontCollection>,
407) -> resvg::usvg::Options<'static> {
408 use i_slint_common::sharedfontique::svg as svg_fonts;
409
410 let Some(font_collection) = font_collection.cloned() else {
411 return resvg::usvg::Options::default();
412 };
413 svg_fonts::options(move |families, attributes, require_char| {
414 let mut fonts = font_collection.lock().ok()?;
415 let collection = &mut fonts.collection;
416 svg_fonts::query_font(
417 &mut collection.inner,
418 &mut collection.source_cache,
419 families,
420 attributes,
421 require_char,
422 )
423 })
424}
425
426#[cfg(feature = "software-renderer")]
427fn load_image_from_bytes(
428 data: &[u8],
429 extension: Option<&str>,
430 scale_factor: f32,
431 font_collection: Option<&SharedFontCollection>,
432) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
433 use resvg::{tiny_skia, usvg};
434
435 let is_svg = matches!(extension, Some("svg") | Some("svgz"));
436
437 if is_svg {
438 let tree = {
439 usvg::Tree::from_data(data, &svg_font_options(font_collection)).map_err(|e| {
440 image::ImageError::Decoding(image::error::DecodingError::new(
441 image::error::ImageFormatHint::Name("svg".into()),
442 e,
443 ))
444 })?
445 };
446
447 let original_size = tree.size();
448 let width = (original_size.width() * scale_factor) as u32;
449 let height = (original_size.height() * scale_factor) as u32;
450
451 let mut buffer = vec![0u8; width as usize * height as usize * 4];
452
453 let size_error = || {
454 image::ImageError::Limits(image::error::LimitError::from_kind(
455 image::error::LimitErrorKind::DimensionError,
456 ))
457 };
458
459 let mut skia_buffer =
460 tiny_skia::PixmapMut::from_bytes(buffer.as_mut_slice(), width, height)
461 .ok_or_else(size_error)?;
462
463 resvg::render(
464 &tree,
465 tiny_skia::Transform::from_scale(scale_factor, scale_factor),
466 &mut skia_buffer,
467 );
468
469 return image::RgbaImage::from_raw(width, height, buffer).ok_or_else(size_error).map(
470 |img| {
471 (
472 img,
473 SourceFormat::RgbaPremultiplied,
474 Size { width: original_size.width() as _, height: original_size.height() as _ },
475 )
476 },
477 );
478 }
479
480 image::load_from_memory(data).map(|mut image| {
481 let (original_width, original_height) = image.dimensions();
482
483 if scale_factor < 1.0 {
484 image = image.resize_exact(
485 (original_width as f32 * scale_factor) as u32,
486 (original_height as f32 * scale_factor) as u32,
487 image::imageops::FilterType::Gaussian,
488 );
489 }
490
491 (
492 image.to_rgba8(),
493 SourceFormat::Rgba,
494 Size { width: original_width, height: original_height },
495 )
496 })
497}
498
499#[cfg(feature = "software-renderer")]
500fn load_image(
501 file: crate::fileaccess::VirtualFile,
502 scale_factor: f32,
503 font_collection: Option<&SharedFontCollection>,
504) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
505 use std::ffi::OsStr;
506
507 let extension = file.canon_path.extension().and_then(OsStr::to_str);
508
509 let data = if let Some(buffer) = file.builtin_contents {
510 buffer.to_vec()
511 } else {
512 std::fs::read(&file.canon_path)?
513 };
514
515 load_image_from_bytes(&data, extension, scale_factor, font_collection)
516}
517
518fn embed_data_uri(
519 global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
520 path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
521 data_uri: &str,
522 _embed_files: EmbedResourcesKind,
523 _scale_factor: f32,
524 diag: &mut BuildDiagnostics,
525 source_location: &Option<crate::diagnostics::SourceLocation>,
526 _font_collection: Option<&SharedFontCollection>,
527) -> ImageReference {
528 if let Some(&resource_id) = path_to_id.get(data_uri) {
529 let resources = global_embedded_resources.borrow();
530 return match &resources[resource_id].kind {
531 #[cfg(feature = "software-renderer")]
532 EmbeddedResourcesKind::TextureData { .. } => {
533 ImageReference::EmbeddedTexture { resource_id }
534 }
535 EmbeddedResourcesKind::DataUriPayload(_, ext) => {
536 ImageReference::EmbeddedData { resource_id, extension: ext.clone() }
537 }
538 _ => ImageReference::None,
539 };
540 }
541
542 let (decoded_data, extension) = match crate::data_uri::decode_data_uri(data_uri) {
543 Ok(result) => result,
544 Err(e) => {
545 diag.push_error(e, source_location);
546 return ImageReference::None;
547 }
548 };
549
550 let mut resources = global_embedded_resources.borrow_mut();
551 let mut push = |kind| {
552 let id = resources.push_and_get_key(EmbeddedResources { path: None, kind });
553 path_to_id.insert(data_uri.into(), id);
554 id
555 };
556
557 #[cfg(feature = "software-renderer")]
558 if _embed_files == EmbedResourcesKind::EmbedTextures {
559 match load_image_from_bytes(
560 &decoded_data,
561 Some(&extension),
562 _scale_factor,
563 _font_collection,
564 )
565 .map_err(|e| e.to_string())
566 {
567 Ok((img, source_format, original_size)) => {
568 let resource_id = push(EmbeddedResourcesKind::TextureData(generate_texture(
569 img,
570 source_format,
571 original_size,
572 )));
573 return ImageReference::EmbeddedTexture { resource_id };
574 }
575 Err(err) => {
576 diag.push_error(format!("Cannot load data URI image: {err}"), source_location);
577 return ImageReference::None;
578 }
579 }
580 }
581
582 let resource_id = push(EmbeddedResourcesKind::DataUriPayload(decoded_data, extension.clone()));
583
584 ImageReference::EmbeddedData { resource_id, extension }
585}