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
82/// The URL of an image reference, as expected by the resource mapper and used
83/// to key the map of mapped resources. A local image reference is an absolute
84/// filesystem path at this stage, so turn it into a `file://` URL; references
85/// that are already URLs (`data:`, `builtin:/`, `https:`, ...) pass through
86/// unchanged.
87fn image_reference_url(resource: &str) -> SmolStr {
88    if crate::pathutils::is_url(std::path::Path::new(resource)) {
89        return resource.into();
90    }
91    // `Url::from_file_path` is absent on `wasm32-unknown-unknown`, which only
92    // ever sees URL references and so never reaches this branch.
93    #[cfg(not(target_arch = "wasm32"))]
94    {
95        url::Url::from_file_path(resource)
96            .map(|url| SmolStr::from(url.as_str()))
97            .unwrap_or_else(|()| resource.into())
98    }
99    #[cfg(target_arch = "wasm32")]
100    {
101        resource.into()
102    }
103}
104
105fn collect_image_urls_from_expression(
106    e: &Expression,
107    urls: &mut HashMap<SmolStr, Option<SmolStr>>,
108) {
109    if let Expression::ImageReference { resource_ref, .. } = e
110        && let ImageReference::AbsolutePath(path) = resource_ref
111        // `data:` URIs are embedded directly and never looked up in this map, so
112        // don't store their (potentially huge) content as a key.
113        && !path.starts_with("data:")
114    {
115        urls.insert(image_reference_url(path), None);
116    };
117
118    e.visit(|e| collect_image_urls_from_expression(e, urls));
119}
120
121fn embed_images_from_expression(
122    e: &mut Expression,
123    urls: &HashMap<SmolStr, Option<SmolStr>>,
124    global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
125    path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
126    embed_files: EmbedResourcesKind,
127    scale_factor: f32,
128    diag: &mut BuildDiagnostics,
129    font_collection: Option<&SharedFontCollection>,
130) {
131    if let Expression::ImageReference { resource_ref, source_location, nine_slice: _ } = e
132        && let ImageReference::AbsolutePath(path) = resource_ref
133    {
134        if path.starts_with("data:") {
135            // Data URIs have no external file to track, so skip for
136            // Nothing (interpreter) and ListAllResources (dependency tracking).
137            if !matches!(
138                embed_files,
139                EmbedResourcesKind::Nothing | EmbedResourcesKind::ListAllResources
140            ) {
141                let image_ref = embed_data_uri(
142                    global_embedded_resources,
143                    path_to_id,
144                    path,
145                    embed_files,
146                    scale_factor,
147                    diag,
148                    source_location,
149                    font_collection,
150                );
151                *resource_ref = image_ref;
152            }
153            return;
154        }
155
156        // use the mapped url, falling back to the original path:
157        let mapped_path =
158            urls.get(&image_reference_url(path)).cloned().flatten().unwrap_or_else(|| path.clone());
159        *path = mapped_path;
160        if embed_files != EmbedResourcesKind::Nothing
161            && (embed_files != EmbedResourcesKind::OnlyBuiltinResources
162                || path.starts_with("builtin:/"))
163        {
164            let image_ref = embed_image(
165                global_embedded_resources,
166                path_to_id,
167                embed_files,
168                path,
169                scale_factor,
170                diag,
171                source_location,
172                font_collection,
173            );
174            if embed_files != EmbedResourcesKind::ListAllResources {
175                *resource_ref = image_ref;
176            }
177        }
178    };
179
180    e.visit_mut(|e| {
181        embed_images_from_expression(
182            e,
183            urls,
184            global_embedded_resources,
185            path_to_id,
186            embed_files,
187            scale_factor,
188            diag,
189            font_collection,
190        )
191    });
192}
193
194fn embed_image(
195    global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
196    path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
197    embed_files: EmbedResourcesKind,
198    path: &str,
199    _scale_factor: f32,
200    diag: &mut BuildDiagnostics,
201    source_location: &Option<crate::diagnostics::SourceLocation>,
202    _font_collection: Option<&SharedFontCollection>,
203) -> ImageReference {
204    let extension = || {
205        std::path::Path::new(path)
206            .extension()
207            .and_then(|e| e.to_str())
208            .map(|x| x.to_string())
209            .unwrap_or_default()
210    };
211
212    if let Some(&resource_id) = path_to_id.get(path) {
213        return match global_embedded_resources.borrow()[resource_id].kind {
214            #[cfg(feature = "software-renderer")]
215            EmbeddedResourcesKind::TextureData { .. } => {
216                ImageReference::EmbeddedTexture { resource_id }
217            }
218            _ => ImageReference::EmbeddedData { resource_id, extension: extension() },
219        };
220    }
221
222    let mut resources = global_embedded_resources.borrow_mut();
223    let mut push = |kind| {
224        let id = resources.push_and_get_key(EmbeddedResources { path: Some(path.into()), kind });
225        path_to_id.insert(path.into(), id);
226        id
227    };
228
229    if embed_files == EmbedResourcesKind::ListAllResources {
230        push(EmbeddedResourcesKind::ListOnly);
231        return ImageReference::None;
232    }
233
234    let Some(_file) = crate::fileaccess::load_file(std::path::Path::new(path)) else {
235        diag.push_error(format!("Cannot find image file {path}"), source_location);
236        return ImageReference::None;
237    };
238
239    #[cfg(feature = "software-renderer")]
240    if embed_files == EmbedResourcesKind::EmbedTextures {
241        return match load_image(_file, _scale_factor, _font_collection) {
242            Ok((img, source_format, original_size)) => {
243                let resource_id = push(EmbeddedResourcesKind::TextureData(generate_texture(
244                    img,
245                    source_format,
246                    original_size,
247                )));
248                ImageReference::EmbeddedTexture { resource_id }
249            }
250            Err(err) => {
251                diag.push_error(format!("Cannot load image file {path}: {err}"), source_location);
252                ImageReference::None
253            }
254        };
255    }
256
257    let resource_id = push(EmbeddedResourcesKind::FileData);
258    ImageReference::EmbeddedData { resource_id, extension: extension() }
259}
260
261#[cfg(feature = "software-renderer")]
262trait Pixel {
263    //fn alpha(&self) -> f32;
264    //fn rgb(&self) -> (u8, u8, u8);
265    fn is_transparent(&self) -> bool;
266}
267#[cfg(feature = "software-renderer")]
268impl Pixel for image::Rgba<u8> {
269    /*fn alpha(&self) -> f32 { self[3] as f32 / 255. }
270    fn rgb(&self) -> (u8, u8, u8) { (self[0], self[1], self[2]) }*/
271    fn is_transparent(&self) -> bool {
272        self[3] <= 1
273    }
274}
275
276#[cfg(feature = "software-renderer")]
277fn generate_texture(
278    image: image::RgbaImage,
279    source_format: SourceFormat,
280    original_size: Size,
281) -> Texture {
282    // Analyze each pixels
283    let mut top = 0;
284    let is_line_transparent = |y| {
285        for x in 0..image.width() {
286            if !image.get_pixel(x, y).is_transparent() {
287                return false;
288            }
289        }
290        true
291    };
292    while top < image.height() && is_line_transparent(top) {
293        top += 1;
294    }
295    if top == image.height() {
296        return Texture::new_empty();
297    }
298    let mut bottom = image.height() - 1;
299    while is_line_transparent(bottom) {
300        bottom -= 1;
301        assert!(bottom > top); // otherwise we would have a transparent image
302    }
303    let is_column_transparent = |x| {
304        for y in top..=bottom {
305            if !image.get_pixel(x, y).is_transparent() {
306                return false;
307            }
308        }
309        true
310    };
311    let mut left = 0;
312    while is_column_transparent(left) {
313        left += 1;
314        assert!(left < image.width()); // otherwise we would have a transparent image
315    }
316    let mut right = image.width() - 1;
317    while is_column_transparent(right) {
318        right -= 1;
319        assert!(right > left); // otherwise we would have a transparent image
320    }
321    let mut is_opaque = true;
322    enum ColorState {
323        Unset,
324        Different,
325        Rgb([u8; 3]),
326    }
327    let mut color = ColorState::Unset;
328    'outer: for y in top..=bottom {
329        for x in left..=right {
330            let p = image.get_pixel(x, y);
331            let alpha = p[3];
332            if alpha != 255 {
333                is_opaque = false;
334            }
335            if alpha == 0 {
336                continue;
337            }
338            let get_pixel = || match source_format {
339                SourceFormat::RgbaPremultiplied => <[u8; 3]>::try_from(&p.0[0..3])
340                    .unwrap()
341                    .map(|v| (v as u16 * 255 / alpha as u16) as u8),
342                SourceFormat::Rgba => p.0[0..3].try_into().unwrap(),
343            };
344            match color {
345                ColorState::Unset => {
346                    color = ColorState::Rgb(get_pixel());
347                }
348                ColorState::Different => {
349                    if !is_opaque {
350                        break 'outer;
351                    }
352                }
353                ColorState::Rgb([a, b, c]) => {
354                    let px = get_pixel();
355                    if a.abs_diff(px[0]) > 2 || b.abs_diff(px[1]) > 2 || c.abs_diff(px[2]) > 2 {
356                        color = ColorState::Different
357                    }
358                }
359            }
360        }
361    }
362
363    let format = if let ColorState::Rgb(c) = color {
364        PixelFormat::AlphaMap(c)
365    } else if is_opaque {
366        PixelFormat::Rgb
367    } else {
368        PixelFormat::RgbaPremultiplied
369    };
370
371    let rect = Rect::from_ltrb(left as _, top as _, (right + 1) as _, (bottom + 1) as _).unwrap();
372    Texture {
373        total_size: Size { width: image.width(), height: image.height() },
374        original_size,
375        rect,
376        data: convert_image(image, source_format, format, rect),
377        format,
378    }
379}
380
381#[cfg(feature = "software-renderer")]
382fn convert_image(
383    image: image::RgbaImage,
384    source_format: SourceFormat,
385    format: PixelFormat,
386    rect: Rect,
387) -> Vec<u8> {
388    let i = image::SubImage::new(&image, rect.x() as _, rect.y() as _, rect.width(), rect.height());
389    match (source_format, format) {
390        (_, PixelFormat::Rgb) => {
391            i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0).take(3)).collect()
392        }
393        (SourceFormat::RgbaPremultiplied, PixelFormat::RgbaPremultiplied)
394        | (SourceFormat::Rgba, PixelFormat::Rgba) => {
395            i.pixels().flat_map(|(_, _, p)| IntoIterator::into_iter(p.0)).collect()
396        }
397        (SourceFormat::Rgba, PixelFormat::RgbaPremultiplied) => i
398            .pixels()
399            .flat_map(|(_, _, p)| {
400                let a = p.0[3] as u32;
401                IntoIterator::into_iter(p.0)
402                    .take(3)
403                    .map(move |x| (x as u32 * a / 255) as u8)
404                    .chain(std::iter::once(a as u8))
405            })
406            .collect(),
407        (SourceFormat::RgbaPremultiplied, PixelFormat::Rgba) => i
408            .pixels()
409            .flat_map(|(_, _, p)| {
410                let a = p.0[3] as u32;
411                IntoIterator::into_iter(p.0)
412                    .take(3)
413                    .map(move |x| (x as u32 * 255 / a) as u8)
414                    .chain(std::iter::once(a as u8))
415            })
416            .collect(),
417        (_, PixelFormat::AlphaMap(_)) => i.pixels().map(|(_, _, p)| p[3]).collect(),
418    }
419}
420
421#[cfg(feature = "software-renderer")]
422enum SourceFormat {
423    RgbaPremultiplied,
424    Rgba,
425}
426
427/// usvg renders SVG `<text>` against its own font database. The compiler has no
428/// `SlintContext`, so resolve those fonts against the collection shared with
429/// `embed_glyphs` (system fonts plus imported fonts) through the shared bridge.
430#[cfg(feature = "software-renderer")]
431fn svg_font_options(
432    font_collection: Option<&SharedFontCollection>,
433) -> resvg::usvg::Options<'static> {
434    use i_slint_common::sharedfontique::svg as svg_fonts;
435
436    let Some(font_collection) = font_collection.cloned() else {
437        return resvg::usvg::Options::default();
438    };
439    svg_fonts::options(move |families, attributes, require_char| {
440        let mut fonts = font_collection.lock().ok()?;
441        let collection = &mut fonts.collection;
442        svg_fonts::query_font(
443            &mut collection.inner,
444            &mut collection.source_cache,
445            families,
446            attributes,
447            require_char,
448        )
449    })
450}
451
452#[cfg(feature = "software-renderer")]
453fn load_image_from_bytes(
454    data: &[u8],
455    extension: Option<&str>,
456    scale_factor: f32,
457    font_collection: Option<&SharedFontCollection>,
458) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
459    use resvg::{tiny_skia, usvg};
460
461    let is_svg = matches!(extension, Some("svg") | Some("svgz"));
462
463    if is_svg {
464        let tree = {
465            usvg::Tree::from_data(data, &svg_font_options(font_collection)).map_err(|e| {
466                image::ImageError::Decoding(image::error::DecodingError::new(
467                    image::error::ImageFormatHint::Name("svg".into()),
468                    e,
469                ))
470            })?
471        };
472
473        let original_size = tree.size();
474        let width = (original_size.width() * scale_factor) as u32;
475        let height = (original_size.height() * scale_factor) as u32;
476
477        let mut buffer = vec![0u8; width as usize * height as usize * 4];
478
479        let size_error = || {
480            image::ImageError::Limits(image::error::LimitError::from_kind(
481                image::error::LimitErrorKind::DimensionError,
482            ))
483        };
484
485        let mut skia_buffer =
486            tiny_skia::PixmapMut::from_bytes(buffer.as_mut_slice(), width, height)
487                .ok_or_else(size_error)?;
488
489        resvg::render(
490            &tree,
491            tiny_skia::Transform::from_scale(scale_factor, scale_factor),
492            &mut skia_buffer,
493        );
494
495        return image::RgbaImage::from_raw(width, height, buffer).ok_or_else(size_error).map(
496            |img| {
497                (
498                    img,
499                    SourceFormat::RgbaPremultiplied,
500                    Size { width: original_size.width() as _, height: original_size.height() as _ },
501                )
502            },
503        );
504    }
505
506    image::load_from_memory(data).map(|mut image| {
507        let (original_width, original_height) = image.dimensions();
508
509        if scale_factor < 1.0 {
510            image = image.resize_exact(
511                (original_width as f32 * scale_factor) as u32,
512                (original_height as f32 * scale_factor) as u32,
513                image::imageops::FilterType::Gaussian,
514            );
515        }
516
517        (
518            image.to_rgba8(),
519            SourceFormat::Rgba,
520            Size { width: original_width, height: original_height },
521        )
522    })
523}
524
525#[cfg(feature = "software-renderer")]
526fn load_image(
527    file: crate::fileaccess::VirtualFile,
528    scale_factor: f32,
529    font_collection: Option<&SharedFontCollection>,
530) -> image::ImageResult<(image::RgbaImage, SourceFormat, Size)> {
531    use std::ffi::OsStr;
532
533    let extension = file.canon_path.extension().and_then(OsStr::to_str);
534
535    let data = if let Some(buffer) = file.builtin_contents {
536        buffer.to_vec()
537    } else {
538        std::fs::read(&file.canon_path)?
539    };
540
541    load_image_from_bytes(&data, extension, scale_factor, font_collection)
542}
543
544fn embed_data_uri(
545    global_embedded_resources: &RefCell<TiVec<EmbeddedResourcesIdx, EmbeddedResources>>,
546    path_to_id: &mut HashMap<SmolStr, EmbeddedResourcesIdx>,
547    data_uri: &str,
548    _embed_files: EmbedResourcesKind,
549    _scale_factor: f32,
550    diag: &mut BuildDiagnostics,
551    source_location: &Option<crate::diagnostics::SourceLocation>,
552    _font_collection: Option<&SharedFontCollection>,
553) -> ImageReference {
554    if let Some(&resource_id) = path_to_id.get(data_uri) {
555        let resources = global_embedded_resources.borrow();
556        return match &resources[resource_id].kind {
557            #[cfg(feature = "software-renderer")]
558            EmbeddedResourcesKind::TextureData { .. } => {
559                ImageReference::EmbeddedTexture { resource_id }
560            }
561            EmbeddedResourcesKind::DataUriPayload(_, ext) => {
562                ImageReference::EmbeddedData { resource_id, extension: ext.clone() }
563            }
564            _ => ImageReference::None,
565        };
566    }
567
568    let (decoded_data, extension) = match crate::data_uri::decode_data_uri(data_uri) {
569        Ok(result) => result,
570        Err(e) => {
571            diag.push_error(e, source_location);
572            return ImageReference::None;
573        }
574    };
575
576    let mut resources = global_embedded_resources.borrow_mut();
577    let mut push = |kind| {
578        let id = resources.push_and_get_key(EmbeddedResources { path: None, kind });
579        path_to_id.insert(data_uri.into(), id);
580        id
581    };
582
583    #[cfg(feature = "software-renderer")]
584    if _embed_files == EmbedResourcesKind::EmbedTextures {
585        match load_image_from_bytes(
586            &decoded_data,
587            Some(&extension),
588            _scale_factor,
589            _font_collection,
590        )
591        .map_err(|e| e.to_string())
592        {
593            Ok((img, source_format, original_size)) => {
594                let resource_id = push(EmbeddedResourcesKind::TextureData(generate_texture(
595                    img,
596                    source_format,
597                    original_size,
598                )));
599                return ImageReference::EmbeddedTexture { resource_id };
600            }
601            Err(err) => {
602                diag.push_error(format!("Cannot load data URI image: {err}"), source_location);
603                return ImageReference::None;
604            }
605        }
606    }
607
608    let resource_id = push(EmbeddedResourcesKind::DataUriPayload(decoded_data, extension.clone()));
609
610    ImageReference::EmbeddedData { resource_id, extension }
611}