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 = "renderer-software")]
10use image::GenericImageView;
11use smol_str::SmolStr;
12use std::cell::RefCell;
13use std::collections::HashMap;
14use typed_index_collections::TiVec;
15use url::Url;
16
17/// The fonts shared with `embed_glyphs` to rasterize SVG `<text>`. Only the
18/// software renderer embeds textures, so elsewhere this is an unused placeholder.
19#[cfg(feature = "renderer-software")]
20pub(crate) type SharedFontCollection = super::embed_glyphs::SharedFontCollection;
21#[cfg(not(feature = "renderer-software"))]
22pub(crate) type SharedFontCollection = ();
23
24pub async fn embed_images(
25    doc: &Document,
26    embed_files: EmbedResourcesKind,
27    scale_factor: f32,
28    resource_url_mapper: &Option<crate::ResourceUrlMapper>,
29    font_collection: Option<&SharedFontCollection>,
30    diag: &mut BuildDiagnostics,
31) {
32    // Slint SC always embeds: the images referenced by `@image-url()` are
33    // decoded into the generated code, whatever `embed_files` says.
34    if embed_files == EmbedResourcesKind::Nothing
35        && resource_url_mapper.is_none()
36        && !diag.is_slint_sc()
37    {
38        return;
39    }
40
41    let global_embedded_resources = &doc.embedded_file_resources;
42    let mut path_to_id = HashMap::<SmolStr, EmbeddedResourcesIdx>::new();
43
44    let mut all_components = Vec::new();
45    doc.visit_all_used_components(|c| all_components.push(c.clone()));
46    let all_components = all_components;
47
48    let mapped_urls = {
49        let mut urls = HashMap::<Url, Option<Url>>::new();
50
51        if let Some(mapper) = resource_url_mapper {
52            // Collect URLs (sync!):
53            for component in &all_components {
54                visit_all_expressions(component, |e, _| {
55                    collect_image_urls_from_expression(e, &mut urls)
56                });
57            }
58
59            // Map URLs (async -- well, not really):
60            for (url, mapped) in urls.iter_mut() {
61                *mapped = (*mapper)(url).await;
62            }
63        }
64
65        urls
66    };
67
68    // Use URLs (sync!):
69    for component in &all_components {
70        visit_all_expressions(component, |e, _| {
71            embed_images_from_expression(
72                e,
73                &mapped_urls,
74                global_embedded_resources,
75                &mut path_to_id,
76                embed_files,
77                scale_factor,
78                diag,
79                font_collection,
80            )
81        });
82    }
83}
84
85/// The URL handed to the resource mapper, and the key of the mapped-resource
86/// map, for a reference the mapper may rewrite. A local [`ImageReference::Path`]
87/// becomes a `file://` URL; an [`ImageReference::Url`] is used as-is. Everything
88/// else (`data:` URIs, already-embedded references) returns `None` and is left
89/// untouched.
90fn reference_mapper_url(resource_ref: &ImageReference) -> Option<Url> {
91    match resource_ref {
92        ImageReference::Url(url) => Some(url.clone()),
93        ImageReference::Path(path) => {
94            // `Url::from_file_path` is absent on `wasm32-unknown-unknown`, which
95            // only ever sees URL references and so never reaches this branch.
96            #[cfg(not(target_arch = "wasm32"))]
97            {
98                Url::from_file_path(path).ok()
99            }
100            #[cfg(target_arch = "wasm32")]
101            {
102                let _ = path;
103                None
104            }
105        }
106        _ => None,
107    }
108}
109
110fn collect_image_urls_from_expression(e: &Expression, urls: &mut HashMap<Url, Option<Url>>) {
111    if let Expression::ImageReference { resource_ref, .. } = e
112        && let Some(url) = reference_mapper_url(resource_ref)
113    {
114        urls.insert(url, None);
115    };
116
117    e.visit(|e| collect_image_urls_from_expression(e, urls));
118}
119
120fn embed_images_from_expression(
121    e: &mut Expression,
122    urls: &HashMap<Url, Option<Url>>,
123    global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
124    path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
125    embed_files: EmbedResourcesKind,
126    scale_factor: f32,
127    diag: &mut BuildDiagnostics,
128    font_collection: Option<&SharedFontCollection>,
129) {
130    if let Expression::ImageReference { resource_ref, source_location, nine_slice: _ } = e {
131        // Apply the resource mapper. A Path/Url may be replaced with the mapped
132        // URL (e.g. a `data:` URL), so re-classify the reference.
133        if let Some(url) = reference_mapper_url(resource_ref)
134            && let Some(mapped) = urls.get(&url).cloned().flatten()
135        {
136            *resource_ref = ImageReference::from_mapped_url(mapped);
137        }
138
139        match resource_ref {
140            ImageReference::DataUri(data) => {
141                // Data URIs have no external file to track, so skip for
142                // Nothing (interpreter) and ListAllResources (dependency tracking).
143                // Slint SC rejected the data URI when resolving @image-url(),
144                // so there is nothing left to embed.
145                if !matches!(
146                    embed_files,
147                    EmbedResourcesKind::Nothing | EmbedResourcesKind::ListAllResources
148                ) && !diag.is_slint_sc()
149                {
150                    let image_ref = embed_data_uri(
151                        global_embedded_resources,
152                        path_to_id,
153                        data,
154                        embed_files,
155                        scale_factor,
156                        diag,
157                        source_location,
158                        font_collection,
159                    );
160                    *resource_ref = image_ref;
161                }
162            }
163            ImageReference::Path(_) | ImageReference::Url(_) => {
164                let is_builtin = matches!(
165                    resource_ref,
166                    ImageReference::Url(url) if url.scheme() == "builtin"
167                );
168                // Slint SC rejected URL references when resolving @image-url(),
169                // so only paths are left to embed there.
170                let embed_for_slint_sc =
171                    diag.is_slint_sc() && matches!(resource_ref, ImageReference::Path(_));
172                if embed_for_slint_sc
173                    || (embed_files != EmbedResourcesKind::Nothing
174                        && (embed_files != EmbedResourcesKind::OnlyBuiltinResources || is_builtin))
175                {
176                    let path = resource_ref.source().expect("Path/Url have a source");
177                    let image_ref = embed_image(
178                        global_embedded_resources,
179                        path_to_id,
180                        embed_files,
181                        path,
182                        scale_factor,
183                        diag,
184                        source_location,
185                        font_collection,
186                    );
187                    if embed_files != EmbedResourcesKind::ListAllResources {
188                        *resource_ref = image_ref;
189                    }
190                }
191            }
192            ImageReference::None
193            | ImageReference::EmbeddedData { .. }
194            | ImageReference::EmbeddedTexture { .. } => {}
195        }
196    };
197
198    e.visit_mut(|e| {
199        embed_images_from_expression(
200            e,
201            urls,
202            global_embedded_resources,
203            path_to_id,
204            embed_files,
205            scale_factor,
206            diag,
207            font_collection,
208        )
209    });
210}
211
212fn embed_image(
213    global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
214    path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
215    embed_files: EmbedResourcesKind,
216    path: &str,
217    _scale_factor: f32,
218    diag: &mut BuildDiagnostics,
219    source_location: &Option<crate::diagnostics::SourceLocation>,
220    _font_collection: Option<&SharedFontCollection>,
221) -> ImageReference {
222    let extension = || {
223        std::path::Path::new(path)
224            .extension()
225            .and_then(|e| e.to_str())
226            .map(|x| x.to_string())
227            .unwrap_or_default()
228    };
229
230    if let Some(&resource_id) = path_to_id.get(path) {
231        return match global_embedded_resources.borrow()[resource_id].kind {
232            #[cfg(feature = "renderer-software")]
233            EmbeddedResourcesKind::TextureData { .. } => {
234                ImageReference::EmbeddedTexture { resource_id }
235            }
236            #[cfg(feature = "slint-sc")]
237            EmbeddedResourcesKind::StaticPixels { .. } => {
238                ImageReference::EmbeddedTexture { resource_id }
239            }
240            _ => ImageReference::EmbeddedData { resource_id, extension: extension() },
241        };
242    }
243
244    let mut resources = global_embedded_resources.borrow_mut();
245    let mut push = |kind| {
246        let id = resources.push_and_get_key(EmbeddedResources { path: Some(path.into()), kind });
247        path_to_id.insert(path.into(), id);
248        id
249    };
250
251    if embed_files == EmbedResourcesKind::ListAllResources {
252        push(EmbeddedResourcesKind::ListOnly);
253        return ImageReference::None;
254    }
255
256    let Some(_file) = crate::fileaccess::load_file(std::path::Path::new(path)) else {
257        diag.push_error(format!("Cannot find image file {path}"), source_location);
258        return ImageReference::None;
259    };
260
261    #[cfg(feature = "slint-sc")]
262    if diag.slint_sc {
263        // The Slint SC runtime has no image decoder: decode now and embed the
264        // pixels into the generated code.
265        if matches!(extension().to_ascii_lowercase().as_str(), "svg" | "svgz") {
266            diag.slint_sc_error("SVG images are", source_location);
267            return ImageReference::None;
268        }
269        return match image::load_from_memory(&_file.read()) {
270            Ok(decoded) => {
271                let resource_id = push(EmbeddedResourcesKind::StaticPixels(decoded.into_rgba8()));
272                ImageReference::EmbeddedTexture { resource_id }
273            }
274            Err(err) => {
275                diag.push_error(format!("Cannot load image file {path}: {err}"), source_location);
276                ImageReference::None
277            }
278        };
279    }
280
281    #[cfg(feature = "renderer-software")]
282    if embed_files == EmbedResourcesKind::EmbedTextures {
283        return match load_image(_file, _scale_factor, _font_collection) {
284            Ok((img, source_format, original_size)) => {
285                let resource_id = push(EmbeddedResourcesKind::TextureData(generate_texture(
286                    img,
287                    source_format,
288                    original_size,
289                )));
290                ImageReference::EmbeddedTexture { resource_id }
291            }
292            Err(err) => {
293                diag.push_error(format!("Cannot load image file {path}: {err}"), source_location);
294                ImageReference::None
295            }
296        };
297    }
298
299    let resource_id = push(EmbeddedResourcesKind::FileData);
300    ImageReference::EmbeddedData { resource_id, extension: extension() }
301}
302
303#[cfg(feature = "renderer-software")]
304trait Pixel {
305    //fn alpha(&self) -> f32;
306    //fn rgb(&self) -> (u8, u8, u8);
307    fn is_transparent(&self) -> bool;
308}
309#[cfg(feature = "renderer-software")]
310impl Pixel for image::Rgba<u8> {
311    /*fn alpha(&self) -> f32 { self[3] as f32 / 255. }
312    fn rgb(&self) -> (u8, u8, u8) { (self[0], self[1], self[2]) }*/
313    fn is_transparent(&self) -> bool {
314        self[3] <= 1
315    }
316}
317
318#[cfg(feature = "renderer-software")]
319fn generate_texture(
320    image: image::RgbaImage,
321    source_format: SourceFormat,
322    original_size: Size,
323) -> Texture {
324    // Analyze each pixels
325    let mut top = 0;
326    let is_line_transparent = |y| {
327        for x in 0..image.width() {
328            if !image.get_pixel(x, y).is_transparent() {
329                return false;
330            }
331        }
332        true
333    };
334    while top < image.height() && is_line_transparent(top) {
335        top += 1;
336    }
337    if top == image.height() {
338        return Texture::new_empty();
339    }
340    let mut bottom = image.height() - 1;
341    while is_line_transparent(bottom) {
342        bottom -= 1;
343        assert!(bottom > top); // otherwise we would have a transparent image
344    }
345    let is_column_transparent = |x| {
346        for y in top..=bottom {
347            if !image.get_pixel(x, y).is_transparent() {
348                return false;
349            }
350        }
351        true
352    };
353    let mut left = 0;
354    while is_column_transparent(left) {
355        left += 1;
356        assert!(left < image.width()); // otherwise we would have a transparent image
357    }
358    let mut right = image.width() - 1;
359    while is_column_transparent(right) {
360        right -= 1;
361        assert!(right > left); // otherwise we would have a transparent image
362    }
363    let mut is_opaque = true;
364    enum ColorState {
365        Unset,
366        Different,
367        Rgb([u8; 3]),
368    }
369    let mut color = ColorState::Unset;
370    'outer: for y in top..=bottom {
371        for x in left..=right {
372            let p = image.get_pixel(x, y);
373            let alpha = p[3];
374            if alpha != 255 {
375                is_opaque = false;
376            }
377            if alpha == 0 {
378                continue;
379            }
380            let get_pixel = || match source_format {
381                SourceFormat::RgbaPremultiplied => <[u8; 3]>::try_from(&p.0[0..3])
382                    .unwrap()
383                    .map(|v| (v as u16 * 255 / alpha as u16) as u8),
384                SourceFormat::Rgba => p.0[0..3].try_into().unwrap(),
385            };
386            match color {
387                ColorState::Unset => {
388                    color = ColorState::Rgb(get_pixel());
389                }
390                ColorState::Different => {
391                    if !is_opaque {
392                        break 'outer;
393                    }
394                }
395                ColorState::Rgb([a, b, c]) => {
396                    let px = get_pixel();
397                    if a.abs_diff(px[0]) > 2 || b.abs_diff(px[1]) > 2 || c.abs_diff(px[2]) > 2 {
398                        color = ColorState::Different
399                    }
400                }
401            }
402        }
403    }
404
405    let format = if let ColorState::Rgb(c) = color {
406        PixelFormat::AlphaMap(c)
407    } else if is_opaque {
408        PixelFormat::Rgb
409    } else {
410        PixelFormat::RgbaPremultiplied
411    };
412
413    let rect = Rect::from_ltrb(left as _, top as _, (right + 1) as _, (bottom + 1) as _).unwrap();
414    Texture {
415        total_size: Size { width: image.width(), height: image.height() },
416        original_size,
417        rect,
418        data: convert_image(image, source_format, format, rect),
419        format,
420    }
421}
422
423#[cfg(feature = "renderer-software")]
424fn convert_image(
425    image: image::RgbaImage,
426    source_format: SourceFormat,
427    format: PixelFormat,
428    rect: Rect,
429) -> Vec<u8> {
430    let i = image::SubImage::new(&image, rect.x() as _, rect.y() as _, rect.width(), rect.height());
431    match (source_format, format) {
432        (_, PixelFormat::Rgb) => {
433            i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0).take(3)).collect()
434        }
435        (SourceFormat::RgbaPremultiplied, PixelFormat::RgbaPremultiplied)
436        | (SourceFormat::Rgba, PixelFormat::Rgba) => {
437            i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0)).collect()
438        }
439        (SourceFormat::Rgba, PixelFormat::RgbaPremultiplied) => i
440            .pixels()
441            .flat_map(|(_, _, p)| {
442                let a = p.0[3] as u32;
443                IntoIterator::into_iter(p.0)
444                    .take(3)
445                    .map(move |x| (x as u32 * a / 255) as u8)
446                    .chain(std::iter::once(a as u8))
447            })
448            .collect(),
449        (SourceFormat::RgbaPremultiplied, PixelFormat::Rgba) => i
450            .pixels()
451            .flat_map(|(_, _, p)| {
452                let a = p.0[3] as u32;
453                IntoIterator::into_iter(p.0)
454                    .take(3)
455                    .map(move |x| (x as u32 * 255 / a) as u8)
456                    .chain(std::iter::once(a as u8))
457            })
458            .collect(),
459        (_, PixelFormat::AlphaMap(_)) => i.pixels().map(|(_, _, p)| p[3]).collect(),
460    }
461}
462
463#[cfg(feature = "renderer-software")]
464enum SourceFormat {
465    RgbaPremultiplied,
466    Rgba,
467}
468
469/// usvg renders SVG `<text>` against its own font database. The compiler has no
470/// `SlintContext`, so resolve those fonts against the collection shared with
471/// `embed_glyphs` (system fonts plus imported fonts) through the shared bridge.
472#[cfg(feature = "renderer-software")]
473fn svg_font_options(
474    font_collection: Option<&SharedFontCollection>,
475) -> resvg::usvg::Options<'static> {
476    use i_slint_common::sharedfontique::svg as svg_fonts;
477
478    let Some(font_collection) = font_collection.cloned() else {
479        return resvg::usvg::Options::default();
480    };
481    svg_fonts::options(move |families, attributes, require_char| {
482        let mut fonts = font_collection.lock().ok()?;
483        let collection = &mut fonts.collection;
484        svg_fonts::query_font(
485            &mut collection.inner,
486            &mut collection.source_cache,
487            families,
488            attributes,
489            require_char,
490        )
491    })
492}
493
494#[cfg(feature = "renderer-software")]
495fn load_image_from_bytes(
496    data: &[u8],
497    extension: Option<&str>,
498    scale_factor: f32,
499    font_collection: Option<&SharedFontCollection>,
500) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
501    use resvg::{tiny_skia, usvg};
502
503    let is_svg = matches!(extension, Some("svg") | Some("svgz"));
504
505    if is_svg {
506        let tree = {
507            usvg::Tree::from_data(data, &svg_font_options(font_collection)).map_err(|e| {
508                image::ImageError::Decoding(image::error::DecodingError::new(
509                    image::error::ImageFormatHint::Name("svg".into()),
510                    e,
511                ))
512            })?
513        };
514
515        let original_size = tree.size();
516        let width = (original_size.width() * scale_factor) as u32;
517        let height = (original_size.height() * scale_factor) as u32;
518
519        let mut buffer = vec![0u8; width as usize * height as usize * 4];
520
521        let size_error = || {
522            image::ImageError::Limits(image::error::LimitError::from_kind(
523                image::error::LimitErrorKind::DimensionError,
524            ))
525        };
526
527        let mut skia_buffer =
528            tiny_skia::PixmapMut::from_bytes(buffer.as_mut_slice(), width, height)
529                .ok_or_else(size_error)?;
530
531        resvg::render(
532            &tree,
533            tiny_skia::Transform::from_scale(scale_factor, scale_factor),
534            &mut skia_buffer,
535        );
536
537        return image::RgbaImage::from_raw(width, height, buffer).ok_or_else(size_error).map(
538            |img| {
539                (
540                    img,
541                    SourceFormat::RgbaPremultiplied,
542                    Size { width: original_size.width() as _, height: original_size.height() as _ },
543                )
544            },
545        );
546    }
547
548    image::load_from_memory(data).map(|mut image| {
549        let (original_width, original_height) = image.dimensions();
550
551        if scale_factor < 1.0 {
552            image = image.resize_exact(
553                (original_width as f32 * scale_factor) as u32,
554                (original_height as f32 * scale_factor) as u32,
555                image::imageops::FilterType::Gaussian,
556            );
557        }
558
559        (
560            image.to_rgba8(),
561            SourceFormat::Rgba,
562            Size { width: original_width, height: original_height },
563        )
564    })
565}
566
567#[cfg(feature = "renderer-software")]
568fn load_image(
569    file: crate::fileaccess::VirtualFile,
570    scale_factor: f32,
571    font_collection: Option<&SharedFontCollection>,
572) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
573    use std::ffi::OsStr;
574
575    let extension = file.canon_path.extension().and_then(OsStr::to_str);
576
577    let data = if let Some(buffer) = file.builtin_contents {
578        buffer.to_vec()
579    } else {
580        std::fs::read(&file.canon_path)?
581    };
582
583    load_image_from_bytes(&data, extension, scale_factor, font_collection)
584}
585
586fn embed_data_uri(
587    global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
588    path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
589    data_uri: &str,
590    _embed_files: EmbedResourcesKind,
591    _scale_factor: f32,
592    diag: &mut BuildDiagnostics,
593    source_location: &Option<crate::diagnostics::SourceLocation>,
594    _font_collection: Option<&SharedFontCollection>,
595) -> ImageReference {
596    if let Some(&resource_id) = path_to_id.get(data_uri) {
597        let resources = global_embedded_resources.borrow();
598        return match &resources[resource_id].kind {
599            #[cfg(feature = "renderer-software")]
600            EmbeddedResourcesKind::TextureData { .. } => {
601                ImageReference::EmbeddedTexture { resource_id }
602            }
603            EmbeddedResourcesKind::DataUriPayload(_, ext) => {
604                ImageReference::EmbeddedData { resource_id, extension: ext.clone() }
605            }
606            _ => ImageReference::None,
607        };
608    }
609
610    let (decoded_data, extension) = match crate::data_uri::decode_data_uri(data_uri) {
611        Ok(result) => result,
612        Err(e) => {
613            diag.push_error(e, source_location);
614            return ImageReference::None;
615        }
616    };
617
618    let mut resources = global_embedded_resources.borrow_mut();
619    let mut push = |kind| {
620        let id = resources.push_and_get_key(EmbeddedResources { path: None, kind });
621        path_to_id.insert(data_uri.into(), id);
622        id
623    };
624
625    #[cfg(feature = "renderer-software")]
626    if _embed_files == EmbedResourcesKind::EmbedTextures {
627        match load_image_from_bytes(
628            &decoded_data,
629            Some(&extension),
630            _scale_factor,
631            _font_collection,
632        )
633        .map_err(|e| e.to_string())
634        {
635            Ok((img, source_format, original_size)) => {
636                let resource_id = push(EmbeddedResourcesKind::TextureData(generate_texture(
637                    img,
638                    source_format,
639                    original_size,
640                )));
641                return ImageReference::EmbeddedTexture { resource_id };
642            }
643            Err(err) => {
644                diag.push_error(format!("Cannot load data URI image: {err}"), source_location);
645                return ImageReference::None;
646            }
647        }
648    }
649
650    let resource_id = push(EmbeddedResourcesKind::DataUriPayload(decoded_data, extension.clone()));
651
652    ImageReference::EmbeddedData { resource_id, extension }
653}