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