Skip to main content

i_slint_core/graphics/
image.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
4// cSpell: ignore EPOC htmlimage
5/*!
6This module contains image decoding and caching related types for the run-time library.
7*/
8
9use crate::lengths::{PhysicalPx, ScaleFactor};
10use crate::slice::Slice;
11#[allow(unused)]
12use crate::{SharedString, SharedVector};
13
14use super::{IntRect, IntSize};
15use crate::items::{ImageFit, ImageHorizontalAlignment, ImageTiling, ImageVerticalAlignment};
16
17#[cfg(feature = "image-decoders")]
18pub mod cache;
19#[cfg(target_arch = "wasm32")]
20mod htmlimage;
21#[cfg(feature = "svg")]
22mod svg;
23
24#[allow(missing_docs)]
25#[cfg_attr(not(feature = "ffi"), i_slint_core_macros::remove_extern)]
26#[vtable::vtable]
27#[repr(C)]
28pub struct OpaqueImageVTable {
29    drop_in_place: extern "C" fn(VRefMut<OpaqueImageVTable>) -> Layout,
30    dealloc: extern "C" fn(&OpaqueImageVTable, ptr: *mut u8, layout: Layout),
31    /// Returns the image size
32    size: extern "C" fn(VRef<OpaqueImageVTable>) -> IntSize,
33    /// Returns a cache key
34    cache_key: extern "C" fn(VRef<OpaqueImageVTable>) -> ImageCacheKey,
35}
36
37#[cfg(feature = "svg")]
38OpaqueImageVTable_static! {
39    /// VTable for RC wrapped SVG helper struct.
40    pub static PARSED_SVG_VT for svg::ParsedSVG
41}
42
43#[cfg(target_arch = "wasm32")]
44OpaqueImageVTable_static! {
45    /// VTable for RC wrapped HtmlImage helper struct.
46    pub static HTML_IMAGE_VT for htmlimage::HTMLImage
47}
48
49OpaqueImageVTable_static! {
50    /// VTable for RC wrapped SVG helper struct.
51    pub static NINE_SLICE_VT for NineSliceImage
52}
53
54/// SharedPixelBuffer is a container for storing image data as pixels. It is
55/// internally reference counted and cheap to clone.
56///
57/// You can construct a new empty shared pixel buffer with [`SharedPixelBuffer::new`],
58/// or you can clone it from an existing contiguous buffer that you might already have, using
59/// [`SharedPixelBuffer::clone_from_slice`].
60///
61/// See the documentation for [`Image`] for examples how to use this type to integrate
62/// Slint with external rendering functions.
63#[derive(Debug, Clone)]
64#[repr(C)]
65pub struct SharedPixelBuffer<Pixel> {
66    width: u32,
67    height: u32,
68    pub(crate) data: SharedVector<Pixel>,
69}
70
71impl<Pixel> SharedPixelBuffer<Pixel> {
72    /// Returns the width of the image in pixels.
73    pub fn width(&self) -> u32 {
74        self.width
75    }
76
77    /// Returns the height of the image in pixels.
78    pub fn height(&self) -> u32 {
79        self.height
80    }
81
82    /// Returns the size of the image in pixels.
83    pub fn size(&self) -> IntSize {
84        [self.width, self.height].into()
85    }
86}
87
88impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
89    /// Return a mutable slice to the pixel data. If the SharedPixelBuffer was shared, this will make a copy of the buffer.
90    pub fn make_mut_slice(&mut self) -> &mut [Pixel] {
91        self.data.make_mut_slice()
92    }
93}
94
95impl<Pixel: Clone + rgb::Pod> SharedPixelBuffer<Pixel>
96where
97    [Pixel]: rgb::ComponentBytes<u8>,
98{
99    /// Returns the pixels interpreted as raw bytes.
100    pub fn as_bytes(&self) -> &[u8] {
101        use rgb::ComponentBytes;
102        self.data.as_slice().as_bytes()
103    }
104
105    /// Returns the pixels interpreted as raw bytes.
106    pub fn make_mut_bytes(&mut self) -> &mut [u8] {
107        use rgb::ComponentBytes;
108        self.data.make_mut_slice().as_bytes_mut()
109    }
110}
111
112impl<Pixel> SharedPixelBuffer<Pixel> {
113    /// Return a slice to the pixel data.
114    pub fn as_slice(&self) -> &[Pixel] {
115        self.data.as_slice()
116    }
117}
118
119impl<Pixel: Clone + Default> SharedPixelBuffer<Pixel> {
120    /// Creates a new SharedPixelBuffer with the given width and height. Each pixel will be initialized with the value
121    /// that [`Default::default()`] returns for the Pixel type.
122    pub fn new(width: u32, height: u32) -> Self {
123        Self {
124            width,
125            height,
126            data: core::iter::repeat_n(Pixel::default(), width as usize * height as usize)
127                .collect(),
128        }
129    }
130}
131
132impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
133    /// Creates a new SharedPixelBuffer by cloning and converting pixels from an existing
134    /// slice. This function is useful when another crate was used to allocate an image
135    /// and you would like to convert it for use in Slint.
136    pub fn clone_from_slice<SourcePixelType>(
137        pixel_slice: &[SourcePixelType],
138        width: u32,
139        height: u32,
140    ) -> Self
141    where
142        [SourcePixelType]: rgb::AsPixels<Pixel>,
143    {
144        use rgb::AsPixels;
145        Self { width, height, data: pixel_slice.as_pixels().into() }
146    }
147}
148
149/// Convenience alias for a pixel with three color channels (red, green and blue), each
150/// encoded as u8.
151pub type Rgb8Pixel = rgb::RGB8;
152/// Convenience alias for a pixel with four color channels (red, green, blue and alpha), each
153/// encoded as u8.
154pub type Rgba8Pixel = rgb::RGBA8;
155
156/// SharedImageBuffer is a container for images that are stored in CPU accessible memory.
157///
158/// The SharedImageBuffer's variants represent the different common formats for encoding
159/// images in pixels.
160#[derive(Clone, Debug)]
161#[repr(C)]
162/// TODO: Make this non_exhaustive before making the type public!
163pub enum SharedImageBuffer {
164    /// This variant holds the data for an image where each pixel has three color channels (red, green,
165    /// and blue) and each channel is encoded as unsigned byte.
166    RGB8(SharedPixelBuffer<Rgb8Pixel>),
167    /// This variant holds the data for an image where each pixel has four color channels (red, green,
168    /// blue and alpha) and each channel is encoded as unsigned byte.
169    RGBA8(SharedPixelBuffer<Rgba8Pixel>),
170    /// This variant holds the data for an image where each pixel has four color channels (red, green,
171    /// blue and alpha) and each channel is encoded as unsigned byte. In contrast to [`Self::RGBA8`],
172    /// this variant assumes that the alpha channel is also already multiplied to each red, green and blue
173    /// component of each pixel.
174    /// Only construct this format if you know that your pixels are encoded this way. It is more efficient
175    /// for rendering.
176    RGBA8Premultiplied(SharedPixelBuffer<Rgba8Pixel>),
177}
178
179impl SharedImageBuffer {
180    /// Returns the width of the image in pixels.
181    #[inline]
182    pub fn width(&self) -> u32 {
183        match self {
184            Self::RGB8(buffer) => buffer.width(),
185            Self::RGBA8(buffer) => buffer.width(),
186            Self::RGBA8Premultiplied(buffer) => buffer.width(),
187        }
188    }
189
190    /// Returns the height of the image in pixels.
191    #[inline]
192    pub fn height(&self) -> u32 {
193        match self {
194            Self::RGB8(buffer) => buffer.height(),
195            Self::RGBA8(buffer) => buffer.height(),
196            Self::RGBA8Premultiplied(buffer) => buffer.height(),
197        }
198    }
199
200    /// Returns the size of the image in pixels.
201    #[inline]
202    pub fn size(&self) -> IntSize {
203        match self {
204            Self::RGB8(buffer) => buffer.size(),
205            Self::RGBA8(buffer) => buffer.size(),
206            Self::RGBA8Premultiplied(buffer) => buffer.size(),
207        }
208    }
209}
210
211impl PartialEq for SharedImageBuffer {
212    fn eq(&self, other: &Self) -> bool {
213        match self {
214            Self::RGB8(lhs_buffer) => {
215                matches!(other, Self::RGB8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
216            }
217            Self::RGBA8(lhs_buffer) => {
218                matches!(other, Self::RGBA8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
219            }
220            Self::RGBA8Premultiplied(lhs_buffer) => {
221                matches!(other, Self::RGBA8Premultiplied(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
222            }
223        }
224    }
225}
226
227#[repr(u8)]
228#[derive(Clone, PartialEq, Debug, Copy)]
229/// The pixel format used for textures.
230pub enum TexturePixelFormat {
231    /// red, green, blue. 24bits.
232    Rgb,
233    /// Red, green, blue, alpha. 32bits.
234    Rgba,
235    /// Red, green, blue, alpha. 32bits. The color are premultiplied by alpha
236    RgbaPremultiplied,
237    /// Alpha map. 8bits. Each pixel is an alpha value. The color is specified separately.
238    AlphaMap,
239    /// Distance field. 8bit interpreted as i8.
240    /// The range is such that i8::MIN corresponds to 3 pixels outside of the shape,
241    /// and i8::MAX corresponds to 3 pixels inside the shape.
242    /// The array must be width * height +1 bytes long. (the extra bit is read but never used)
243    SignedDistanceField,
244}
245
246impl TexturePixelFormat {
247    /// The number of bytes in a pixel
248    pub fn bpp(self) -> usize {
249        match self {
250            TexturePixelFormat::Rgb => 3,
251            TexturePixelFormat::Rgba => 4,
252            TexturePixelFormat::RgbaPremultiplied => 4,
253            TexturePixelFormat::AlphaMap => 1,
254            TexturePixelFormat::SignedDistanceField => 1,
255        }
256    }
257}
258
259#[repr(C)]
260#[derive(Clone, PartialEq, Debug)]
261/// Some raw pixel data which is typically stored in the binary
262pub struct StaticTexture {
263    /// The position and size of the texture within the image
264    pub rect: IntRect,
265    /// The pixel format of this texture
266    pub format: TexturePixelFormat,
267    /// The color, for the alpha map ones
268    pub color: crate::Color,
269    /// index in the data array
270    pub index: usize,
271}
272
273/// A texture is stored in read-only memory and may be composed of sub-textures.
274#[repr(C)]
275#[derive(Clone, PartialEq, Debug)]
276pub struct StaticTextures {
277    /// The total size of the image (this might not be the size of the full image
278    /// as some transparent part are not part of any texture)
279    pub size: IntSize,
280    /// The size of the image before the compiler applied any scaling
281    pub original_size: IntSize,
282    /// The pixel data referenced by the textures
283    pub data: Slice<'static, u8>,
284    /// The list of textures
285    pub textures: Slice<'static, StaticTexture>,
286}
287
288/// A struct that provides a path as a string as well as the last modification
289/// time of the file it points to.
290#[derive(PartialEq, Eq, Debug, Hash, Clone)]
291#[repr(C)]
292#[cfg(any(feature = "std", feature = "ffi"))]
293pub struct CachedPath {
294    path: SharedString,
295    /// SystemTime since UNIX_EPOC as secs
296    last_modified: u32,
297}
298
299#[cfg(feature = "std")]
300impl CachedPath {
301    fn new<P: AsRef<std::path::Path>>(path: P) -> Self {
302        let path_str = path.as_ref().to_string_lossy().as_ref().into();
303        let timestamp = std::fs::metadata(path)
304            .and_then(|md| md.modified())
305            .unwrap_or(std::time::UNIX_EPOCH)
306            .duration_since(std::time::UNIX_EPOCH)
307            .map(|t| t.as_secs() as u32)
308            .unwrap_or_default();
309        Self { path: path_str, last_modified: timestamp }
310    }
311}
312
313/// ImageCacheKey encapsulates the different ways of indexing images in the
314/// cache of decoded images.
315#[derive(PartialEq, Eq, Debug, Hash, Clone)]
316#[repr(u8)]
317pub enum ImageCacheKey {
318    /// This variant indicates that no image cache key can be created for the image.
319    /// For example this is the case for programmatically created images.
320    Invalid = 0,
321    #[cfg(any(feature = "std", feature = "ffi"))]
322    /// The image is identified by its path on the file system and the last modification time stamp.
323    Path(CachedPath) = 1,
324    /// The image is identified by a URL.
325    #[cfg(target_arch = "wasm32")]
326    URL(SharedString) = 2,
327    /// The image is identified by the static address of its encoded data.
328    EmbeddedData(usize) = 3,
329}
330
331impl ImageCacheKey {
332    /// Returns a new cache key if decoded image data can be stored in image cache for
333    /// the given ImageInner.
334    pub fn new(resource: &ImageInner) -> Option<Self> {
335        let key = match resource {
336            ImageInner::None => return None,
337            ImageInner::EmbeddedImage { cache_key, .. } => cache_key.clone(),
338            ImageInner::StaticTextures(textures) => {
339                Self::from_embedded_image_data(textures.data.as_slice())
340            }
341            #[cfg(feature = "svg")]
342            ImageInner::Svg(parsed_svg) => parsed_svg.cache_key(),
343            #[cfg(target_arch = "wasm32")]
344            ImageInner::HTMLImage(htmlimage) => Self::URL(htmlimage.source().into()),
345            ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).cache_key(),
346            #[cfg(not(target_arch = "wasm32"))]
347            ImageInner::BorrowedOpenGLTexture(..) => return None,
348            ImageInner::NineSlice(nine) => vtable::VRc::borrow(nine).cache_key(),
349            #[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))]
350            ImageInner::WGPUTexture(..) => return None,
351        };
352        if matches!(key, ImageCacheKey::Invalid) { None } else { Some(key) }
353    }
354
355    /// Returns a cache key for static embedded image data.
356    pub fn from_embedded_image_data(data: &'static [u8]) -> Self {
357        Self::EmbeddedData(data.as_ptr() as usize)
358    }
359}
360
361/// Represent a nine-slice image with the base image and the 4 borders
362pub struct NineSliceImage(pub ImageInner, pub [u16; 4]);
363
364impl NineSliceImage {
365    /// return the backing Image
366    pub fn image(&self) -> Image {
367        Image(self.0.clone())
368    }
369}
370
371impl OpaqueImage for NineSliceImage {
372    fn size(&self) -> IntSize {
373        self.0.size()
374    }
375    fn cache_key(&self) -> ImageCacheKey {
376        ImageCacheKey::new(&self.0).unwrap_or(ImageCacheKey::Invalid)
377    }
378}
379
380/// Represents a `wgpu::Texture` for each version of WGPU we support.
381/// Represents a `wgpu::Texture` for each version of WGPU we support.
382#[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))]
383#[derive(Clone, Debug)]
384pub enum WGPUTexture {
385    /// A texture for WGPU version 28.
386    #[cfg(feature = "unstable-wgpu-28")]
387    WGPU28Texture(wgpu_28::Texture),
388    /// A texture for WGPU version 29.
389    #[cfg(feature = "unstable-wgpu-29")]
390    WGPU29Texture(wgpu_29::Texture),
391}
392
393#[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))]
394impl OpaqueImage for WGPUTexture {
395    fn size(&self) -> IntSize {
396        match self {
397            #[cfg(feature = "unstable-wgpu-28")]
398            Self::WGPU28Texture(texture) => {
399                let size = texture.size();
400                (size.width, size.height).into()
401            }
402            #[cfg(feature = "unstable-wgpu-29")]
403            Self::WGPU29Texture(texture) => {
404                let size = texture.size();
405                (size.width, size.height).into()
406            }
407        }
408    }
409    fn cache_key(&self) -> ImageCacheKey {
410        ImageCacheKey::Invalid
411    }
412}
413
414/// A resource is a reference to binary data, for example images. They can be accessible on the file
415/// system or embedded in the resulting binary. Or they might be URLs to a web server and a downloaded
416/// is necessary before they can be used.
417/// cbindgen:prefix-with-name
418#[derive(Clone, Debug, Default)]
419#[repr(u8)]
420#[allow(missing_docs)]
421pub enum ImageInner {
422    /// A resource that does not represent any data.
423    #[default]
424    None = 0,
425    EmbeddedImage {
426        cache_key: ImageCacheKey,
427        buffer: SharedImageBuffer,
428    } = 1,
429    #[cfg(feature = "svg")]
430    Svg(vtable::VRc<OpaqueImageVTable, svg::ParsedSVG>) = 2,
431    StaticTextures(&'static StaticTextures) = 3,
432    #[cfg(target_arch = "wasm32")]
433    HTMLImage(vtable::VRc<OpaqueImageVTable, htmlimage::HTMLImage>) = 4,
434    BackendStorage(vtable::VRc<OpaqueImageVTable>) = 5,
435    #[cfg(not(target_arch = "wasm32"))]
436    BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6,
437    NineSlice(vtable::VRc<OpaqueImageVTable, NineSliceImage>) = 7,
438    #[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))]
439    WGPUTexture(WGPUTexture) = 8,
440}
441
442impl ImageInner {
443    /// Return or render the image into a buffer
444    ///
445    /// `target_size_for_scalable_source` is the size to use if the image is scalable.
446    /// (when unspecified, will default to the intrinsic size of the image)
447    ///
448    /// Returns None if the image can't be rendered in a buffer or if the image is empty
449    pub fn render_to_buffer(
450        &self,
451        _target_size_for_scalable_source: Option<euclid::Size2D<u32, PhysicalPx>>,
452    ) -> Option<SharedImageBuffer> {
453        match self {
454            ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()),
455            #[cfg(feature = "svg")]
456            ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) {
457                Ok(b) => Some(b),
458                // Ignore error when rendering a 0x0 image, that's just an empty image
459                Err(resvg::usvg::Error::InvalidSize) => None,
460                Err(err) => {
461                    std::eprintln!("Error rendering SVG: {err}");
462                    None
463                }
464            },
465            ImageInner::StaticTextures(ts) => {
466                let mut buffer =
467                    SharedPixelBuffer::<Rgba8Pixel>::new(ts.size.width, ts.size.height);
468                let stride = buffer.width() as usize;
469                let slice = buffer.make_mut_slice();
470                for t in ts.textures.iter() {
471                    let rect = t.rect.to_usize();
472                    for y in 0..rect.height() {
473                        let slice = &mut slice[(rect.min_y() + y) * stride..][rect.x_range()];
474                        let source = &ts.data[t.index + y * rect.width() * t.format.bpp()..];
475                        match t.format {
476                            TexturePixelFormat::Rgb => {
477                                let mut iter = source.chunks_exact(3).map(|p| Rgba8Pixel {
478                                    r: p[0],
479                                    g: p[1],
480                                    b: p[2],
481                                    a: 255,
482                                });
483                                slice.fill_with(|| iter.next().unwrap());
484                            }
485                            TexturePixelFormat::RgbaPremultiplied => {
486                                let mut iter = source.chunks_exact(4).map(|p| Rgba8Pixel {
487                                    r: p[0],
488                                    g: p[1],
489                                    b: p[2],
490                                    a: p[3],
491                                });
492                                slice.fill_with(|| iter.next().unwrap());
493                            }
494                            TexturePixelFormat::Rgba => {
495                                let mut iter = source.chunks_exact(4).map(|p| {
496                                    let a = p[3];
497                                    Rgba8Pixel {
498                                        r: (p[0] as u16 * a as u16 / 255) as u8,
499                                        g: (p[1] as u16 * a as u16 / 255) as u8,
500                                        b: (p[2] as u16 * a as u16 / 255) as u8,
501                                        a,
502                                    }
503                                });
504                                slice.fill_with(|| iter.next().unwrap());
505                            }
506                            TexturePixelFormat::AlphaMap => {
507                                let col = t.color.to_argb_u8();
508                                let mut iter = source.iter().map(|p| {
509                                    let a = *p as u32 * col.alpha as u32;
510                                    Rgba8Pixel {
511                                        r: (col.red as u32 * a / (255 * 255)) as u8,
512                                        g: (col.green as u32 * a / (255 * 255)) as u8,
513                                        b: (col.blue as u32 * a / (255 * 255)) as u8,
514                                        a: (a / 255) as u8,
515                                    }
516                                });
517                                slice.fill_with(|| iter.next().unwrap());
518                            }
519                            TexturePixelFormat::SignedDistanceField => {
520                                todo!("converting from a signed distance field to an image")
521                            }
522                        };
523                    }
524                }
525                Some(SharedImageBuffer::RGBA8Premultiplied(buffer))
526            }
527            ImageInner::NineSlice(nine) => nine.0.render_to_buffer(None),
528            _ => None,
529        }
530    }
531
532    /// Returns true if the image is an SVG (either backed by resvg or HTML image wrapper).
533    pub fn is_svg(&self) -> bool {
534        match self {
535            #[cfg(feature = "svg")]
536            Self::Svg(_) => true,
537            #[cfg(target_arch = "wasm32")]
538            Self::HTMLImage(html_image) => html_image.is_svg(),
539            _ => false,
540        }
541    }
542
543    /// Return the image size
544    pub fn size(&self) -> IntSize {
545        match self {
546            ImageInner::None => Default::default(),
547            ImageInner::EmbeddedImage { buffer, .. } => buffer.size(),
548            ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
549            #[cfg(feature = "svg")]
550            ImageInner::Svg(svg) => svg.size(),
551            #[cfg(target_arch = "wasm32")]
552            ImageInner::HTMLImage(htmlimage) => htmlimage.size().unwrap_or_default(),
553            ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size(),
554            #[cfg(not(target_arch = "wasm32"))]
555            ImageInner::BorrowedOpenGLTexture(BorrowedOpenGLTexture { size, .. }) => *size,
556            ImageInner::NineSlice(nine) => nine.0.size(),
557            #[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))]
558            ImageInner::WGPUTexture(texture) => texture.size(),
559        }
560    }
561
562    /// Internal helper to abstract over either loading from a file or parsing internal data.
563    ///
564    /// This can create an `ImageInner` with a dangling cache key reference if used incorrectly,
565    /// which could lead to bad behavior. This constructor should be called from within
566    /// `ImageCache::lookup_image_in_cache_or_create`, or `ImageCacheKey::Invalid` should be
567    /// supplied.
568    #[cfg(feature = "image-decoders")]
569    pub(crate) fn load_from_data_with_cache_key(
570        cache_key: ImageCacheKey,
571        data: Slice<'_, u8>,
572        format: Slice<'_, u8>,
573    ) -> Option<Self> {
574        #[cfg(feature = "svg")]
575        if format.as_slice() == b"svg" || format.as_slice() == b"svgz" {
576            return Some(ImageInner::Svg(vtable::VRc::new(
577                svg::load_from_data(data.as_slice(), cache_key).map_or_else(
578                    |svg_err| {
579                        crate::debug_log!("Error loading SVG: {}", svg_err);
580                        None
581                    },
582                    Some,
583                )?,
584            )));
585        }
586
587        let format = std::str::from_utf8(format.as_slice())
588            .ok()
589            .and_then(image::ImageFormat::from_extension);
590        let maybe_image = if let Some(format) = format {
591            image::load_from_memory_with_format(data.as_slice(), format)
592        } else {
593            image::load_from_memory(data.as_slice())
594        };
595
596        match maybe_image {
597            Ok(image) => Some(ImageInner::EmbeddedImage {
598                cache_key,
599                buffer: dynamic_image_to_shared_image_buffer(image),
600            }),
601            Err(decode_err) => {
602                crate::debug_log!("Error decoding embedded image: {}", decode_err);
603                None
604            }
605        }
606    }
607}
608
609/// Convert `image::DynamicImage` to `SharedImageBuffer`
610#[cfg(feature = "image-decoders")]
611fn dynamic_image_to_shared_image_buffer(dynamic_image: image::DynamicImage) -> SharedImageBuffer {
612    use rgb::AsPixels;
613
614    if dynamic_image.color().has_alpha() {
615        let rgba8image = dynamic_image.to_rgba8();
616        // Prefer pre-multiplied alpha so that smooth-scaling won't bleed the alpha when blending
617        // in the renderers.
618        SharedImageBuffer::RGBA8Premultiplied(SharedPixelBuffer {
619            width: rgba8image.width(),
620            height: rgba8image.height(),
621            data: rgba8image
622                .as_pixels()
623                .iter()
624                .map(|pixel| Image::rgba_to_premultiplied_rgba(*pixel))
625                .collect(),
626        })
627    } else {
628        let rgb8image = dynamic_image.to_rgb8();
629        SharedImageBuffer::RGB8(SharedPixelBuffer::clone_from_slice(
630            rgb8image.as_raw(),
631            rgb8image.width(),
632            rgb8image.height(),
633        ))
634    }
635}
636
637impl PartialEq for ImageInner {
638    fn eq(&self, other: &Self) -> bool {
639        match (self, other) {
640            (
641                Self::EmbeddedImage { cache_key: l_cache_key, buffer: l_buffer },
642                Self::EmbeddedImage { cache_key: r_cache_key, buffer: r_buffer },
643            ) => l_cache_key == r_cache_key && l_buffer == r_buffer,
644            #[cfg(feature = "svg")]
645            (Self::Svg(l0), Self::Svg(r0)) => vtable::VRc::ptr_eq(l0, r0),
646            (Self::StaticTextures(l0), Self::StaticTextures(r0)) => l0 == r0,
647            #[cfg(target_arch = "wasm32")]
648            (Self::HTMLImage(l0), Self::HTMLImage(r0)) => vtable::VRc::ptr_eq(l0, r0),
649            (Self::BackendStorage(l0), Self::BackendStorage(r0)) => vtable::VRc::ptr_eq(l0, r0),
650            #[cfg(not(target_arch = "wasm32"))]
651            (Self::BorrowedOpenGLTexture(l0), Self::BorrowedOpenGLTexture(r0)) => l0 == r0,
652            (Self::NineSlice(l), Self::NineSlice(r)) => l.0 == r.0 && l.1 == r.1,
653            _ => false,
654        }
655    }
656}
657
658impl<'a> From<&'a Image> for &'a ImageInner {
659    fn from(other: &'a Image) -> Self {
660        &other.0
661    }
662}
663
664/// Error generated if an image cannot be loaded for any reasons.
665#[derive(Default, Debug, PartialEq)]
666pub struct LoadImageError(());
667
668impl core::fmt::Display for LoadImageError {
669    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
670        f.write_str("The image cannot be loaded")
671    }
672}
673
674#[cfg(feature = "std")]
675impl std::error::Error for LoadImageError {}
676
677/// An image type that can be displayed by the Image element. You can construct
678/// Image objects from a path to an image file on disk, using [`Self::load_from_path`].
679///
680/// Another typical use-case is to render the image content with Rust code.
681/// For this it's most efficient to create a new SharedPixelBuffer with the known dimensions
682/// and pass the mutable slice to your rendering function. Afterwards you can create an
683/// Image.
684///
685/// The following example creates a 320x200 RGB pixel buffer and calls an external
686/// low_level_render() function to draw a shape into it. Finally the result is
687/// stored in an Image with [`Self::from_rgb8()`]:
688/// ```
689/// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgb8Pixel};
690///
691/// fn low_level_render(width: u32, height: u32, buffer: &mut [u8]) {
692///     // render beautiful circle or other shapes here
693/// }
694///
695/// let mut pixel_buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
696///
697/// low_level_render(pixel_buffer.width(), pixel_buffer.height(),
698///                  pixel_buffer.make_mut_bytes());
699///
700/// let image = Image::from_rgb8(pixel_buffer);
701/// ```
702///
703/// Another use-case is to import existing image data into Slint, by
704/// creating a new Image through cloning of another image type.
705///
706/// The following example uses the popular [image crate](https://docs.rs/image/) to
707/// load a `.png` file from disk, apply brightening filter on it and then import
708/// it into an [`Image`]:
709/// ```no_run
710/// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgba8Pixel};
711/// let mut cat_image = image::open("cat.png").expect("Error loading cat image").into_rgba8();
712///
713/// image::imageops::colorops::brighten_in_place(&mut cat_image, 20);
714///
715/// let buffer = SharedPixelBuffer::<Rgba8Pixel>::clone_from_slice(
716///     cat_image.as_raw(),
717///     cat_image.width(),
718///     cat_image.height(),
719/// );
720/// let image = Image::from_rgba8(buffer);
721/// ```
722///
723/// A popular software (CPU) rendering library in Rust is tiny-skia. The following example shows
724/// how to use tiny-skia to render into a [`SharedPixelBuffer`]:
725/// ```
726/// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgba8Pixel};
727/// let mut pixel_buffer = SharedPixelBuffer::<Rgba8Pixel>::new(640, 480);
728/// let width = pixel_buffer.width();
729/// let height = pixel_buffer.height();
730/// let mut pixmap = tiny_skia::PixmapMut::from_bytes(
731///     pixel_buffer.make_mut_bytes(), width, height
732/// ).unwrap();
733/// pixmap.fill(tiny_skia::Color::TRANSPARENT);
734///
735/// let circle = tiny_skia::PathBuilder::from_circle(320., 240., 150.).unwrap();
736///
737/// let mut paint = tiny_skia::Paint::default();
738/// paint.shader = tiny_skia::LinearGradient::new(
739///     tiny_skia::Point::from_xy(100.0, 100.0),
740///     tiny_skia::Point::from_xy(400.0, 400.0),
741///     vec![
742///         tiny_skia::GradientStop::new(0.0, tiny_skia::Color::from_rgba8(50, 127, 150, 200)),
743///         tiny_skia::GradientStop::new(1.0, tiny_skia::Color::from_rgba8(220, 140, 75, 180)),
744///     ],
745///     tiny_skia::SpreadMode::Pad,
746///     tiny_skia::Transform::identity(),
747/// ).unwrap();
748///
749/// pixmap.fill_path(&circle, &paint, tiny_skia::FillRule::Winding, Default::default(), None);
750///
751/// let image = Image::from_rgba8_premultiplied(pixel_buffer);
752/// ```
753///
754/// ### Sending Image to a thread
755///
756/// `Image` is not [`Send`], because it uses internal cache that are local to the Slint thread.
757/// If you want to create image data in a thread and send that to slint, construct the
758/// [`SharedPixelBuffer`] in a thread, and send that to Slint's UI thread.
759///
760/// ```rust,no_run
761/// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgba8Pixel};
762/// std::thread::spawn(move || {
763///     let mut pixel_buffer = SharedPixelBuffer::<Rgba8Pixel>::new(640, 480);
764///     // ... fill the pixel_buffer with data as shown in the previous example ...
765///     slint::invoke_from_event_loop(move || {
766///         // this will run in the Slint's UI thread
767///         let image = Image::from_rgba8_premultiplied(pixel_buffer);
768///         // ... use the image, eg:
769///         // my_ui_handle.upgrade().unwrap().set_image(image);
770///     });
771/// });
772/// ```
773#[repr(transparent)]
774#[derive(Default, Clone, Debug, PartialEq, derive_more::From)]
775pub struct Image(pub(crate) ImageInner);
776
777impl Image {
778    #[cfg(feature = "image-decoders")]
779    /// Load an Image from a path to a file containing an image.
780    ///
781    /// Supported formats are SVG, PNG and JPEG.
782    /// Enable support for additional formats supported by the [`image` crate](https://crates.io/crates/image) (
783    /// AVIF, BMP, DDS, Farbfeld, GIF, HDR, ICO, JPEG, EXR, PNG, PNM, QOI, TGA, TIFF, WebP)
784    /// by enabling the `image-default-formats` cargo feature.
785    pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> {
786        self::cache::IMAGE_CACHE.with(|global_cache| {
787            let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into();
788            global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(()))
789        })
790    }
791
792    /// Creates a new Image from the specified shared pixel buffer, where each pixel has three color
793    /// channels (red, green and blue) encoded as u8.
794    pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
795        Image(ImageInner::EmbeddedImage {
796            cache_key: ImageCacheKey::Invalid,
797            buffer: SharedImageBuffer::RGB8(buffer),
798        })
799    }
800
801    /// Creates a new Image from the specified shared pixel buffer, where each pixel has four color
802    /// channels (red, green, blue and alpha) encoded as u8.
803    pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
804        Image(ImageInner::EmbeddedImage {
805            cache_key: ImageCacheKey::Invalid,
806            buffer: SharedImageBuffer::RGBA8(buffer),
807        })
808    }
809
810    /// Creates a new Image from the specified shared pixel buffer, where each pixel has four color
811    /// channels (red, green, blue and alpha) encoded as u8 and, in contrast to [`Self::from_rgba8`],
812    /// the alpha channel is also assumed to be multiplied to the red, green and blue channels.
813    ///
814    /// Only construct an Image with this function if you know that your pixels are encoded this way.
815    pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
816        Image(ImageInner::EmbeddedImage {
817            cache_key: ImageCacheKey::Invalid,
818            buffer: SharedImageBuffer::RGBA8Premultiplied(buffer),
819        })
820    }
821
822    /// Returns the pixel buffer for the Image if available in RGB format without alpha.
823    /// Returns None if the pixels cannot be obtained, for example when the image was created from borrowed OpenGL textures.
824    pub fn to_rgb8(&self) -> Option<SharedPixelBuffer<Rgb8Pixel>> {
825        self.0.render_to_buffer(None).and_then(|image| match image {
826            SharedImageBuffer::RGB8(buffer) => Some(buffer),
827            _ => None,
828        })
829    }
830
831    /// Returns the pixel buffer for the Image if available in RGBA format.
832    /// Returns None if the pixels cannot be obtained, for example when the image was created from borrowed OpenGL textures.
833    pub fn to_rgba8(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
834        self.0.render_to_buffer(None).map(|image| match image {
835            SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
836                width: buffer.width,
837                height: buffer.height,
838                data: buffer.data.into_iter().map(Into::into).collect(),
839            },
840            SharedImageBuffer::RGBA8(buffer) => buffer,
841            SharedImageBuffer::RGBA8Premultiplied(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
842                width: buffer.width,
843                height: buffer.height,
844                data: buffer.data.into_iter().map(Image::premultiplied_rgba_to_rgba).collect(),
845            },
846        })
847    }
848
849    /// Returns the pixel buffer for the Image if available in RGBA format, with the alpha channel pre-multiplied
850    /// to the red, green, and blue channels.
851    /// Returns None if the pixels cannot be obtained, for example when the image was created from borrowed OpenGL textures.
852    pub fn to_rgba8_premultiplied(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
853        self.0.render_to_buffer(None).map(|image| match image {
854            SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
855                width: buffer.width,
856                height: buffer.height,
857                data: buffer.data.into_iter().map(Into::into).collect(),
858            },
859            SharedImageBuffer::RGBA8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
860                width: buffer.width,
861                height: buffer.height,
862                data: buffer.data.into_iter().map(Image::rgba_to_premultiplied_rgba).collect(),
863            },
864            SharedImageBuffer::RGBA8Premultiplied(buffer) => buffer,
865        })
866    }
867
868    /// Returns the pixel converted from premultiplied RGBA to RGBA.
869    fn premultiplied_rgba_to_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
870        if pixel.a == 0 {
871            Rgba8Pixel::new(0, 0, 0, 0)
872        } else {
873            let af = pixel.a as u32;
874            let round = (af / 2) as u32;
875            Rgba8Pixel {
876                r: ((pixel.r as u32 * 255 + round) / af).min(255) as u8,
877                g: ((pixel.g as u32 * 255 + round) / af).min(255) as u8,
878                b: ((pixel.b as u32 * 255 + round) / af).min(255) as u8,
879                a: pixel.a,
880            }
881        }
882    }
883
884    /// Returns the pixel converted from RGBA to premultiplied RGBA.
885    fn rgba_to_premultiplied_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
886        if pixel.a == 255 {
887            pixel
888        } else {
889            let af = pixel.a as u32;
890            Rgba8Pixel {
891                r: (((pixel.r as u32 * af + 128) * 257) >> 16) as u8,
892                g: (((pixel.g as u32 * af + 128) * 257) >> 16) as u8,
893                b: (((pixel.b as u32 * af + 128) * 257) >> 16) as u8,
894                a: pixel.a,
895            }
896        }
897    }
898
899    /// Returns the [WGPU](http://wgpu.rs) 28.x texture that this image wraps; returns None if the image does not
900    /// hold such a previously wrapped texture.
901    ///
902    /// *Note*: This function is behind a feature flag and may be removed or changed in future minor releases,
903    ///         as new major WGPU releases become available.
904    #[cfg(feature = "unstable-wgpu-28")]
905    pub fn to_wgpu_28_texture(&self) -> Option<wgpu_28::Texture> {
906        match &self.0 {
907            ImageInner::WGPUTexture(WGPUTexture::WGPU28Texture(texture)) => Some(texture.clone()),
908            _ => None,
909        }
910    }
911
912    /// Returns the [WGPU](http://wgpu.rs) 29.x texture that this image wraps; returns None if the image does not
913    /// hold such a previously wrapped texture.
914    ///
915    /// *Note*: This function is behind a feature flag and may be removed or changed in future minor releases,
916    ///         as new major WGPU releases become available.
917    #[cfg(feature = "unstable-wgpu-29")]
918    pub fn to_wgpu_29_texture(&self) -> Option<wgpu_29::Texture> {
919        match &self.0 {
920            ImageInner::WGPUTexture(WGPUTexture::WGPU29Texture(texture)) => Some(texture.clone()),
921            _ => None,
922        }
923    }
924
925    /// Creates a new Image from an existing OpenGL texture. The texture remains borrowed by Slint
926    /// for the duration of being used for rendering, such as when assigned as source property to
927    /// an `Image` element. It's the application's responsibility to delete the texture when it is
928    /// not used anymore.
929    ///
930    /// The texture must be bindable against the `GL_TEXTURE_2D` target, have `GL_RGBA` as format
931    /// for the pixel data.
932    ///
933    /// When Slint renders the texture, it assumes that the origin of the texture is at the top-left.
934    /// This is different from the default OpenGL coordinate system.
935    ///
936    /// # Safety
937    ///
938    /// This function is unsafe because invalid texture ids may lead to undefined behavior in OpenGL
939    /// drivers. A valid texture id is one that was created by the same OpenGL context that is
940    /// current during any of the invocations of the callback set on [`Window::set_rendering_notifier()`](crate::api::Window::set_rendering_notifier).
941    /// OpenGL contexts between instances of [`slint::Window`](crate::api::Window) are not sharing resources. Consequently
942    /// [`slint::Image`](Self) objects created from borrowed OpenGL textures cannot be shared between
943    /// different windows.
944    #[allow(unsafe_code)]
945    #[cfg(not(target_arch = "wasm32"))]
946    #[deprecated(since = "1.2.0", note = "Use BorrowedOpenGLTextureBuilder")]
947    pub unsafe fn from_borrowed_gl_2d_rgba_texture(
948        texture_id: core::num::NonZeroU32,
949        size: IntSize,
950    ) -> Self {
951        unsafe { BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size).build() }
952    }
953
954    /// Creates a new Image from the specified buffer, which contains SVG raw data.
955    #[cfg(feature = "svg")]
956    pub fn load_from_svg_data(buffer: &[u8]) -> Result<Self, LoadImageError> {
957        let cache_key = ImageCacheKey::Invalid;
958        Ok(Image(ImageInner::Svg(vtable::VRc::new(
959            svg::load_from_data(buffer, cache_key).map_err(|_| LoadImageError(()))?,
960        ))))
961    }
962
963    /// Sets the nine-slice edges of the image.
964    ///
965    /// [Nine-slice scaling](https://en.wikipedia.org/wiki/9-slice_scaling) is a method for scaling
966    /// images in such a way that the corners are not distorted.
967    /// The arguments define the pixel sizes of the edges that cut the image into 9 slices.
968    pub fn set_nine_slice_edges(&mut self, top: u16, right: u16, bottom: u16, left: u16) {
969        if top == 0 && left == 0 && right == 0 && bottom == 0 {
970            if let ImageInner::NineSlice(n) = &self.0 {
971                self.0 = n.0.clone();
972            }
973        } else {
974            let array = [top, right, bottom, left];
975            let inner = if let ImageInner::NineSlice(n) = &mut self.0 {
976                n.0.clone()
977            } else {
978                self.0.clone()
979            };
980            self.0 = ImageInner::NineSlice(vtable::VRc::new(NineSliceImage(inner, array)));
981        }
982    }
983
984    /// Returns the size of the Image in pixels.
985    pub fn size(&self) -> IntSize {
986        self.0.size()
987    }
988
989    #[cfg(feature = "std")]
990    /// Returns the path of the image on disk, if it was constructed via [`Self::load_from_path`].
991    ///
992    /// For example:
993    /// ```
994    /// # use std::path::Path;
995    /// # use i_slint_core::graphics::*;
996    /// let path_buf = Path::new(env!("CARGO_MANIFEST_DIR"))
997    ///     .join("../../demos/printerdemo/ui/images/cat.jpg");
998    /// let image = Image::load_from_path(&path_buf).unwrap();
999    /// assert_eq!(image.path(), Some(path_buf.as_path()));
1000    /// ```
1001    pub fn path(&self) -> Option<&std::path::Path> {
1002        match &self.0 {
1003            ImageInner::EmbeddedImage {
1004                cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1005                ..
1006            } => Some(std::path::Path::new(path.as_str())),
1007            ImageInner::NineSlice(nine) => match &nine.0 {
1008                ImageInner::EmbeddedImage {
1009                    cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1010                    ..
1011                } => Some(std::path::Path::new(path.as_str())),
1012                _ => None,
1013            },
1014            _ => None,
1015        }
1016    }
1017}
1018
1019#[cfg(feature = "image-decoders")]
1020/// Load an Image from a path to a file containing an image.
1021///
1022/// Supported formats are SVG, PNG and JPEG.
1023/// Enable support for additional formats supported by the [`image` crate](https://crates.io/crates/image) (
1024/// AVIF, BMP, DDS, Farbfeld, GIF, HDR, ICO, JPEG, EXR, PNG, PNM, QOI, TGA, TIFF, WebP)
1025/// by enabling the `image-default-formats` cargo feature.
1026pub fn load_image_from_dynamic_data(bytes: &[u8], format: &str) -> Result<Image, LoadImageError> {
1027    ImageInner::load_from_data_with_cache_key(
1028        ImageCacheKey::Invalid,
1029        bytes.into(),
1030        format.as_bytes().into(),
1031    )
1032    .map(Image)
1033    .ok_or(Default::default())
1034}
1035
1036/// This enum describes the origin to use when rendering a borrowed OpenGL texture.
1037/// Use this with [`BorrowedOpenGLTextureBuilder::origin`].
1038#[derive(Copy, Clone, Debug, PartialEq, Default)]
1039#[repr(u8)]
1040#[non_exhaustive]
1041pub enum BorrowedOpenGLTextureOrigin {
1042    /// The top-left of the texture is the top-left of the texture drawn on the screen.
1043    #[default]
1044    TopLeft,
1045    /// The bottom-left of the texture is the top-left of the texture draw on the screen,
1046    /// flipping it vertically.
1047    BottomLeft,
1048}
1049
1050/// Factory to create [`slint::Image`](crate::graphics::Image) from an existing OpenGL texture.
1051///
1052/// Methods can be chained on it in order to configure it.
1053///
1054///  * `origin`: Change the texture's origin when rendering (default: TopLeft).
1055///
1056/// Complete the builder by calling [`Self::build()`] to create a [`slint::Image`](crate::graphics::Image):
1057///
1058/// ```
1059/// # use i_slint_core::graphics::{BorrowedOpenGLTextureBuilder, Image, IntSize, BorrowedOpenGLTextureOrigin};
1060/// # let texture_id = core::num::NonZeroU32::new(1).unwrap();
1061/// # let size = IntSize::new(100, 100);
1062/// let builder = unsafe { BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size) }
1063///              .origin(BorrowedOpenGLTextureOrigin::TopLeft);
1064///
1065/// let image: slint::Image = builder.build();
1066/// ```
1067#[cfg(not(target_arch = "wasm32"))]
1068pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture);
1069
1070#[cfg(not(target_arch = "wasm32"))]
1071impl BorrowedOpenGLTextureBuilder {
1072    /// Generates the base configuration for a borrowed OpenGL texture.
1073    ///
1074    /// The texture must be bindable against the `GL_TEXTURE_2D` target, have `GL_RGBA` as format
1075    /// for the pixel data.
1076    ///
1077    /// By default, when Slint renders the texture, it assumes that the origin of the texture is at the top-left.
1078    /// This is different from the default OpenGL coordinate system. Use the `mirror_vertically` function
1079    /// to reconfigure this.
1080    ///
1081    /// # Safety
1082    ///
1083    /// This function is unsafe because invalid texture ids may lead to undefined behavior in OpenGL
1084    /// drivers. A valid texture id is one that was created by the same OpenGL context that is
1085    /// current during any of the invocations of the callback set on [`Window::set_rendering_notifier()`](crate::api::Window::set_rendering_notifier).
1086    /// OpenGL contexts between instances of [`slint::Window`](crate::api::Window) are not sharing resources. Consequently
1087    /// [`slint::Image`](Image) objects created from borrowed OpenGL textures cannot be shared between
1088    /// different windows.
1089    #[allow(unsafe_code)]
1090    pub unsafe fn new_gl_2d_rgba_texture(texture_id: core::num::NonZeroU32, size: IntSize) -> Self {
1091        Self(BorrowedOpenGLTexture { texture_id, size, origin: Default::default() })
1092    }
1093
1094    /// Configures the texture to be rendered vertically mirrored.
1095    pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self {
1096        self.0.origin = origin;
1097        self
1098    }
1099
1100    /// Completes the process of building a slint::Image that holds a borrowed OpenGL texture.
1101    pub fn build(self) -> Image {
1102        Image(ImageInner::BorrowedOpenGLTexture(self.0))
1103    }
1104}
1105
1106/// Load an image from an image embedded in the binary.
1107/// This is called by the generated code.
1108#[cfg(feature = "image-decoders")]
1109pub fn load_image_from_embedded_data(data: Slice<'static, u8>, format: Slice<'_, u8>) -> Image {
1110    self::cache::IMAGE_CACHE.with(|global_cache| {
1111        global_cache.borrow_mut().load_image_from_embedded_data(data, format).unwrap_or_default()
1112    })
1113}
1114
1115#[test]
1116fn test_image_size_from_buffer_without_backend() {
1117    {
1118        assert_eq!(Image::default().size(), Default::default());
1119        assert!(Image::default().to_rgb8().is_none());
1120        assert!(Image::default().to_rgba8().is_none());
1121        assert!(Image::default().to_rgba8_premultiplied().is_none());
1122    }
1123    {
1124        let buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
1125        let image = Image::from_rgb8(buffer.clone());
1126        assert_eq!(image.size(), [320, 200].into());
1127        assert_eq!(image.to_rgb8().as_ref().map(|b| b.as_slice()), Some(buffer.as_slice()));
1128    }
1129}
1130
1131#[cfg(feature = "svg")]
1132#[test]
1133fn test_image_size_from_svg() {
1134    let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
1135    let image = Image::load_from_svg_data(simple_svg.as_bytes()).unwrap();
1136    assert_eq!(image.size(), [320, 200].into());
1137    assert_eq!(image.to_rgba8().unwrap().size(), image.size());
1138}
1139
1140#[cfg(feature = "svg")]
1141#[test]
1142fn test_image_invalid_svg() {
1143    let invalid_svg = r#"AaBbCcDd"#;
1144    let result = Image::load_from_svg_data(invalid_svg.as_bytes());
1145    assert!(result.is_err());
1146}
1147
1148/// The result of the fit function
1149#[derive(Debug)]
1150pub struct FitResult {
1151    /// The clip rect in the source image (in source image coordinate)
1152    pub clip_rect: IntRect,
1153    /// The scale to apply to go from the source to the target horizontally
1154    pub source_to_target_x: f32,
1155    /// The scale to apply to go from the source to the target vertically
1156    pub source_to_target_y: f32,
1157    /// The size of the target
1158    pub size: euclid::Size2D<f32, PhysicalPx>,
1159    /// The offset in the target in which we draw the image
1160    pub offset: euclid::Point2D<f32, PhysicalPx>,
1161    /// When Some, it means the image should be tiled instead of stretched to the target
1162    /// but still scaled with the source_to_target_x and source_to_target_y factor
1163    /// The point is the coordinate within the image's clip_rect of the pixel at the offset
1164    pub tiled: Option<euclid::default::Point2D<u32>>,
1165}
1166
1167impl FitResult {
1168    fn adjust_for_tiling(
1169        self,
1170        ratio: f32,
1171        alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1172        tiling: (ImageTiling, ImageTiling),
1173    ) -> Self {
1174        let mut r = self;
1175        let mut tiled = euclid::Point2D::default();
1176        let target = r.size;
1177        let o = r.clip_rect.size.cast::<f32>();
1178        match tiling.0 {
1179            ImageTiling::None => {
1180                r.size.width = o.width * r.source_to_target_x;
1181                if (o.width as f32) > target.width / r.source_to_target_x {
1182                    let diff = (o.width as f32 - target.width / r.source_to_target_x) as i32;
1183                    r.clip_rect.size.width -= diff;
1184                    r.clip_rect.origin.x += match alignment.0 {
1185                        ImageHorizontalAlignment::Center => diff / 2,
1186                        ImageHorizontalAlignment::Left => 0,
1187                        ImageHorizontalAlignment::Right => diff,
1188                    };
1189                    r.size.width = target.width;
1190                } else if (o.width as f32) < target.width / r.source_to_target_x {
1191                    r.offset.x += match alignment.0 {
1192                        ImageHorizontalAlignment::Center => {
1193                            (target.width - o.width as f32 * r.source_to_target_x) / 2.
1194                        }
1195                        ImageHorizontalAlignment::Left => 0.,
1196                        ImageHorizontalAlignment::Right => {
1197                            target.width - o.width as f32 * r.source_to_target_x
1198                        }
1199                    };
1200                }
1201            }
1202            ImageTiling::Repeat => {
1203                tiled.x = match alignment.0 {
1204                    ImageHorizontalAlignment::Left => 0,
1205                    ImageHorizontalAlignment::Center => {
1206                        ((o.width - target.width / ratio) / 2.).rem_euclid(o.width) as u32
1207                    }
1208                    ImageHorizontalAlignment::Right => {
1209                        (-target.width / ratio).rem_euclid(o.width) as u32
1210                    }
1211                };
1212                r.source_to_target_x = ratio;
1213            }
1214            ImageTiling::Round => {
1215                if target.width / ratio <= o.width * 1.5 {
1216                    r.source_to_target_x = target.width / o.width;
1217                } else {
1218                    let mut rem = (target.width / ratio).rem_euclid(o.width);
1219                    if rem > o.width / 2. {
1220                        rem -= o.width;
1221                    }
1222                    r.source_to_target_x = ratio * target.width / (target.width - rem * ratio);
1223                }
1224            }
1225        }
1226
1227        match tiling.1 {
1228            ImageTiling::None => {
1229                r.size.height = o.height * r.source_to_target_y;
1230                if (o.height as f32) > target.height / r.source_to_target_y {
1231                    let diff = (o.height as f32 - target.height / r.source_to_target_y) as i32;
1232                    r.clip_rect.size.height -= diff;
1233                    r.clip_rect.origin.y += match alignment.1 {
1234                        ImageVerticalAlignment::Center => diff / 2,
1235                        ImageVerticalAlignment::Top => 0,
1236                        ImageVerticalAlignment::Bottom => diff,
1237                    };
1238                    r.size.height = target.height;
1239                } else if (o.height as f32) < target.height / r.source_to_target_y {
1240                    r.offset.y += match alignment.1 {
1241                        ImageVerticalAlignment::Center => {
1242                            (target.height - o.height as f32 * r.source_to_target_y) / 2.
1243                        }
1244                        ImageVerticalAlignment::Top => 0.,
1245                        ImageVerticalAlignment::Bottom => {
1246                            target.height - o.height as f32 * r.source_to_target_y
1247                        }
1248                    };
1249                }
1250            }
1251            ImageTiling::Repeat => {
1252                tiled.y = match alignment.1 {
1253                    ImageVerticalAlignment::Top => 0,
1254                    ImageVerticalAlignment::Center => {
1255                        ((o.height - target.height / ratio) / 2.).rem_euclid(o.height) as u32
1256                    }
1257                    ImageVerticalAlignment::Bottom => {
1258                        (-target.height / ratio).rem_euclid(o.height) as u32
1259                    }
1260                };
1261                r.source_to_target_y = ratio;
1262            }
1263            ImageTiling::Round => {
1264                if target.height / ratio <= o.height * 1.5 {
1265                    r.source_to_target_y = target.height / o.height;
1266                } else {
1267                    let mut rem = (target.height / ratio).rem_euclid(o.height);
1268                    if rem > o.height / 2. {
1269                        rem -= o.height;
1270                    }
1271                    r.source_to_target_y = ratio * target.height / (target.height - rem * ratio);
1272                }
1273            }
1274        }
1275        let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1276        r.tiled = has_tiling.then_some(tiled);
1277        r
1278    }
1279}
1280
1281#[cfg(not(feature = "std"))]
1282trait RemEuclid {
1283    fn rem_euclid(self, b: f32) -> f32;
1284}
1285#[cfg(not(feature = "std"))]
1286impl RemEuclid for f32 {
1287    fn rem_euclid(self, b: f32) -> f32 {
1288        num_traits::Euclid::rem_euclid(&self, &b)
1289    }
1290}
1291
1292/// Return an FitResult that can be used to render an image in a buffer that matches a given ImageFit
1293pub fn fit(
1294    image_fit: ImageFit,
1295    target: euclid::Size2D<f32, PhysicalPx>,
1296    source_rect: IntRect,
1297    scale_factor: ScaleFactor,
1298    alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1299    tiling: (ImageTiling, ImageTiling),
1300) -> FitResult {
1301    let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1302    let o = source_rect.size.cast::<f32>();
1303    let ratio = match image_fit {
1304        // If there is any tiling, we ignore image_fit
1305        _ if has_tiling => scale_factor.get(),
1306        ImageFit::Fill => {
1307            return FitResult {
1308                clip_rect: source_rect,
1309                source_to_target_x: target.width / o.width,
1310                source_to_target_y: target.height / o.height,
1311                size: target,
1312                offset: Default::default(),
1313                tiled: None,
1314            };
1315        }
1316        ImageFit::Preserve => scale_factor.get(),
1317        ImageFit::Contain => f32::min(target.width / o.width, target.height / o.height),
1318        ImageFit::Cover => f32::max(target.width / o.width, target.height / o.height),
1319    };
1320
1321    FitResult {
1322        clip_rect: source_rect,
1323        source_to_target_x: ratio,
1324        source_to_target_y: ratio,
1325        size: target,
1326        offset: euclid::Point2D::default(),
1327        tiled: None,
1328    }
1329    .adjust_for_tiling(ratio, alignment, tiling)
1330}
1331
1332/// Generate an iterator of  [`FitResult`] for each slice of a nine-slice border image
1333pub fn fit9slice(
1334    source_rect: IntSize,
1335    [t, r, b, l]: [u16; 4],
1336    target: euclid::Size2D<f32, PhysicalPx>,
1337    scale_factor: ScaleFactor,
1338    alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1339    tiling: (ImageTiling, ImageTiling),
1340) -> impl Iterator<Item = FitResult> {
1341    let fit_to = |clip_rect: euclid::default::Rect<u16>, target: euclid::Rect<f32, PhysicalPx>| {
1342        (!clip_rect.is_empty() && !target.is_empty()).then(|| {
1343            FitResult {
1344                clip_rect: clip_rect.cast(),
1345                source_to_target_x: target.width() / clip_rect.width() as f32,
1346                source_to_target_y: target.height() / clip_rect.height() as f32,
1347                size: target.size,
1348                offset: target.origin,
1349                tiled: None,
1350            }
1351            .adjust_for_tiling(scale_factor.get(), alignment, tiling)
1352        })
1353    };
1354    use euclid::rect;
1355    let sf = |x| scale_factor.get() * x as f32;
1356    let source = source_rect.cast::<u16>();
1357    if t + b > source.height || l + r > source.width {
1358        [None, None, None, None, None, None, None, None, None]
1359    } else {
1360        [
1361            fit_to(rect(0, 0, l, t), rect(0., 0., sf(l), sf(t))),
1362            fit_to(
1363                rect(l, 0, source.width - l - r, t),
1364                rect(sf(l), 0., target.width - sf(l) - sf(r), sf(t)),
1365            ),
1366            fit_to(rect(source.width - r, 0, r, t), rect(target.width - sf(r), 0., sf(r), sf(t))),
1367            fit_to(
1368                rect(0, t, l, source.height - t - b),
1369                rect(0., sf(t), sf(l), target.height - sf(t) - sf(b)),
1370            ),
1371            fit_to(
1372                rect(l, t, source.width - l - r, source.height - t - b),
1373                rect(sf(l), sf(t), target.width - sf(l) - sf(r), target.height - sf(t) - sf(b)),
1374            ),
1375            fit_to(
1376                rect(source.width - r, t, r, source.height - t - b),
1377                rect(target.width - sf(r), sf(t), sf(r), target.height - sf(t) - sf(b)),
1378            ),
1379            fit_to(rect(0, source.height - b, l, b), rect(0., target.height - sf(b), sf(l), sf(b))),
1380            fit_to(
1381                rect(l, source.height - b, source.width - l - r, b),
1382                rect(sf(l), target.height - sf(b), target.width - sf(l) - sf(r), sf(b)),
1383            ),
1384            fit_to(
1385                rect(source.width - r, source.height - b, r, b),
1386                rect(target.width - sf(r), target.height - sf(b), sf(r), sf(b)),
1387            ),
1388        ]
1389    }
1390    .into_iter()
1391    .flatten()
1392}
1393
1394#[cfg(feature = "ffi")]
1395pub(crate) mod ffi {
1396    #![allow(unsafe_code)]
1397
1398    use super::*;
1399
1400    // Expand Rgb8Pixel so that cbindgen can see it. (is in fact rgb::RGB<u8>)
1401    /// Represents an RGB pixel.
1402    #[cfg(cbindgen)]
1403    #[repr(C)]
1404    struct Rgb8Pixel {
1405        /// red value (between 0 and 255)
1406        r: u8,
1407        /// green value (between 0 and 255)
1408        g: u8,
1409        /// blue value (between 0 and 255)
1410        b: u8,
1411    }
1412
1413    // Expand Rgba8Pixel so that cbindgen can see it. (is in fact rgb::RGBA<u8>)
1414    /// Represents an RGBA pixel.
1415    #[cfg(cbindgen)]
1416    #[repr(C)]
1417    struct Rgba8Pixel {
1418        /// red value (between 0 and 255)
1419        r: u8,
1420        /// green value (between 0 and 255)
1421        g: u8,
1422        /// blue value (between 0 and 255)
1423        b: u8,
1424        /// alpha value (between 0 and 255)
1425        a: u8,
1426    }
1427
1428    #[cfg(feature = "image-decoders")]
1429    #[unsafe(no_mangle)]
1430    pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) {
1431        unsafe {
1432            core::ptr::write(
1433                image,
1434                Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or_default(),
1435            )
1436        }
1437    }
1438
1439    #[cfg(feature = "std")]
1440    #[unsafe(no_mangle)]
1441    pub unsafe extern "C" fn slint_image_load_from_embedded_data(
1442        data: Slice<'static, u8>,
1443        format: Slice<'static, u8>,
1444        image: *mut Image,
1445    ) {
1446        unsafe { core::ptr::write(image, super::load_image_from_embedded_data(data, format)) };
1447    }
1448
1449    #[unsafe(no_mangle)]
1450    pub extern "C" fn slint_image_size(image: &Image) -> IntSize {
1451        image.size()
1452    }
1453
1454    #[unsafe(no_mangle)]
1455    pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
1456        match &image.0 {
1457            #[cfg(feature = "std")]
1458            ImageInner::EmbeddedImage {
1459                cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1460                ..
1461            } => Some(path),
1462            ImageInner::NineSlice(nine) => match &nine.0 {
1463                #[cfg(feature = "std")]
1464                ImageInner::EmbeddedImage {
1465                    cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1466                    ..
1467                } => Some(path),
1468                _ => None,
1469            },
1470            _ => None,
1471        }
1472    }
1473
1474    #[unsafe(no_mangle)]
1475    pub unsafe extern "C" fn slint_image_from_embedded_textures(
1476        textures: &'static StaticTextures,
1477        image: *mut Image,
1478    ) {
1479        unsafe { core::ptr::write(image, Image::from(ImageInner::StaticTextures(textures))) };
1480    }
1481
1482    #[unsafe(no_mangle)]
1483    pub extern "C" fn slint_image_compare_equal(image1: &Image, image2: &Image) -> bool {
1484        image1.eq(image2)
1485    }
1486
1487    /// Call [`Image::set_nine_slice_edges`]
1488    #[unsafe(no_mangle)]
1489    pub extern "C" fn slint_image_set_nine_slice_edges(
1490        image: &mut Image,
1491        top: u16,
1492        right: u16,
1493        bottom: u16,
1494        left: u16,
1495    ) {
1496        image.set_nine_slice_edges(top, right, bottom, left);
1497    }
1498
1499    #[unsafe(no_mangle)]
1500    pub extern "C" fn slint_image_to_rgb8(
1501        image: &Image,
1502        data: &mut SharedVector<Rgb8Pixel>,
1503        width: &mut u32,
1504        height: &mut u32,
1505    ) -> bool {
1506        image.to_rgb8().is_some_and(|pixel_buffer| {
1507            *data = pixel_buffer.data.clone();
1508            *width = pixel_buffer.width();
1509            *height = pixel_buffer.height();
1510            true
1511        })
1512    }
1513
1514    #[unsafe(no_mangle)]
1515    pub extern "C" fn slint_image_to_rgba8(
1516        image: &Image,
1517        data: &mut SharedVector<Rgba8Pixel>,
1518        width: &mut u32,
1519        height: &mut u32,
1520    ) -> bool {
1521        image.to_rgba8().is_some_and(|pixel_buffer| {
1522            *data = pixel_buffer.data.clone();
1523            *width = pixel_buffer.width();
1524            *height = pixel_buffer.height();
1525            true
1526        })
1527    }
1528
1529    #[unsafe(no_mangle)]
1530    pub extern "C" fn slint_image_to_rgba8_premultiplied(
1531        image: &Image,
1532        data: &mut SharedVector<Rgba8Pixel>,
1533        width: &mut u32,
1534        height: &mut u32,
1535    ) -> bool {
1536        image.to_rgba8_premultiplied().is_some_and(|pixel_buffer| {
1537            *data = pixel_buffer.data.clone();
1538            *width = pixel_buffer.width();
1539            *height = pixel_buffer.height();
1540            true
1541        })
1542    }
1543}
1544
1545/// This structure contains fields to identify and render an OpenGL texture that Slint borrows from the application code.
1546/// Use this to embed a native OpenGL texture into a Slint scene.
1547///
1548/// The ownership of the texture remains with the application. It is the application's responsibility to delete the texture
1549/// when it is not used anymore.
1550///
1551/// Note that only 2D RGBA textures are supported.
1552#[derive(Clone, Debug, PartialEq)]
1553#[non_exhaustive]
1554#[cfg(not(target_arch = "wasm32"))]
1555#[repr(C)]
1556pub struct BorrowedOpenGLTexture {
1557    /// The id or name of the texture, as created by [`glGenTextures`](https://registry.khronos.org/OpenGL-Refpages/gl4/html/glGenTextures.xhtml).
1558    pub texture_id: core::num::NonZeroU32,
1559    /// The size of the texture in pixels.
1560    pub size: IntSize,
1561    /// Origin of the texture when rendering.
1562    pub origin: BorrowedOpenGLTextureOrigin,
1563}
1564
1565#[cfg(test)]
1566mod tests {
1567    use crate::graphics::Rgba8Pixel;
1568
1569    use super::Image;
1570
1571    #[test]
1572    fn test_premultiplied_to_rgb_zero_alpha() {
1573        let pixel = Rgba8Pixel::new(5, 10, 15, 0);
1574        let converted = Image::premultiplied_rgba_to_rgba(pixel);
1575        assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1576    }
1577
1578    #[test]
1579    fn test_premultiplied_to_rgb_full_alpha() {
1580        let pixel = Rgba8Pixel::new(5, 10, 15, 255);
1581        let converted = Image::premultiplied_rgba_to_rgba(pixel);
1582        assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 255));
1583    }
1584
1585    #[test]
1586    fn test_premultiplied_to_rgb() {
1587        let pixel = Rgba8Pixel::new(5, 10, 15, 128);
1588        let converted = Image::premultiplied_rgba_to_rgba(pixel);
1589        assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 128));
1590    }
1591
1592    #[test]
1593    fn test_rgb_to_premultiplied_zero_alpha() {
1594        let pixel = Rgba8Pixel::new(10, 20, 30, 0);
1595        let converted = Image::rgba_to_premultiplied_rgba(pixel);
1596        assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1597    }
1598
1599    #[test]
1600    fn test_rgb_to_premultiplied_full_alpha() {
1601        let pixel = Rgba8Pixel::new(10, 20, 30, 255);
1602        let converted = Image::rgba_to_premultiplied_rgba(pixel);
1603        assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 255));
1604    }
1605
1606    #[test]
1607    fn test_rgb_to_premultiplied() {
1608        let pixel = Rgba8Pixel::new(10, 20, 30, 128);
1609        let converted = Image::rgba_to_premultiplied_rgba(pixel);
1610        assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 128));
1611    }
1612}