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
19/// The fonts shared with `embed_glyphs` to rasterize SVG `<text>`. Only the
20/// software renderer embeds textures, so elsewhere this is an unused placeholder.
21#[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            // Collect URLs (sync!):
50            for component in &all_components {
51                visit_all_expressions(component, |e, _| {
52                    collect_image_urls_from_expression(e, &mut urls)
53                });
54            }
55
56            // Map URLs (async -- well, not really):
57            for i in urls.iter_mut() {
58                *i.1 = (*mapper)(i.0).await.map(SmolStr::new);
59            }
60        }
61
62        urls
63    };
64
65    // Use URLs (sync!):
66    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            // Data URIs have no external file to track, so skip for
110            // Nothing (interpreter) and ListAllResources (dependency tracking).
111            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        // used mapped path:
131        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 alpha(&self) -> f32;
238    //fn rgb(&self) -> (u8, u8, u8);
239    fn is_transparent(&self) -> bool;
240}
241#[cfg(feature = "software-renderer")]
242impl Pixel for image::Rgba<u8> {
243    /*fn alpha(&self) -> f32 { self[3] as f32 / 255. }
244    fn rgb(&self) -> (u8, u8, u8) { (self[0], self[1], self[2]) }*/
245    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    // Analyze each pixels
257    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); // otherwise we would have a transparent image
276    }
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()); // otherwise we would have a transparent image
289    }
290    let mut right = image.width() - 1;
291    while is_column_transparent(right) {
292        right -= 1;
293        assert!(right > left); // otherwise we would have a transparent image
294    }
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/// usvg renders SVG `<text>` against its own font database. The compiler has no
402/// `SlintContext`, so resolve those fonts against the collection shared with
403/// `embed_glyphs` (system fonts plus imported fonts) through the shared bridge.
404#[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}