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::diagnostics::BuildDiagnostics;
5use crate::embedded_resources::*;
6use crate::expression_tree::{Expression, ImageReference};
7use crate::object_tree::*;
8use crate::EmbedResourcesKind;
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: f64,
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 { ref resource_ref, .. } = e {
75        if let ImageReference::AbsolutePath(path) = resource_ref {
76            urls.insert(path.clone(), None);
77        }
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: f64,
89    diag: &mut BuildDiagnostics,
90) {
91    if let Expression::ImageReference { ref mut resource_ref, source_location, nine_slice: _ } = e {
92        if let ImageReference::AbsolutePath(path) = resource_ref {
93            // used mapped path:
94            let mapped_path =
95                urls.get(path).unwrap_or(&Some(path.clone())).clone().unwrap_or(path.clone());
96            *path = mapped_path;
97            if embed_files != EmbedResourcesKind::Nothing
98                && (embed_files != EmbedResourcesKind::OnlyBuiltinResources
99                    || path.starts_with("builtin:/"))
100            {
101                let image_ref = embed_image(
102                    global_embedded_resources,
103                    embed_files,
104                    path,
105                    scale_factor,
106                    diag,
107                    source_location,
108                );
109                if embed_files != EmbedResourcesKind::ListAllResources {
110                    *resource_ref = image_ref;
111                }
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: f64,
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 {
287                            u - t
288                        } else {
289                            t - u
290                        }
291                    };
292                    let px = get_pixel();
293                    if abs_diff(a, px[0]) > 2 || abs_diff(b, px[1]) > 2 || abs_diff(c, px[2]) > 2 {
294                        color = ColorState::Different
295                    }
296                }
297            }
298        }
299    }
300
301    let format = if let ColorState::Rgb(c) = color {
302        PixelFormat::AlphaMap(c)
303    } else if is_opaque {
304        PixelFormat::Rgb
305    } else {
306        PixelFormat::RgbaPremultiplied
307    };
308
309    let rect = Rect::from_ltrb(left as _, top as _, (right + 1) as _, (bottom + 1) as _).unwrap();
310    Texture {
311        total_size: Size { width: image.width(), height: image.height() },
312        original_size,
313        rect,
314        data: convert_image(image, source_format, format, rect),
315        format,
316    }
317}
318
319#[cfg(feature = "software-renderer")]
320fn convert_image(
321    image: image::RgbaImage,
322    source_format: SourceFormat,
323    format: PixelFormat,
324    rect: Rect,
325) -> Vec<u8> {
326    let i = image::SubImage::new(&image, rect.x() as _, rect.y() as _, rect.width(), rect.height());
327    match (source_format, format) {
328        (_, PixelFormat::Rgb) => {
329            i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0).take(3)).collect()
330        }
331        (SourceFormat::RgbaPremultiplied, PixelFormat::RgbaPremultiplied)
332        | (SourceFormat::Rgba, PixelFormat::Rgba) => {
333            i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0)).collect()
334        }
335        (SourceFormat::Rgba, PixelFormat::RgbaPremultiplied) => i
336            .pixels()
337            .flat_map(|(_, _, p)| {
338                let a = p.0[3] as u32;
339                IntoIterator::into_iter(p.0)
340                    .take(3)
341                    .map(move |x| (x as u32 * a / 255) as u8)
342                    .chain(std::iter::once(a as u8))
343            })
344            .collect(),
345        (SourceFormat::RgbaPremultiplied, PixelFormat::Rgba) => i
346            .pixels()
347            .flat_map(|(_, _, p)| {
348                let a = p.0[3] as u32;
349                IntoIterator::into_iter(p.0)
350                    .take(3)
351                    .map(move |x| (x as u32 * 255 / a) as u8)
352                    .chain(std::iter::once(a as u8))
353            })
354            .collect(),
355        (_, PixelFormat::AlphaMap(_)) => i.pixels().map(|(_, _, p)| p[3]).collect(),
356    }
357}
358
359#[cfg(feature = "software-renderer")]
360enum SourceFormat {
361    RgbaPremultiplied,
362    Rgba,
363}
364
365#[cfg(feature = "software-renderer")]
366fn load_image(
367    file: crate::fileaccess::VirtualFile,
368    scale_factor: f64,
369) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
370    use resvg::{tiny_skia, usvg};
371    use std::ffi::OsStr;
372    if file.canon_path.extension() == Some(OsStr::new("svg"))
373        || file.canon_path.extension() == Some(OsStr::new("svgz"))
374    {
375        let tree = {
376            let option = usvg::Options::default();
377            match file.builtin_contents {
378                Some(data) => usvg::Tree::from_data(data, &option),
379                None => usvg::Tree::from_data(
380                    std::fs::read(&file.canon_path).map_err(image::ImageError::IoError)?.as_slice(),
381                    &option,
382                ),
383            }
384            .map_err(|e| {
385                image::ImageError::Decoding(image::error::DecodingError::new(
386                    image::error::ImageFormatHint::Name("svg".into()),
387                    e,
388                ))
389            })
390        }?;
391        let scale_factor = scale_factor as f32;
392        // TODO: ideally we should find the size used for that `Image`
393        let original_size = tree.size();
394        let width = original_size.width() * scale_factor;
395        let height = original_size.height() * scale_factor;
396
397        let mut buffer = vec![0u8; width as usize * height as usize * 4];
398        let size_error = || {
399            image::ImageError::Limits(image::error::LimitError::from_kind(
400                image::error::LimitErrorKind::DimensionError,
401            ))
402        };
403        let mut skia_buffer =
404            tiny_skia::PixmapMut::from_bytes(buffer.as_mut_slice(), width as u32, height as u32)
405                .ok_or_else(size_error)?;
406        resvg::render(
407            &tree,
408            tiny_skia::Transform::from_scale(scale_factor as _, scale_factor as _),
409            &mut skia_buffer,
410        );
411        return image::RgbaImage::from_raw(width as u32, height as u32, buffer)
412            .ok_or_else(size_error)
413            .map(|img| {
414                (
415                    img,
416                    SourceFormat::RgbaPremultiplied,
417                    Size { width: original_size.width() as _, height: original_size.height() as _ },
418                )
419            });
420    }
421    if let Some(buffer) = file.builtin_contents {
422        image::load_from_memory(buffer)
423    } else {
424        image::open(file.canon_path)
425    }
426    .map(|mut image| {
427        let (original_width, original_height) = image.dimensions();
428
429        if scale_factor < 1. {
430            image = image.resize_exact(
431                (original_width as f64 * scale_factor) as u32,
432                (original_height as f64 * scale_factor) as u32,
433                image::imageops::FilterType::Gaussian,
434            );
435        }
436
437        (
438            image.to_rgba8(),
439            SourceFormat::Rgba,
440            Size { width: original_width, height: original_height },
441        )
442    })
443}