Skip to main content

i_slint_compiler/passes/
embed_images.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use 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
19pub async fn embed_images(
20    doc: &Document,
21    embed_files: EmbedResourcesKind,
22    scale_factor: f32,
23    resource_url_mapper: &Option<Rc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = Option<String>>>>>>,
24    diag: &mut BuildDiagnostics,
25) {
26    if embed_files == EmbedResourcesKind::Nothing && resource_url_mapper.is_none() {
27        return;
28    }
29
30    let global_embedded_resources = &doc.embedded_file_resources;
31    let mut path_to_id = HashMap::<SmolStr, EmbeddedResourcesIdx>::new();
32
33    let mut all_components = Vec::new();
34    doc.visit_all_used_components(|c| all_components.push(c.clone()));
35    let all_components = all_components;
36
37    let mapped_urls = {
38        let mut urls = HashMap::<SmolStr, Option<SmolStr>>::new();
39
40        if let Some(mapper) = resource_url_mapper {
41            // Collect URLs (sync!):
42            for component in &all_components {
43                visit_all_expressions(component, |e, _| {
44                    collect_image_urls_from_expression(e, &mut urls)
45                });
46            }
47
48            // Map URLs (async -- well, not really):
49            for i in urls.iter_mut() {
50                *i.1 = (*mapper)(i.0).await.map(SmolStr::new);
51            }
52        }
53
54        urls
55    };
56
57    // Use URLs (sync!):
58    for component in &all_components {
59        visit_all_expressions(component, |e, _| {
60            embed_images_from_expression(
61                e,
62                &mapped_urls,
63                global_embedded_resources,
64                &mut path_to_id,
65                embed_files,
66                scale_factor,
67                diag,
68            )
69        });
70    }
71}
72
73fn collect_image_urls_from_expression(
74    e: &Expression,
75    urls: &mut HashMap<SmolStr, Option<SmolStr>>,
76) {
77    if let Expression::ImageReference { resource_ref, .. } = e
78        && let ImageReference::AbsolutePath(path) = resource_ref
79    {
80        urls.insert(path.clone(), None);
81    };
82
83    e.visit(|e| collect_image_urls_from_expression(e, urls));
84}
85
86fn embed_images_from_expression(
87    e: &mut Expression,
88    urls: &HashMap<SmolStr, Option<SmolStr>>,
89    global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
90    path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
91    embed_files: EmbedResourcesKind,
92    scale_factor: f32,
93    diag: &mut BuildDiagnostics,
94) {
95    if let Expression::ImageReference { resource_ref, source_location, nine_slice: _ } = e
96        && let ImageReference::AbsolutePath(path) = resource_ref
97    {
98        if path.starts_with("data:") {
99            let image_ref = embed_data_uri(
100                global_embedded_resources,
101                path,
102                embed_files,
103                scale_factor,
104                diag,
105                source_location,
106            );
107            *resource_ref = image_ref;
108            return;
109        }
110
111        // used mapped path:
112        let mapped_path =
113            urls.get(path).unwrap_or(&Some(path.clone())).clone().unwrap_or(path.clone());
114        *path = mapped_path;
115        if embed_files != EmbedResourcesKind::Nothing
116            && (embed_files != EmbedResourcesKind::OnlyBuiltinResources
117                || path.starts_with("builtin:/"))
118        {
119            let image_ref = embed_image(
120                global_embedded_resources,
121                path_to_id,
122                embed_files,
123                path,
124                scale_factor,
125                diag,
126                source_location,
127            );
128            if embed_files != EmbedResourcesKind::ListAllResources {
129                *resource_ref = image_ref;
130            }
131        }
132    };
133
134    e.visit_mut(|e| {
135        embed_images_from_expression(
136            e,
137            urls,
138            global_embedded_resources,
139            path_to_id,
140            embed_files,
141            scale_factor,
142            diag,
143        )
144    });
145}
146
147fn embed_image(
148    global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
149    path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
150    embed_files: EmbedResourcesKind,
151    path: &str,
152    _scale_factor: f32,
153    diag: &mut BuildDiagnostics,
154    source_location: &Option<crate::diagnostics::SourceLocation>,
155) -> ImageReference {
156    let extension = || {
157        std::path::Path::new(path)
158            .extension()
159            .and_then(|e| e.to_str())
160            .map(|x| x.to_string())
161            .unwrap_or_default()
162    };
163
164    if let Some(&resource_id) = path_to_id.get(path) {
165        return match global_embedded_resources.borrow()[resource_id].kind {
166            #[cfg(feature = "software-renderer")]
167            EmbeddedResourcesKind::TextureData { .. } => {
168                ImageReference::EmbeddedTexture { resource_id }
169            }
170            _ => ImageReference::EmbeddedData { resource_id, extension: extension() },
171        };
172    }
173
174    let mut resources = global_embedded_resources.borrow_mut();
175    let mut push = |kind| {
176        let id = resources.push_and_get_key(EmbeddedResources { path: Some(path.into()), kind });
177        path_to_id.insert(path.into(), id);
178        id
179    };
180
181    if embed_files == EmbedResourcesKind::ListAllResources {
182        push(EmbeddedResourcesKind::ListOnly);
183        return ImageReference::None;
184    }
185
186    let Some(_file) = crate::fileaccess::load_file(std::path::Path::new(path)) else {
187        diag.push_error(format!("Cannot find image file {path}"), source_location);
188        return ImageReference::None;
189    };
190
191    #[cfg(feature = "software-renderer")]
192    if embed_files == EmbedResourcesKind::EmbedTextures {
193        return match load_image(_file, _scale_factor) {
194            Ok((img, source_format, original_size)) => {
195                let resource_id = push(EmbeddedResourcesKind::TextureData(generate_texture(
196                    img,
197                    source_format,
198                    original_size,
199                )));
200                ImageReference::EmbeddedTexture { resource_id }
201            }
202            Err(err) => {
203                diag.push_error(format!("Cannot load image file {path}: {err}"), source_location);
204                ImageReference::None
205            }
206        };
207    }
208
209    let resource_id = push(EmbeddedResourcesKind::FileData);
210    ImageReference::EmbeddedData { resource_id, extension: extension() }
211}
212
213#[cfg(feature = "software-renderer")]
214trait Pixel {
215    //fn alpha(&self) -> f32;
216    //fn rgb(&self) -> (u8, u8, u8);
217    fn is_transparent(&self) -> bool;
218}
219#[cfg(feature = "software-renderer")]
220impl Pixel for image::Rgba<u8> {
221    /*fn alpha(&self) -> f32 { self[3] as f32 / 255. }
222    fn rgb(&self) -> (u8, u8, u8) { (self[0], self[1], self[2]) }*/
223    fn is_transparent(&self) -> bool {
224        self[3] <= 1
225    }
226}
227
228#[cfg(feature = "software-renderer")]
229fn generate_texture(
230    image: image::RgbaImage,
231    source_format: SourceFormat,
232    original_size: Size,
233) -> Texture {
234    // Analyze each pixels
235    let mut top = 0;
236    let is_line_transparent = |y| {
237        for x in 0..image.width() {
238            if !image.get_pixel(x, y).is_transparent() {
239                return false;
240            }
241        }
242        true
243    };
244    while top < image.height() && is_line_transparent(top) {
245        top += 1;
246    }
247    if top == image.height() {
248        return Texture::new_empty();
249    }
250    let mut bottom = image.height() - 1;
251    while is_line_transparent(bottom) {
252        bottom -= 1;
253        assert!(bottom > top); // otherwise we would have a transparent image
254    }
255    let is_column_transparent = |x| {
256        for y in top..=bottom {
257            if !image.get_pixel(x, y).is_transparent() {
258                return false;
259            }
260        }
261        true
262    };
263    let mut left = 0;
264    while is_column_transparent(left) {
265        left += 1;
266        assert!(left < image.width()); // otherwise we would have a transparent image
267    }
268    let mut right = image.width() - 1;
269    while is_column_transparent(right) {
270        right -= 1;
271        assert!(right > left); // otherwise we would have a transparent image
272    }
273    let mut is_opaque = true;
274    enum ColorState {
275        Unset,
276        Different,
277        Rgb([u8; 3]),
278    }
279    let mut color = ColorState::Unset;
280    'outer: for y in top..=bottom {
281        for x in left..=right {
282            let p = image.get_pixel(x, y);
283            let alpha = p[3];
284            if alpha != 255 {
285                is_opaque = false;
286            }
287            if alpha == 0 {
288                continue;
289            }
290            let get_pixel = || match source_format {
291                SourceFormat::RgbaPremultiplied => <[u8; 3]>::try_from(&p.0[0..3])
292                    .unwrap()
293                    .map(|v| (v as u16 * 255 / alpha as u16) as u8),
294                SourceFormat::Rgba => p.0[0..3].try_into().unwrap(),
295            };
296            match color {
297                ColorState::Unset => {
298                    color = ColorState::Rgb(get_pixel());
299                }
300                ColorState::Different => {
301                    if !is_opaque {
302                        break 'outer;
303                    }
304                }
305                ColorState::Rgb([a, b, c]) => {
306                    let px = get_pixel();
307                    if a.abs_diff(px[0]) > 2 || b.abs_diff(px[1]) > 2 || c.abs_diff(px[2]) > 2 {
308                        color = ColorState::Different
309                    }
310                }
311            }
312        }
313    }
314
315    let format = if let ColorState::Rgb(c) = color {
316        PixelFormat::AlphaMap(c)
317    } else if is_opaque {
318        PixelFormat::Rgb
319    } else {
320        PixelFormat::RgbaPremultiplied
321    };
322
323    let rect = Rect::from_ltrb(left as _, top as _, (right + 1) as _, (bottom + 1) as _).unwrap();
324    Texture {
325        total_size: Size { width: image.width(), height: image.height() },
326        original_size,
327        rect,
328        data: convert_image(image, source_format, format, rect),
329        format,
330    }
331}
332
333#[cfg(feature = "software-renderer")]
334fn convert_image(
335    image: image::RgbaImage,
336    source_format: SourceFormat,
337    format: PixelFormat,
338    rect: Rect,
339) -> Vec<u8> {
340    let i = image::SubImage::new(&image, rect.x() as _, rect.y() as _, rect.width(), rect.height());
341    match (source_format, format) {
342        (_, PixelFormat::Rgb) => {
343            i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0).take(3)).collect()
344        }
345        (SourceFormat::RgbaPremultiplied, PixelFormat::RgbaPremultiplied)
346        | (SourceFormat::Rgba, PixelFormat::Rgba) => {
347            i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0)).collect()
348        }
349        (SourceFormat::Rgba, PixelFormat::RgbaPremultiplied) => i
350            .pixels()
351            .flat_map(|(_, _, p)| {
352                let a = p.0[3] as u32;
353                IntoIterator::into_iter(p.0)
354                    .take(3)
355                    .map(move |x| (x as u32 * a / 255) as u8)
356                    .chain(std::iter::once(a as u8))
357            })
358            .collect(),
359        (SourceFormat::RgbaPremultiplied, PixelFormat::Rgba) => i
360            .pixels()
361            .flat_map(|(_, _, p)| {
362                let a = p.0[3] as u32;
363                IntoIterator::into_iter(p.0)
364                    .take(3)
365                    .map(move |x| (x as u32 * 255 / a) as u8)
366                    .chain(std::iter::once(a as u8))
367            })
368            .collect(),
369        (_, PixelFormat::AlphaMap(_)) => i.pixels().map(|(_, _, p)| p[3]).collect(),
370    }
371}
372
373#[cfg(feature = "software-renderer")]
374enum SourceFormat {
375    RgbaPremultiplied,
376    Rgba,
377}
378
379#[cfg(feature = "software-renderer")]
380fn load_image_from_bytes(
381    data: &[u8],
382    extension: Option<&str>,
383    scale_factor: f32,
384) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
385    use resvg::{tiny_skia, usvg};
386
387    let is_svg = matches!(extension, Some("svg") | Some("svgz"));
388
389    if is_svg {
390        let tree = {
391            let option = usvg::Options::default();
392            usvg::Tree::from_data(data, &option).map_err(|e| {
393                image::ImageError::Decoding(image::error::DecodingError::new(
394                    image::error::ImageFormatHint::Name("svg".into()),
395                    e,
396                ))
397            })?
398        };
399
400        let original_size = tree.size();
401        let width = (original_size.width() * scale_factor) as u32;
402        let height = (original_size.height() * scale_factor) as u32;
403
404        let mut buffer = vec![0u8; width as usize * height as usize * 4];
405
406        let size_error = || {
407            image::ImageError::Limits(image::error::LimitError::from_kind(
408                image::error::LimitErrorKind::DimensionError,
409            ))
410        };
411
412        let mut skia_buffer =
413            tiny_skia::PixmapMut::from_bytes(buffer.as_mut_slice(), width, height)
414                .ok_or_else(size_error)?;
415
416        resvg::render(
417            &tree,
418            tiny_skia::Transform::from_scale(scale_factor, scale_factor),
419            &mut skia_buffer,
420        );
421
422        return image::RgbaImage::from_raw(width, height, buffer).ok_or_else(size_error).map(
423            |img| {
424                (
425                    img,
426                    SourceFormat::RgbaPremultiplied,
427                    Size { width: original_size.width() as _, height: original_size.height() as _ },
428                )
429            },
430        );
431    }
432
433    image::load_from_memory(data).map(|mut image| {
434        let (original_width, original_height) = image.dimensions();
435
436        if scale_factor < 1.0 {
437            image = image.resize_exact(
438                (original_width as f32 * scale_factor) as u32,
439                (original_height as f32 * scale_factor) as u32,
440                image::imageops::FilterType::Gaussian,
441            );
442        }
443
444        (
445            image.to_rgba8(),
446            SourceFormat::Rgba,
447            Size { width: original_width, height: original_height },
448        )
449    })
450}
451
452#[cfg(feature = "software-renderer")]
453fn load_image(
454    file: crate::fileaccess::VirtualFile,
455    scale_factor: f32,
456) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
457    use std::ffi::OsStr;
458
459    let extension = file.canon_path.extension().and_then(OsStr::to_str);
460
461    let data = if let Some(buffer) = file.builtin_contents {
462        buffer.to_vec()
463    } else {
464        std::fs::read(&file.canon_path)?
465    };
466
467    load_image_from_bytes(&data, extension, scale_factor)
468}
469
470#[cfg(feature = "software-renderer")]
471fn load_image_from_data_uri(
472    decoded_data: &[u8],
473    extension: &str,
474    scale_factor: f32,
475) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
476    load_image_from_bytes(decoded_data, Some(extension), scale_factor)
477}
478
479fn embed_data_uri(
480    global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
481    data_uri: &str,
482    _embed_files: EmbedResourcesKind,
483    _scale_factor: f32,
484    diag: &mut BuildDiagnostics,
485    source_location: &Option<crate::diagnostics::SourceLocation>,
486) -> ImageReference {
487    let (decoded_data, extension) = match crate::data_uri::decode_data_uri(data_uri) {
488        Ok(result) => result,
489        Err(e) => {
490            diag.push_error(e, source_location);
491            return ImageReference::None;
492        }
493    };
494
495    let mut resources = global_embedded_resources.borrow_mut();
496
497    #[cfg(feature = "software-renderer")]
498    if _embed_files == EmbedResourcesKind::EmbedTextures {
499        let data_buffer = decoded_data.clone();
500        match load_image_from_data_uri(&data_buffer, &extension, _scale_factor)
501            .map_err(|e| e.to_string())
502        {
503            Ok((img, source_format, original_size)) => {
504                let resource_id = resources.push_and_get_key(EmbeddedResources {
505                    path: None,
506                    kind: EmbeddedResourcesKind::TextureData(generate_texture(
507                        img,
508                        source_format,
509                        original_size,
510                    )),
511                });
512                return ImageReference::EmbeddedTexture { resource_id };
513            }
514            Err(err) => {
515                diag.push_error(format!("Cannot load data URI image: {err}"), source_location);
516                return ImageReference::None;
517            }
518        }
519    }
520
521    let resource_id = resources.push_and_get_key(EmbeddedResources {
522        path: None,
523        kind: EmbeddedResourcesKind::DataUriPayload(decoded_data, extension.clone()),
524    });
525
526    ImageReference::EmbeddedData { resource_id, extension }
527}