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(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
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(all(feature = "image-decoders", not(target_arch = "wasm32")))]
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-29", feature = "unstable-wgpu-30"))]
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#[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
382#[derive(Clone, Debug)]
383pub enum WGPUTexture {
384    /// A texture for WGPU version 29.
385    #[cfg(feature = "unstable-wgpu-29")]
386    WGPU29Texture(wgpu_29::Texture),
387    /// A texture for WGPU version 30.
388    #[cfg(feature = "unstable-wgpu-30")]
389    WGPU30Texture(wgpu_30::Texture),
390}
391
392#[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
393impl OpaqueImage for WGPUTexture {
394    fn size(&self) -> IntSize {
395        match self {
396            #[cfg(feature = "unstable-wgpu-29")]
397            Self::WGPU29Texture(texture) => {
398                let size = texture.size();
399                (size.width, size.height).into()
400            }
401            #[cfg(feature = "unstable-wgpu-30")]
402            Self::WGPU30Texture(texture) => {
403                let size = texture.size();
404                (size.width, size.height).into()
405            }
406        }
407    }
408    fn cache_key(&self) -> ImageCacheKey {
409        ImageCacheKey::Invalid
410    }
411}
412
413/// A resource is a reference to binary data, for example images. They can be accessible on the file
414/// system or embedded in the resulting binary. Or they might be URLs to a web server and a downloaded
415/// is necessary before they can be used.
416/// cbindgen:prefix-with-name
417#[derive(Clone, Debug, Default)]
418#[repr(u8)]
419#[allow(missing_docs)]
420pub enum ImageInner {
421    /// A resource that does not represent any data.
422    #[default]
423    None = 0,
424    EmbeddedImage {
425        cache_key: ImageCacheKey,
426        buffer: SharedImageBuffer,
427    } = 1,
428    #[cfg(feature = "svg")]
429    Svg(vtable::VRc<OpaqueImageVTable, svg::ParsedSVG>) = 2,
430    StaticTextures(&'static StaticTextures) = 3,
431    #[cfg(target_arch = "wasm32")]
432    HTMLImage(vtable::VRc<OpaqueImageVTable, htmlimage::HTMLImage>) = 4,
433    BackendStorage(vtable::VRc<OpaqueImageVTable>) = 5,
434    #[cfg(not(target_arch = "wasm32"))]
435    BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6,
436    NineSlice(vtable::VRc<OpaqueImageVTable, NineSliceImage>) = 7,
437    #[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
438    WGPUTexture(WGPUTexture) = 8,
439}
440
441impl ImageInner {
442    /// Return or render the image into a buffer
443    ///
444    /// `target_size_for_scalable_source` is the size to use if the image is scalable.
445    /// (when unspecified, will default to the intrinsic size of the image)
446    ///
447    /// Returns None if the image can't be rendered in a buffer or if the image is empty
448    pub fn render_to_buffer(
449        &self,
450        _target_size_for_scalable_source: Option<euclid::Size2D<u32, PhysicalPx>>,
451    ) -> Option<SharedImageBuffer> {
452        match self {
453            ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()),
454            #[cfg(feature = "svg")]
455            ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) {
456                Ok(b) => Some(b),
457                // Ignore error when rendering a 0x0 image, that's just an empty image
458                Err(resvg::usvg::Error::InvalidSize) => None,
459                Err(err) => {
460                    std::eprintln!("Error rendering SVG: {err}");
461                    None
462                }
463            },
464            ImageInner::StaticTextures(ts) => {
465                let mut buffer =
466                    SharedPixelBuffer::<Rgba8Pixel>::new(ts.size.width, ts.size.height);
467                let stride = buffer.width() as usize;
468                let slice = buffer.make_mut_slice();
469                for t in ts.textures.iter() {
470                    let rect = t.rect.to_usize();
471                    for y in 0..rect.height() {
472                        let slice = &mut slice[(rect.min_y() + y) * stride..][rect.x_range()];
473                        let source = &ts.data[t.index + y * rect.width() * t.format.bpp()..];
474                        match t.format {
475                            TexturePixelFormat::Rgb => {
476                                let mut iter = source
477                                    .as_chunks::<3>()
478                                    .0
479                                    .iter()
480                                    .map(|p| Rgba8Pixel { r: p[0], g: p[1], b: p[2], a: 255 });
481                                slice.fill_with(|| iter.next().unwrap());
482                            }
483                            TexturePixelFormat::RgbaPremultiplied => {
484                                let mut iter = source
485                                    .as_chunks::<4>()
486                                    .0
487                                    .iter()
488                                    .map(|p| Rgba8Pixel { r: p[0], g: p[1], b: p[2], a: p[3] });
489                                slice.fill_with(|| iter.next().unwrap());
490                            }
491                            TexturePixelFormat::Rgba => {
492                                let mut iter = source.as_chunks::<4>().0.iter().map(|p| {
493                                    let a = p[3];
494                                    Rgba8Pixel {
495                                        r: (p[0] as u16 * a as u16 / 255) as u8,
496                                        g: (p[1] as u16 * a as u16 / 255) as u8,
497                                        b: (p[2] as u16 * a as u16 / 255) as u8,
498                                        a,
499                                    }
500                                });
501                                slice.fill_with(|| iter.next().unwrap());
502                            }
503                            TexturePixelFormat::AlphaMap => {
504                                let col = t.color.to_argb_u8();
505                                let mut iter = source.iter().map(|p| {
506                                    let a = *p as u32 * col.alpha as u32;
507                                    Rgba8Pixel {
508                                        r: (col.red as u32 * a / (255 * 255)) as u8,
509                                        g: (col.green as u32 * a / (255 * 255)) as u8,
510                                        b: (col.blue as u32 * a / (255 * 255)) as u8,
511                                        a: (a / 255) as u8,
512                                    }
513                                });
514                                slice.fill_with(|| iter.next().unwrap());
515                            }
516                            TexturePixelFormat::SignedDistanceField => {
517                                todo!("converting from a signed distance field to an image")
518                            }
519                        };
520                    }
521                }
522                Some(SharedImageBuffer::RGBA8Premultiplied(buffer))
523            }
524            ImageInner::NineSlice(nine) => nine.0.render_to_buffer(None),
525            _ => None,
526        }
527    }
528
529    /// Returns true if the image is an SVG (either backed by resvg or HTML image wrapper).
530    pub fn is_svg(&self) -> bool {
531        match self {
532            #[cfg(feature = "svg")]
533            Self::Svg(_) => true,
534            #[cfg(target_arch = "wasm32")]
535            Self::HTMLImage(html_image) => html_image.is_svg(),
536            _ => false,
537        }
538    }
539
540    /// Return the image size
541    pub fn size(&self) -> IntSize {
542        match self {
543            ImageInner::None => Default::default(),
544            ImageInner::EmbeddedImage { buffer, .. } => buffer.size(),
545            ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
546            #[cfg(feature = "svg")]
547            ImageInner::Svg(svg) => svg.size(),
548            #[cfg(target_arch = "wasm32")]
549            ImageInner::HTMLImage(htmlimage) => htmlimage.size().unwrap_or_default(),
550            ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size(),
551            #[cfg(not(target_arch = "wasm32"))]
552            ImageInner::BorrowedOpenGLTexture(BorrowedOpenGLTexture { size, .. }) => *size,
553            ImageInner::NineSlice(nine) => nine.0.size(),
554            #[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
555            ImageInner::WGPUTexture(texture) => texture.size(),
556        }
557    }
558
559    /// Internal helper to abstract over either loading from a file or parsing internal data.
560    ///
561    /// This can create an `ImageInner` with a dangling cache key reference if used incorrectly,
562    /// which could lead to bad behavior. This constructor should be called from within
563    /// `ImageCache::lookup_image_in_cache_or_create`, or `ImageCacheKey::Invalid` should be
564    /// supplied.
565    #[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
566    pub(crate) fn load_from_data_with_cache_key(
567        cache_key: ImageCacheKey,
568        data: Slice<'_, u8>,
569        format: Slice<'_, u8>,
570    ) -> Option<Self> {
571        // On the web, let the browser decode the image instead of shipping decoders in the binary.
572        #[cfg(target_arch = "wasm32")]
573        {
574            let _ = cache_key;
575            let mime_type = core::str::from_utf8(format.as_slice())
576                .ok()
577                .and_then(image_mime_type_from_extension)
578                .unwrap_or_else(|| {
579                    if data.starts_with(b"<?xml") || data.starts_with(b"<svg") {
580                        "image/svg+xml"
581                    } else {
582                        // An empty type makes the browser sniff the format from the data.
583                        ""
584                    }
585                });
586            if mime_type == "image/svg+xml" && data.starts_with(&[0x1f, 0x8b]) {
587                crate::debug_log!("Compressed SVG (.svgz) is not supported on the web");
588                return None;
589            }
590            return htmlimage::HTMLImage::new_from_data(data.as_slice(), mime_type)
591                .map(|html_image| ImageInner::HTMLImage(vtable::VRc::new(html_image)));
592        }
593
594        #[cfg(not(target_arch = "wasm32"))]
595        {
596            #[cfg(feature = "svg")]
597            if format.as_slice() == b"svg"
598                || format.as_slice() == b"svgz"
599                || (format.is_empty() && (data.starts_with(b"<?xml") || data.starts_with(b"<svg")))
600            {
601                return Some(ImageInner::Svg(vtable::VRc::new(
602                    svg::load_from_data(data.as_slice(), cache_key).map_or_else(
603                        |svg_err| {
604                            crate::debug_log!("Error loading SVG: {}", svg_err);
605                            None
606                        },
607                        Some,
608                    )?,
609                )));
610            }
611
612            let format = std::str::from_utf8(format.as_slice())
613                .ok()
614                .and_then(image::ImageFormat::from_extension);
615            let maybe_image = if let Some(format) = format {
616                image::load_from_memory_with_format(data.as_slice(), format)
617            } else {
618                image::load_from_memory(data.as_slice())
619            };
620
621            match maybe_image {
622                Ok(image) => Some(ImageInner::EmbeddedImage {
623                    cache_key,
624                    buffer: dynamic_image_to_shared_image_buffer(image),
625                }),
626                Err(decode_err) => {
627                    crate::debug_log!("Error decoding embedded image: {}", decode_err);
628                    None
629                }
630            }
631        }
632    }
633}
634
635/// Convert `image::DynamicImage` to `SharedImageBuffer`
636#[cfg(all(feature = "image-decoders", not(target_arch = "wasm32")))]
637fn dynamic_image_to_shared_image_buffer(dynamic_image: image::DynamicImage) -> SharedImageBuffer {
638    use rgb::AsPixels;
639
640    if dynamic_image.color().has_alpha() {
641        let rgba8image = dynamic_image.to_rgba8();
642        // Prefer pre-multiplied alpha so that smooth-scaling won't bleed the alpha when blending
643        // in the renderers.
644        SharedImageBuffer::RGBA8Premultiplied(SharedPixelBuffer {
645            width: rgba8image.width(),
646            height: rgba8image.height(),
647            data: rgba8image
648                .as_pixels()
649                .iter()
650                .map(|pixel| Image::rgba_to_premultiplied_rgba(*pixel))
651                .collect(),
652        })
653    } else {
654        let rgb8image = dynamic_image.to_rgb8();
655        SharedImageBuffer::RGB8(SharedPixelBuffer::clone_from_slice(
656            rgb8image.as_raw(),
657            rgb8image.width(),
658            rgb8image.height(),
659        ))
660    }
661}
662
663impl PartialEq for ImageInner {
664    fn eq(&self, other: &Self) -> bool {
665        match (self, other) {
666            (
667                Self::EmbeddedImage { cache_key: l_cache_key, buffer: l_buffer },
668                Self::EmbeddedImage { cache_key: r_cache_key, buffer: r_buffer },
669            ) => l_cache_key == r_cache_key && l_buffer == r_buffer,
670            #[cfg(feature = "svg")]
671            (Self::Svg(l0), Self::Svg(r0)) => vtable::VRc::ptr_eq(l0, r0),
672            (Self::StaticTextures(l0), Self::StaticTextures(r0)) => l0 == r0,
673            #[cfg(target_arch = "wasm32")]
674            (Self::HTMLImage(l0), Self::HTMLImage(r0)) => vtable::VRc::ptr_eq(l0, r0),
675            (Self::BackendStorage(l0), Self::BackendStorage(r0)) => vtable::VRc::ptr_eq(l0, r0),
676            #[cfg(not(target_arch = "wasm32"))]
677            (Self::BorrowedOpenGLTexture(l0), Self::BorrowedOpenGLTexture(r0)) => l0 == r0,
678            (Self::NineSlice(l), Self::NineSlice(r)) => l.0 == r.0 && l.1 == r.1,
679            _ => false,
680        }
681    }
682}
683
684impl<'a> From<&'a Image> for &'a ImageInner {
685    fn from(other: &'a Image) -> Self {
686        &other.0
687    }
688}
689
690/// Error generated if an image cannot be loaded for any reasons.
691#[derive(Default, Debug, PartialEq)]
692pub struct LoadImageError(());
693
694impl core::fmt::Display for LoadImageError {
695    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
696        f.write_str("The image cannot be loaded")
697    }
698}
699
700#[cfg(feature = "std")]
701impl std::error::Error for LoadImageError {}
702
703/// An image type that can be displayed by the Image element. You can construct
704/// Image objects from a path to an image file on disk, using [`Self::load_from_path`].
705///
706/// Another typical use-case is to render the image content with Rust code.
707/// For this it's most efficient to create a new SharedPixelBuffer with the known dimensions
708/// and pass the mutable slice to your rendering function. Afterwards you can create an
709/// Image.
710///
711/// The following example creates a 320x200 RGB pixel buffer and calls an external
712/// low_level_render() function to draw a shape into it. Finally the result is
713/// stored in an Image with [`Self::from_rgb8()`]:
714/// ```
715/// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgb8Pixel};
716///
717/// fn low_level_render(width: u32, height: u32, buffer: &mut [u8]) {
718///     // render beautiful circle or other shapes here
719/// }
720///
721/// let mut pixel_buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
722///
723/// low_level_render(pixel_buffer.width(), pixel_buffer.height(),
724///                  pixel_buffer.make_mut_bytes());
725///
726/// let image = Image::from_rgb8(pixel_buffer);
727/// ```
728///
729/// Another use-case is to import existing image data into Slint, by
730/// creating a new Image through cloning of another image type.
731///
732/// The following example uses the popular [image crate](https://docs.rs/image/) to
733/// load a `.png` file from disk, apply brightening filter on it and then import
734/// it into an [`Image`]:
735/// ```no_run
736/// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgba8Pixel};
737/// let mut cat_image = image::open("cat.png").expect("Error loading cat image").into_rgba8();
738///
739/// image::imageops::colorops::brighten_in_place(&mut cat_image, 20);
740///
741/// let buffer = SharedPixelBuffer::<Rgba8Pixel>::clone_from_slice(
742///     cat_image.as_raw(),
743///     cat_image.width(),
744///     cat_image.height(),
745/// );
746/// let image = Image::from_rgba8(buffer);
747/// ```
748///
749/// A popular software (CPU) rendering library in Rust is tiny-skia. The following example shows
750/// how to use tiny-skia to render into a [`SharedPixelBuffer`]:
751/// ```
752/// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgba8Pixel};
753/// let mut pixel_buffer = SharedPixelBuffer::<Rgba8Pixel>::new(640, 480);
754/// let width = pixel_buffer.width();
755/// let height = pixel_buffer.height();
756/// let mut pixmap = tiny_skia::PixmapMut::from_bytes(
757///     pixel_buffer.make_mut_bytes(), width, height
758/// ).unwrap();
759/// pixmap.fill(tiny_skia::Color::TRANSPARENT);
760///
761/// let circle = tiny_skia::PathBuilder::from_circle(320., 240., 150.).unwrap();
762///
763/// let mut paint = tiny_skia::Paint::default();
764/// paint.shader = tiny_skia::LinearGradient::new(
765///     tiny_skia::Point::from_xy(100.0, 100.0),
766///     tiny_skia::Point::from_xy(400.0, 400.0),
767///     vec![
768///         tiny_skia::GradientStop::new(0.0, tiny_skia::Color::from_rgba8(50, 127, 150, 200)),
769///         tiny_skia::GradientStop::new(1.0, tiny_skia::Color::from_rgba8(220, 140, 75, 180)),
770///     ],
771///     tiny_skia::SpreadMode::Pad,
772///     tiny_skia::Transform::identity(),
773/// ).unwrap();
774///
775/// pixmap.fill_path(&circle, &paint, tiny_skia::FillRule::Winding, Default::default(), None);
776///
777/// let image = Image::from_rgba8_premultiplied(pixel_buffer);
778/// ```
779///
780/// ### Sending Image to a thread
781///
782/// `Image` is not [`Send`], because it uses internal cache that are local to the Slint thread.
783/// If you want to create image data in a thread and send that to slint, construct the
784/// [`SharedPixelBuffer`] in a thread, and send that to Slint's UI thread.
785///
786/// ```rust,no_run
787/// # use i_slint_core::graphics::{SharedPixelBuffer, Image, Rgba8Pixel};
788/// std::thread::spawn(move || {
789///     let mut pixel_buffer = SharedPixelBuffer::<Rgba8Pixel>::new(640, 480);
790///     // ... fill the pixel_buffer with data as shown in the previous example ...
791///     slint::invoke_from_event_loop(move || {
792///         // this will run in the Slint's UI thread
793///         let image = Image::from_rgba8_premultiplied(pixel_buffer);
794///         // ... use the image, eg:
795///         // my_ui_handle.upgrade().unwrap().set_image(image);
796///     });
797/// });
798/// ```
799#[repr(transparent)]
800#[derive(Default, Clone, Debug, PartialEq, derive_more::From)]
801pub struct Image(pub(crate) ImageInner);
802
803impl Image {
804    #[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
805    /// Load an Image from a path to a file containing an image.
806    ///
807    /// Supported formats are SVG, PNG and JPEG.
808    /// Enable support for additional formats supported by the [`image` crate](https://crates.io/crates/image) (
809    /// AVIF, BMP, DDS, Farbfeld, GIF, HDR, ICO, JPEG, EXR, PNG, PNM, QOI, TGA, TIFF, WebP)
810    /// by enabling the `image-default-formats` cargo feature.
811    ///
812    /// This function always fails on the web, where there is no file system.
813    pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> {
814        self::cache::IMAGE_CACHE.with(|global_cache| {
815            let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into();
816            global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(()))
817        })
818    }
819
820    /// Creates a new Image from the specified shared pixel buffer, where each pixel has three color
821    /// channels (red, green and blue) encoded as u8.
822    pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
823        Image(ImageInner::EmbeddedImage {
824            cache_key: ImageCacheKey::Invalid,
825            buffer: SharedImageBuffer::RGB8(buffer),
826        })
827    }
828
829    /// Creates a new Image from the specified shared pixel buffer, where each pixel has four color
830    /// channels (red, green, blue and alpha) encoded as u8.
831    pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
832        Image(ImageInner::EmbeddedImage {
833            cache_key: ImageCacheKey::Invalid,
834            buffer: SharedImageBuffer::RGBA8(buffer),
835        })
836    }
837
838    /// Creates a new Image from the specified shared pixel buffer, where each pixel has four color
839    /// channels (red, green, blue and alpha) encoded as u8 and, in contrast to [`Self::from_rgba8`],
840    /// the alpha channel is also assumed to be multiplied to the red, green and blue channels.
841    ///
842    /// Only construct an Image with this function if you know that your pixels are encoded this way.
843    pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
844        Image(ImageInner::EmbeddedImage {
845            cache_key: ImageCacheKey::Invalid,
846            buffer: SharedImageBuffer::RGBA8Premultiplied(buffer),
847        })
848    }
849
850    /// Returns the pixel buffer for the Image if available in RGB format without alpha.
851    /// Returns None if the pixels cannot be obtained, for example when the image was created from borrowed OpenGL textures.
852    pub fn to_rgb8(&self) -> Option<SharedPixelBuffer<Rgb8Pixel>> {
853        self.0.render_to_buffer(None).and_then(|image| match image {
854            SharedImageBuffer::RGB8(buffer) => Some(buffer),
855            _ => None,
856        })
857    }
858
859    /// Returns the pixel buffer for the Image if available in RGBA format.
860    /// Returns None if the pixels cannot be obtained, for example when the image was created from borrowed OpenGL textures.
861    pub fn to_rgba8(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
862        self.render_to_rgba8(None)
863    }
864
865    fn render_to_rgba8(
866        &self,
867        target_size: Option<euclid::Size2D<u32, PhysicalPx>>,
868    ) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
869        self.0.render_to_buffer(target_size).map(|image| match image {
870            SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
871                width: buffer.width,
872                height: buffer.height,
873                data: buffer.data.into_iter().map(Into::into).collect(),
874            },
875            SharedImageBuffer::RGBA8(buffer) => buffer,
876            SharedImageBuffer::RGBA8Premultiplied(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
877                width: buffer.width,
878                height: buffer.height,
879                data: buffer.data.into_iter().map(Image::premultiplied_rgba_to_rgba).collect(),
880            },
881        })
882    }
883
884    /// Returns the pixel buffer for the Image if available in RGBA format, with the alpha channel pre-multiplied
885    /// to the red, green, and blue channels.
886    /// Returns None if the pixels cannot be obtained, for example when the image was created from borrowed OpenGL textures.
887    pub fn to_rgba8_premultiplied(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
888        self.0.render_to_buffer(None).map(|image| match image {
889            SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
890                width: buffer.width,
891                height: buffer.height,
892                data: buffer.data.into_iter().map(Into::into).collect(),
893            },
894            SharedImageBuffer::RGBA8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
895                width: buffer.width,
896                height: buffer.height,
897                data: buffer.data.into_iter().map(Image::rgba_to_premultiplied_rgba).collect(),
898            },
899            SharedImageBuffer::RGBA8Premultiplied(buffer) => buffer,
900        })
901    }
902
903    /// Returns the pixel converted from premultiplied RGBA to RGBA.
904    fn premultiplied_rgba_to_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
905        if pixel.a == 0 {
906            Rgba8Pixel::new(0, 0, 0, 0)
907        } else {
908            let af = pixel.a as u32;
909            let round = (af / 2) as u32;
910            Rgba8Pixel {
911                r: ((pixel.r as u32 * 255 + round) / af).min(255) as u8,
912                g: ((pixel.g as u32 * 255 + round) / af).min(255) as u8,
913                b: ((pixel.b as u32 * 255 + round) / af).min(255) as u8,
914                a: pixel.a,
915            }
916        }
917    }
918
919    /// Returns the pixel converted from RGBA to premultiplied RGBA.
920    fn rgba_to_premultiplied_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
921        if pixel.a == 255 {
922            pixel
923        } else {
924            let af = pixel.a as u32;
925            Rgba8Pixel {
926                r: (((pixel.r as u32 * af + 128) * 257) >> 16) as u8,
927                g: (((pixel.g as u32 * af + 128) * 257) >> 16) as u8,
928                b: (((pixel.b as u32 * af + 128) * 257) >> 16) as u8,
929                a: pixel.a,
930            }
931        }
932    }
933
934    /// Returns the [WGPU](http://wgpu.rs) 29.x texture that this image wraps; returns None if the image does not
935    /// hold such a previously wrapped texture.
936    ///
937    /// *Note*: This function is behind a feature flag and may be removed or changed in future minor releases,
938    ///         as new major WGPU releases become available.
939    #[cfg(feature = "unstable-wgpu-29")]
940    pub fn to_wgpu_29_texture(&self) -> Option<wgpu_29::Texture> {
941        match &self.0 {
942            ImageInner::WGPUTexture(WGPUTexture::WGPU29Texture(texture)) => Some(texture.clone()),
943            _ => None,
944        }
945    }
946
947    /// Returns the [WGPU](http://wgpu.rs) 30.x texture that this image wraps; returns None if the image does not
948    /// hold such a previously wrapped texture.
949    ///
950    /// *Note*: This function is behind a feature flag and may be removed or changed in future minor releases,
951    ///         as new major WGPU releases become available.
952    #[cfg(feature = "unstable-wgpu-30")]
953    pub fn to_wgpu_30_texture(&self) -> Option<wgpu_30::Texture> {
954        match &self.0 {
955            ImageInner::WGPUTexture(WGPUTexture::WGPU30Texture(texture)) => Some(texture.clone()),
956            _ => None,
957        }
958    }
959
960    /// Creates a new Image from an existing OpenGL texture. The texture remains borrowed by Slint
961    /// for the duration of being used for rendering, such as when assigned as source property to
962    /// an `Image` element. It's the application's responsibility to delete the texture when it is
963    /// not used anymore.
964    ///
965    /// The texture must be bindable against the `GL_TEXTURE_2D` target, have `GL_RGBA` as format
966    /// for the pixel data.
967    ///
968    /// When Slint renders the texture, it assumes that the origin of the texture is at the top-left.
969    /// This is different from the default OpenGL coordinate system.
970    ///
971    /// # Safety
972    ///
973    /// This function is unsafe because invalid texture ids may lead to undefined behavior in OpenGL
974    /// drivers. A valid texture id is one that was created by the same OpenGL context that is
975    /// current during any of the invocations of the callback set on [`Window::set_rendering_notifier()`](crate::api::Window::set_rendering_notifier).
976    /// OpenGL contexts between instances of [`slint::Window`](crate::api::Window) are not sharing resources. Consequently
977    /// [`slint::Image`](Self) objects created from borrowed OpenGL textures cannot be shared between
978    /// different windows.
979    #[allow(unsafe_code)]
980    #[cfg(not(target_arch = "wasm32"))]
981    #[deprecated(since = "1.2.0", note = "Use BorrowedOpenGLTextureBuilder")]
982    pub unsafe fn from_borrowed_gl_2d_rgba_texture(
983        texture_id: core::num::NonZeroU32,
984        size: IntSize,
985    ) -> Self {
986        unsafe { BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size).build() }
987    }
988
989    /// Creates a new Image from the specified buffer, which contains SVG raw data.
990    ///
991    /// On the web, the browser renders the SVG, and compressed SVG data (svgz) is not supported.
992    #[cfg(any(feature = "svg", target_arch = "wasm32"))]
993    pub fn load_from_svg_data(buffer: &[u8]) -> Result<Self, LoadImageError> {
994        // On the web, the browser decodes the SVG.
995        #[cfg(target_arch = "wasm32")]
996        {
997            htmlimage::HTMLImage::new_from_data(buffer, "image/svg+xml")
998                .map(|html_image| Image(ImageInner::HTMLImage(vtable::VRc::new(html_image))))
999                .ok_or(LoadImageError(()))
1000        }
1001        #[cfg(not(target_arch = "wasm32"))]
1002        {
1003            let cache_key = ImageCacheKey::Invalid;
1004            Ok(Image(ImageInner::Svg(vtable::VRc::new(
1005                svg::load_from_data(buffer, cache_key).map_err(|_| LoadImageError(()))?,
1006            ))))
1007        }
1008    }
1009
1010    /// Creates a new Image from a buffer in memory holding the content of an encoded image file,
1011    /// such as a PNG, JPEG or SVG.
1012    ///
1013    /// `format` is the lowercase file extension of the encoded data (for example `"png"`, `"jpg"`
1014    /// or `"svg"`). Pass `None` to guess the format from the data; SVG is only recognized by this
1015    /// guess when the data begins with an `<?xml` or `<svg` tag, otherwise pass `Some("svg")`.
1016    ///
1017    /// The supported formats are the same as for [`Self::load_from_path`].
1018    #[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
1019    pub fn load_from_data(data: &[u8], format: Option<&str>) -> Result<Self, LoadImageError> {
1020        ImageInner::load_from_data_with_cache_key(
1021            ImageCacheKey::Invalid,
1022            Slice::from_slice(data),
1023            Slice::from_slice(format.unwrap_or_default().as_bytes()),
1024        )
1025        .map(Image)
1026        .ok_or(LoadImageError(()))
1027    }
1028
1029    /// Sets the nine-slice edges of the image.
1030    ///
1031    /// [Nine-slice scaling](https://en.wikipedia.org/wiki/9-slice_scaling) is a method for scaling
1032    /// images in such a way that the corners are not distorted.
1033    /// The arguments define the pixel sizes of the edges that cut the image into 9 slices.
1034    pub fn set_nine_slice_edges(&mut self, top: u16, right: u16, bottom: u16, left: u16) {
1035        if top == 0 && left == 0 && right == 0 && bottom == 0 {
1036            if let ImageInner::NineSlice(n) = &self.0 {
1037                self.0 = n.0.clone();
1038            }
1039        } else {
1040            let array = [top, right, bottom, left];
1041            let inner = if let ImageInner::NineSlice(n) = &mut self.0 {
1042                n.0.clone()
1043            } else {
1044                self.0.clone()
1045            };
1046            self.0 = ImageInner::NineSlice(vtable::VRc::new(NineSliceImage(inner, array)));
1047        }
1048    }
1049
1050    /// Returns the size of the Image in pixels.
1051    pub fn size(&self) -> IntSize {
1052        self.0.size()
1053    }
1054
1055    #[cfg(feature = "std")]
1056    /// Returns the path of the image on disk, if it was constructed via [`Self::load_from_path`].
1057    ///
1058    /// For example:
1059    /// ```
1060    /// # use std::path::Path;
1061    /// # use i_slint_core::graphics::*;
1062    /// let path_buf = Path::new(env!("CARGO_MANIFEST_DIR"))
1063    ///     .join("../../demos/printerdemo/ui/images/cat.jpg");
1064    /// let image = Image::load_from_path(&path_buf).unwrap();
1065    /// assert_eq!(image.path(), Some(path_buf.as_path()));
1066    /// ```
1067    pub fn path(&self) -> Option<&std::path::Path> {
1068        match &self.0 {
1069            ImageInner::EmbeddedImage {
1070                cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1071                ..
1072            } => Some(std::path::Path::new(path.as_str())),
1073            ImageInner::NineSlice(nine) => match &nine.0 {
1074                ImageInner::EmbeddedImage {
1075                    cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1076                    ..
1077                } => Some(std::path::Path::new(path.as_str())),
1078                _ => None,
1079            },
1080            _ => None,
1081        }
1082    }
1083}
1084
1085/// Like [`Image::to_rgba8`], but scalable sources (such as SVGs) are rasterized to
1086/// `target_size` (in physical pixels) instead of their intrinsic size.
1087/// Returns None if the pixels cannot be obtained.
1088pub fn image_to_rgba8_with_target_size(
1089    image: &Image,
1090    target_size: IntSize,
1091) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
1092    image.render_to_rgba8(Some(target_size.cast_unit()))
1093}
1094
1095#[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
1096/// Load an image from the decoded payload of a data URI.
1097/// This is called by the interpreter.
1098pub fn load_image_from_data_uri(
1099    uri: &str,
1100    bytes: &[u8],
1101    format: &str,
1102) -> Result<Image, LoadImageError> {
1103    // The browser loads images asynchronously, so every evaluation of the same data URI
1104    // must share one image and its loading state: cache under the URI.
1105    #[cfg(target_arch = "wasm32")]
1106    {
1107        self::cache::IMAGE_CACHE.with(|global_cache| {
1108            global_cache
1109                .borrow_mut()
1110                .load_image_from_data_uri(uri, bytes, format)
1111                .ok_or(LoadImageError(()))
1112        })
1113    }
1114    // Native decoding is synchronous; don't fill the cache with one-shot images.
1115    #[cfg(not(target_arch = "wasm32"))]
1116    {
1117        let _ = uri;
1118        ImageInner::load_from_data_with_cache_key(
1119            ImageCacheKey::Invalid,
1120            bytes.into(),
1121            format.as_bytes().into(),
1122        )
1123        .map(Image)
1124        .ok_or(Default::default())
1125    }
1126}
1127
1128/// Returns the MIME type for an image file extension, or None if the extension is unknown.
1129/// The extension is matched ASCII case-insensitively and given without the leading dot.
1130pub fn image_mime_type_from_extension(extension: &str) -> Option<&'static str> {
1131    for (ext, mime) in [
1132        ("png", "image/png"),
1133        ("jpg", "image/jpeg"),
1134        ("jpeg", "image/jpeg"),
1135        ("svg", "image/svg+xml"),
1136        ("svgz", "image/svg+xml"),
1137        ("gif", "image/gif"),
1138        ("webp", "image/webp"),
1139        ("bmp", "image/bmp"),
1140        ("ico", "image/x-icon"),
1141        ("avif", "image/avif"),
1142    ] {
1143        if extension.eq_ignore_ascii_case(ext) {
1144            return Some(mime);
1145        }
1146    }
1147    None
1148}
1149
1150/// This enum describes the origin to use when rendering a borrowed OpenGL texture.
1151/// Use this with [`BorrowedOpenGLTextureBuilder::origin`].
1152#[derive(Copy, Clone, Debug, PartialEq, Default)]
1153#[repr(u8)]
1154#[non_exhaustive]
1155pub enum BorrowedOpenGLTextureOrigin {
1156    /// The top-left of the texture is the top-left of the texture drawn on the screen.
1157    #[default]
1158    TopLeft,
1159    /// The bottom-left of the texture is the top-left of the texture draw on the screen,
1160    /// flipping it vertically.
1161    BottomLeft,
1162}
1163
1164/// Factory to create [`slint::Image`](crate::graphics::Image) from an existing OpenGL texture.
1165///
1166/// Methods can be chained on it in order to configure it.
1167///
1168///  * `origin`: Change the texture's origin when rendering (default: TopLeft).
1169///
1170/// Complete the builder by calling [`Self::build()`] to create a [`slint::Image`](crate::graphics::Image):
1171///
1172/// ```
1173/// # use i_slint_core::graphics::{BorrowedOpenGLTextureBuilder, Image, IntSize, BorrowedOpenGLTextureOrigin};
1174/// # let texture_id = core::num::NonZeroU32::new(1).unwrap();
1175/// # let size = IntSize::new(100, 100);
1176/// let builder = unsafe { BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size) }
1177///              .origin(BorrowedOpenGLTextureOrigin::TopLeft);
1178///
1179/// let image: slint::Image = builder.build();
1180/// ```
1181#[cfg(not(target_arch = "wasm32"))]
1182pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture);
1183
1184#[cfg(not(target_arch = "wasm32"))]
1185impl BorrowedOpenGLTextureBuilder {
1186    /// Generates the base configuration for a borrowed OpenGL texture.
1187    ///
1188    /// The texture must be bindable against the `GL_TEXTURE_2D` target, have `GL_RGBA` as format
1189    /// for the pixel data.
1190    ///
1191    /// By default, when Slint renders the texture, it assumes that the origin of the texture is at the top-left.
1192    /// This is different from the default OpenGL coordinate system. Use the `mirror_vertically` function
1193    /// to reconfigure this.
1194    ///
1195    /// # Safety
1196    ///
1197    /// This function is unsafe because invalid texture ids may lead to undefined behavior in OpenGL
1198    /// drivers. A valid texture id is one that was created by the same OpenGL context that is
1199    /// current during any of the invocations of the callback set on [`Window::set_rendering_notifier()`](crate::api::Window::set_rendering_notifier).
1200    /// OpenGL contexts between instances of [`slint::Window`](crate::api::Window) are not sharing resources. Consequently
1201    /// [`slint::Image`](Image) objects created from borrowed OpenGL textures cannot be shared between
1202    /// different windows.
1203    #[allow(unsafe_code)]
1204    pub unsafe fn new_gl_2d_rgba_texture(texture_id: core::num::NonZeroU32, size: IntSize) -> Self {
1205        Self(BorrowedOpenGLTexture { texture_id, size, origin: Default::default() })
1206    }
1207
1208    /// Configures the texture to be rendered vertically mirrored.
1209    pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self {
1210        self.0.origin = origin;
1211        self
1212    }
1213
1214    /// Completes the process of building a slint::Image that holds a borrowed OpenGL texture.
1215    pub fn build(self) -> Image {
1216        Image(ImageInner::BorrowedOpenGLTexture(self.0))
1217    }
1218}
1219
1220/// Load an image by handing a URL to an HTML `<img>` element for the browser
1221/// to fetch. This is a web-only mechanism used by slintpad to display image
1222/// references that are URLs rather than file-system paths; it is not general
1223/// network image loading.
1224/// This is called by the interpreter and the generated code.
1225#[cfg(all(target_arch = "wasm32", feature = "std"))]
1226pub fn load_as_html_image(url: &str) -> Result<Image, LoadImageError> {
1227    self::cache::IMAGE_CACHE.with(|global_cache| {
1228        global_cache.borrow_mut().load_as_html_image(url).ok_or(LoadImageError(()))
1229    })
1230}
1231
1232/// Load an image from an image embedded in the binary.
1233/// This is called by the generated code.
1234#[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
1235pub fn load_image_from_embedded_data(data: Slice<'static, u8>, format: Slice<'_, u8>) -> Image {
1236    self::cache::IMAGE_CACHE.with(|global_cache| {
1237        global_cache.borrow_mut().load_image_from_embedded_data(data, format).unwrap_or_default()
1238    })
1239}
1240
1241#[test]
1242fn test_image_size_from_buffer_without_backend() {
1243    {
1244        assert_eq!(Image::default().size(), Default::default());
1245        assert!(Image::default().to_rgb8().is_none());
1246        assert!(Image::default().to_rgba8().is_none());
1247        assert!(Image::default().to_rgba8_premultiplied().is_none());
1248    }
1249    {
1250        let buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
1251        let image = Image::from_rgb8(buffer.clone());
1252        assert_eq!(image.size(), [320, 200].into());
1253        assert_eq!(image.to_rgb8().as_ref().map(|b| b.as_slice()), Some(buffer.as_slice()));
1254    }
1255}
1256
1257#[cfg(feature = "svg")]
1258#[test]
1259// memchr's manually aligned SIMD loads are a false positive under -Zmiri-symbolic-alignment-check
1260#[cfg_attr(miri, ignore)]
1261fn test_image_size_from_svg() {
1262    let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
1263    let image = Image::load_from_svg_data(simple_svg.as_bytes()).unwrap();
1264    assert_eq!(image.size(), [320, 200].into());
1265    assert_eq!(image.to_rgba8().unwrap().size(), image.size());
1266}
1267
1268#[cfg(feature = "svg")]
1269#[test]
1270// memchr's manually aligned SIMD loads are a false positive under -Zmiri-symbolic-alignment-check
1271#[cfg_attr(miri, ignore)]
1272fn test_image_invalid_svg() {
1273    let invalid_svg = r#"AaBbCcDd"#;
1274    let result = Image::load_from_svg_data(invalid_svg.as_bytes());
1275    assert!(result.is_err());
1276}
1277
1278#[cfg(feature = "svg")]
1279#[test]
1280// memchr's manually aligned SIMD loads are a false positive under -Zmiri-symbolic-alignment-check
1281#[cfg_attr(miri, ignore)]
1282fn test_image_load_from_data_svg() {
1283    let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
1284    // The leading `<svg` tag lets the format be guessed.
1285    let guessed = Image::load_from_data(simple_svg.as_bytes(), None).unwrap();
1286    assert_eq!(guessed.size(), [320, 200].into());
1287    // An explicit format hint works too.
1288    let hinted = Image::load_from_data(simple_svg.as_bytes(), Some("svg")).unwrap();
1289    assert_eq!(hinted.size(), [320, 200].into());
1290}
1291
1292#[cfg(feature = "svg")]
1293#[test]
1294// memchr's manually aligned SIMD loads are a false positive under -Zmiri-symbolic-alignment-check
1295#[cfg_attr(miri, ignore)]
1296fn test_image_load_from_data_svgz() {
1297    // usvg gates gzip decompression behind its `svgz` feature, so a missing feature would
1298    // silently turn every compressed SVG into a load error. Gzip of the `simple_svg` above.
1299    const SIMPLE_SVGZ: &[u8] = &[
1300        0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0xb3, 0x29, 0x2e, 0x4b, 0x57,
1301        0x28, 0xcf, 0x4c, 0x29, 0xc9, 0xb0, 0x55, 0x32, 0x36, 0x32, 0x50, 0x52, 0xc8, 0x48, 0xcd,
1302        0x4c, 0xcf, 0x28, 0xb1, 0x55, 0x32, 0x32, 0x00, 0x72, 0x2a, 0x72, 0x73, 0xf2, 0x8a, 0x6d,
1303        0x95, 0x32, 0x4a, 0x4a, 0x0a, 0xac, 0xf4, 0xf5, 0xcb, 0xcb, 0xcb, 0xf5, 0xca, 0x8d, 0xf5,
1304        0xf2, 0x8b, 0xd2, 0xf5, 0x81, 0xb2, 0x06, 0xfa, 0x40, 0xad, 0x4a, 0x76, 0x36, 0x20, 0xca,
1305        0x0e, 0x00, 0x37, 0x91, 0x7a, 0xd6, 0x47, 0x00, 0x00, 0x00,
1306    ];
1307    let image = Image::load_from_data(SIMPLE_SVGZ, Some("svgz")).unwrap();
1308    assert_eq!(image.size(), [320, 200].into());
1309}
1310
1311#[cfg(feature = "image-decoders")]
1312#[test]
1313#[cfg_attr(miri, ignore)]
1314fn test_image_load_from_data_png() {
1315    let mut png = std::io::Cursor::new(std::vec::Vec::new());
1316    image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(2, 3, image::Rgb([0, 255, 0])))
1317        .write_to(&mut png, image::ImageFormat::Png)
1318        .unwrap();
1319    let png = png.into_inner();
1320
1321    // The PNG magic bytes let the format be guessed.
1322    let guessed = Image::load_from_data(&png, None).unwrap();
1323    assert_eq!(guessed.size(), [2, 3].into());
1324    // An explicit format hint works too.
1325    let hinted = Image::load_from_data(&png, Some("png")).unwrap();
1326    assert_eq!(hinted.size(), [2, 3].into());
1327
1328    assert!(Image::load_from_data(b"not an image", None).is_err());
1329}
1330
1331/// The result of the fit function
1332#[derive(Debug)]
1333pub struct FitResult {
1334    /// The clip rect in the source image (in source image coordinate)
1335    pub clip_rect: IntRect,
1336    /// The scale to apply to go from the source to the target horizontally
1337    pub source_to_target_x: f32,
1338    /// The scale to apply to go from the source to the target vertically
1339    pub source_to_target_y: f32,
1340    /// The size of the target
1341    pub size: euclid::Size2D<f32, PhysicalPx>,
1342    /// The offset in the target in which we draw the image
1343    pub offset: euclid::Point2D<f32, PhysicalPx>,
1344    /// When Some, it means the image should be tiled instead of stretched to the target
1345    /// but still scaled with the source_to_target_x and source_to_target_y factor
1346    /// The point is the coordinate within the image's clip_rect of the pixel at the offset
1347    pub tiled: Option<euclid::default::Point2D<u32>>,
1348}
1349
1350impl FitResult {
1351    fn adjust_for_tiling(
1352        self,
1353        ratio: f32,
1354        alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1355        tiling: (ImageTiling, ImageTiling),
1356    ) -> Self {
1357        let mut r = self;
1358        let mut tiled = euclid::Point2D::default();
1359        let target = r.size;
1360        let o = r.clip_rect.size.cast::<f32>();
1361        match tiling.0 {
1362            ImageTiling::None => {
1363                r.size.width = o.width * r.source_to_target_x;
1364                if (o.width as f32) > target.width / r.source_to_target_x {
1365                    let diff = (o.width as f32 - target.width / r.source_to_target_x) as i32;
1366                    r.clip_rect.size.width -= diff;
1367                    r.clip_rect.origin.x += match alignment.0 {
1368                        ImageHorizontalAlignment::Center => diff / 2,
1369                        ImageHorizontalAlignment::Left => 0,
1370                        ImageHorizontalAlignment::Right => diff,
1371                    };
1372                    r.size.width = target.width;
1373                } else if (o.width as f32) < target.width / r.source_to_target_x {
1374                    r.offset.x += match alignment.0 {
1375                        ImageHorizontalAlignment::Center => {
1376                            (target.width - o.width as f32 * r.source_to_target_x) / 2.
1377                        }
1378                        ImageHorizontalAlignment::Left => 0.,
1379                        ImageHorizontalAlignment::Right => {
1380                            target.width - o.width as f32 * r.source_to_target_x
1381                        }
1382                    };
1383                }
1384            }
1385            ImageTiling::Repeat => {
1386                tiled.x = match alignment.0 {
1387                    ImageHorizontalAlignment::Left => 0,
1388                    ImageHorizontalAlignment::Center => {
1389                        ((o.width - target.width / ratio) / 2.).rem_euclid(o.width) as u32
1390                    }
1391                    ImageHorizontalAlignment::Right => {
1392                        (-target.width / ratio).rem_euclid(o.width) as u32
1393                    }
1394                };
1395                r.source_to_target_x = ratio;
1396            }
1397            ImageTiling::Round => {
1398                if target.width / ratio <= o.width * 1.5 {
1399                    r.source_to_target_x = target.width / o.width;
1400                } else {
1401                    let mut rem = (target.width / ratio).rem_euclid(o.width);
1402                    if rem > o.width / 2. {
1403                        rem -= o.width;
1404                    }
1405                    r.source_to_target_x = ratio * target.width / (target.width - rem * ratio);
1406                }
1407            }
1408        }
1409
1410        match tiling.1 {
1411            ImageTiling::None => {
1412                r.size.height = o.height * r.source_to_target_y;
1413                if (o.height as f32) > target.height / r.source_to_target_y {
1414                    let diff = (o.height as f32 - target.height / r.source_to_target_y) as i32;
1415                    r.clip_rect.size.height -= diff;
1416                    r.clip_rect.origin.y += match alignment.1 {
1417                        ImageVerticalAlignment::Center => diff / 2,
1418                        ImageVerticalAlignment::Top => 0,
1419                        ImageVerticalAlignment::Bottom => diff,
1420                    };
1421                    r.size.height = target.height;
1422                } else if (o.height as f32) < target.height / r.source_to_target_y {
1423                    r.offset.y += match alignment.1 {
1424                        ImageVerticalAlignment::Center => {
1425                            (target.height - o.height as f32 * r.source_to_target_y) / 2.
1426                        }
1427                        ImageVerticalAlignment::Top => 0.,
1428                        ImageVerticalAlignment::Bottom => {
1429                            target.height - o.height as f32 * r.source_to_target_y
1430                        }
1431                    };
1432                }
1433            }
1434            ImageTiling::Repeat => {
1435                tiled.y = match alignment.1 {
1436                    ImageVerticalAlignment::Top => 0,
1437                    ImageVerticalAlignment::Center => {
1438                        ((o.height - target.height / ratio) / 2.).rem_euclid(o.height) as u32
1439                    }
1440                    ImageVerticalAlignment::Bottom => {
1441                        (-target.height / ratio).rem_euclid(o.height) as u32
1442                    }
1443                };
1444                r.source_to_target_y = ratio;
1445            }
1446            ImageTiling::Round => {
1447                if target.height / ratio <= o.height * 1.5 {
1448                    r.source_to_target_y = target.height / o.height;
1449                } else {
1450                    let mut rem = (target.height / ratio).rem_euclid(o.height);
1451                    if rem > o.height / 2. {
1452                        rem -= o.height;
1453                    }
1454                    r.source_to_target_y = ratio * target.height / (target.height - rem * ratio);
1455                }
1456            }
1457        }
1458        let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1459        r.tiled = has_tiling.then_some(tiled);
1460        r
1461    }
1462}
1463
1464#[cfg(not(feature = "std"))]
1465trait RemEuclid {
1466    fn rem_euclid(self, b: f32) -> f32;
1467}
1468#[cfg(not(feature = "std"))]
1469impl RemEuclid for f32 {
1470    fn rem_euclid(self, b: f32) -> f32 {
1471        num_traits::Euclid::rem_euclid(&self, &b)
1472    }
1473}
1474
1475/// Return an FitResult that can be used to render an image in a buffer that matches a given ImageFit
1476pub fn fit(
1477    image_fit: ImageFit,
1478    target: euclid::Size2D<f32, PhysicalPx>,
1479    source_rect: IntRect,
1480    scale_factor: ScaleFactor,
1481    alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1482    tiling: (ImageTiling, ImageTiling),
1483) -> FitResult {
1484    let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1485    let o = source_rect.size.cast::<f32>();
1486    let ratio = match image_fit {
1487        // If there is any tiling, we ignore image_fit
1488        _ if has_tiling => scale_factor.get(),
1489        ImageFit::Fill => {
1490            return FitResult {
1491                clip_rect: source_rect,
1492                source_to_target_x: target.width / o.width,
1493                source_to_target_y: target.height / o.height,
1494                size: target,
1495                offset: Default::default(),
1496                tiled: None,
1497            };
1498        }
1499        ImageFit::Preserve => scale_factor.get(),
1500        ImageFit::Contain => f32::min(target.width / o.width, target.height / o.height),
1501        ImageFit::Cover => f32::max(target.width / o.width, target.height / o.height),
1502    };
1503
1504    FitResult {
1505        clip_rect: source_rect,
1506        source_to_target_x: ratio,
1507        source_to_target_y: ratio,
1508        size: target,
1509        offset: euclid::Point2D::default(),
1510        tiled: None,
1511    }
1512    .adjust_for_tiling(ratio, alignment, tiling)
1513}
1514
1515/// The pixel size a scalable image (an SVG) needs to be rasterized at to fill `target`
1516/// under `image_fit`, i.e. its intrinsic size scaled by what [`fit`] would scale it by.
1517///
1518/// Returns `None` when the source has no area to scale from, or when the fitted size
1519/// collapses to nothing.
1520pub fn scalable_render_size(
1521    source_size: IntSize,
1522    image_fit: ImageFit,
1523    target: euclid::Size2D<f32, PhysicalPx>,
1524    scale_factor: ScaleFactor,
1525    tiling: (ImageTiling, ImageTiling),
1526) -> Option<euclid::Size2D<u32, PhysicalPx>> {
1527    let source = source_size.cast::<f32>();
1528    if source.is_empty() {
1529        return None;
1530    }
1531    let fit = fit(
1532        image_fit,
1533        target,
1534        IntRect::from_size(source_size.cast()),
1535        scale_factor,
1536        // Only the size is of interest here, so the alignment doesn't matter.
1537        Default::default(),
1538        tiling,
1539    );
1540    let size = euclid::size2(
1541        (source.width * fit.source_to_target_x) as u32,
1542        (source.height * fit.source_to_target_y) as u32,
1543    );
1544    (!size.is_empty()).then_some(size)
1545}
1546
1547/// Generate an iterator of  [`FitResult`] for each slice of a nine-slice border image
1548pub fn fit9slice(
1549    source_rect: IntSize,
1550    [t, r, b, l]: [u16; 4],
1551    target: euclid::Size2D<f32, PhysicalPx>,
1552    scale_factor: ScaleFactor,
1553    alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1554    tiling: (ImageTiling, ImageTiling),
1555) -> impl Iterator<Item = FitResult> {
1556    let fit_to = |clip_rect: euclid::default::Rect<u16>, target: euclid::Rect<f32, PhysicalPx>| {
1557        (!clip_rect.is_empty() && !target.is_empty()).then(|| {
1558            FitResult {
1559                clip_rect: clip_rect.cast(),
1560                source_to_target_x: target.width() / clip_rect.width() as f32,
1561                source_to_target_y: target.height() / clip_rect.height() as f32,
1562                size: target.size,
1563                offset: target.origin,
1564                tiled: None,
1565            }
1566            .adjust_for_tiling(scale_factor.get(), alignment, tiling)
1567        })
1568    };
1569    use euclid::rect;
1570    let sf = |x| scale_factor.get() * x as f32;
1571    let source = source_rect.cast::<u16>();
1572    if t + b > source.height || l + r > source.width {
1573        [None, None, None, None, None, None, None, None, None]
1574    } else {
1575        [
1576            fit_to(rect(0, 0, l, t), rect(0., 0., sf(l), sf(t))),
1577            fit_to(
1578                rect(l, 0, source.width - l - r, t),
1579                rect(sf(l), 0., target.width - sf(l) - sf(r), sf(t)),
1580            ),
1581            fit_to(rect(source.width - r, 0, r, t), rect(target.width - sf(r), 0., sf(r), sf(t))),
1582            fit_to(
1583                rect(0, t, l, source.height - t - b),
1584                rect(0., sf(t), sf(l), target.height - sf(t) - sf(b)),
1585            ),
1586            fit_to(
1587                rect(l, t, source.width - l - r, source.height - t - b),
1588                rect(sf(l), sf(t), target.width - sf(l) - sf(r), target.height - sf(t) - sf(b)),
1589            ),
1590            fit_to(
1591                rect(source.width - r, t, r, source.height - t - b),
1592                rect(target.width - sf(r), sf(t), sf(r), target.height - sf(t) - sf(b)),
1593            ),
1594            fit_to(rect(0, source.height - b, l, b), rect(0., target.height - sf(b), sf(l), sf(b))),
1595            fit_to(
1596                rect(l, source.height - b, source.width - l - r, b),
1597                rect(sf(l), target.height - sf(b), target.width - sf(l) - sf(r), sf(b)),
1598            ),
1599            fit_to(
1600                rect(source.width - r, source.height - b, r, b),
1601                rect(target.width - sf(r), target.height - sf(b), sf(r), sf(b)),
1602            ),
1603        ]
1604    }
1605    .into_iter()
1606    .flatten()
1607}
1608
1609#[cfg(feature = "ffi")]
1610pub(crate) mod ffi {
1611    #![allow(unsafe_code)]
1612
1613    use super::*;
1614
1615    // Expand Rgb8Pixel so that cbindgen can see it. (is in fact rgb::RGB<u8>)
1616    /// Represents an RGB pixel.
1617    #[cfg(cbindgen)]
1618    #[repr(C)]
1619    struct Rgb8Pixel {
1620        /// red value (between 0 and 255)
1621        r: u8,
1622        /// green value (between 0 and 255)
1623        g: u8,
1624        /// blue value (between 0 and 255)
1625        b: u8,
1626    }
1627
1628    // Expand Rgba8Pixel so that cbindgen can see it. (is in fact rgb::RGBA<u8>)
1629    /// Represents an RGBA pixel.
1630    #[cfg(cbindgen)]
1631    #[repr(C)]
1632    struct Rgba8Pixel {
1633        /// red value (between 0 and 255)
1634        r: u8,
1635        /// green value (between 0 and 255)
1636        g: u8,
1637        /// blue value (between 0 and 255)
1638        b: u8,
1639        /// alpha value (between 0 and 255)
1640        a: u8,
1641    }
1642
1643    // Keep the cfg free of target_arch: cbindgen maps target_arch = wasm32 to a C macro and
1644    // would guard the declaration, but the C++ API is native only and relies on it being there.
1645    #[cfg(all(feature = "std", feature = "image-decoders"))]
1646    #[unsafe(no_mangle)]
1647    pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) {
1648        unsafe {
1649            core::ptr::write(
1650                image,
1651                Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or_default(),
1652            )
1653        }
1654    }
1655
1656    #[cfg(all(feature = "std", feature = "image-decoders"))]
1657    #[unsafe(no_mangle)]
1658    pub unsafe extern "C" fn slint_image_load_from_embedded_data(
1659        data: Slice<'static, u8>,
1660        format: Slice<'static, u8>,
1661        image: *mut Image,
1662    ) {
1663        unsafe { core::ptr::write(image, super::load_image_from_embedded_data(data, format)) };
1664    }
1665
1666    /// Unlike `slint_image_load_from_embedded_data`, this does not go through the image cache,
1667    /// so `data` does not have to be `'static`.
1668    #[cfg(all(feature = "std", feature = "image-decoders"))]
1669    #[unsafe(no_mangle)]
1670    pub unsafe extern "C" fn slint_image_load_from_data(
1671        data: Slice<'_, u8>,
1672        format: Slice<'_, u8>,
1673        image: *mut Image,
1674    ) {
1675        let format = core::str::from_utf8(format.as_slice()).ok();
1676        let loaded = super::Image::load_from_data(data.as_slice(), format).unwrap_or_default();
1677        unsafe { core::ptr::write(image, loaded) };
1678    }
1679
1680    #[unsafe(no_mangle)]
1681    pub extern "C" fn slint_image_size(image: &Image) -> IntSize {
1682        image.size()
1683    }
1684
1685    #[unsafe(no_mangle)]
1686    pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
1687        match &image.0 {
1688            #[cfg(feature = "std")]
1689            ImageInner::EmbeddedImage {
1690                cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1691                ..
1692            } => Some(path),
1693            ImageInner::NineSlice(nine) => match &nine.0 {
1694                #[cfg(feature = "std")]
1695                ImageInner::EmbeddedImage {
1696                    cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1697                    ..
1698                } => Some(path),
1699                _ => None,
1700            },
1701            _ => None,
1702        }
1703    }
1704
1705    #[unsafe(no_mangle)]
1706    pub unsafe extern "C" fn slint_image_from_embedded_textures(
1707        textures: &'static StaticTextures,
1708        image: *mut Image,
1709    ) {
1710        unsafe { core::ptr::write(image, Image::from(ImageInner::StaticTextures(textures))) };
1711    }
1712
1713    #[unsafe(no_mangle)]
1714    pub extern "C" fn slint_image_compare_equal(image1: &Image, image2: &Image) -> bool {
1715        image1.eq(image2)
1716    }
1717
1718    /// Call [`Image::set_nine_slice_edges`]
1719    #[unsafe(no_mangle)]
1720    pub extern "C" fn slint_image_set_nine_slice_edges(
1721        image: &mut Image,
1722        top: u16,
1723        right: u16,
1724        bottom: u16,
1725        left: u16,
1726    ) {
1727        image.set_nine_slice_edges(top, right, bottom, left);
1728    }
1729
1730    #[unsafe(no_mangle)]
1731    pub extern "C" fn slint_image_to_rgb8(
1732        image: &Image,
1733        data: &mut SharedVector<Rgb8Pixel>,
1734        width: &mut u32,
1735        height: &mut u32,
1736    ) -> bool {
1737        image.to_rgb8().is_some_and(|pixel_buffer| {
1738            *data = pixel_buffer.data.clone();
1739            *width = pixel_buffer.width();
1740            *height = pixel_buffer.height();
1741            true
1742        })
1743    }
1744
1745    #[unsafe(no_mangle)]
1746    pub extern "C" fn slint_image_to_rgba8(
1747        image: &Image,
1748        data: &mut SharedVector<Rgba8Pixel>,
1749        width: &mut u32,
1750        height: &mut u32,
1751    ) -> bool {
1752        image.to_rgba8().is_some_and(|pixel_buffer| {
1753            *data = pixel_buffer.data.clone();
1754            *width = pixel_buffer.width();
1755            *height = pixel_buffer.height();
1756            true
1757        })
1758    }
1759
1760    #[unsafe(no_mangle)]
1761    pub extern "C" fn slint_image_to_rgba8_premultiplied(
1762        image: &Image,
1763        data: &mut SharedVector<Rgba8Pixel>,
1764        width: &mut u32,
1765        height: &mut u32,
1766    ) -> bool {
1767        image.to_rgba8_premultiplied().is_some_and(|pixel_buffer| {
1768            *data = pixel_buffer.data.clone();
1769            *width = pixel_buffer.width();
1770            *height = pixel_buffer.height();
1771            true
1772        })
1773    }
1774}
1775
1776/// This structure contains fields to identify and render an OpenGL texture that Slint borrows from the application code.
1777/// Use this to embed a native OpenGL texture into a Slint scene.
1778///
1779/// The ownership of the texture remains with the application. It is the application's responsibility to delete the texture
1780/// when it is not used anymore.
1781///
1782/// Note that only 2D RGBA textures are supported.
1783#[derive(Clone, Debug, PartialEq)]
1784#[non_exhaustive]
1785#[cfg(not(target_arch = "wasm32"))]
1786#[repr(C)]
1787pub struct BorrowedOpenGLTexture {
1788    /// The id or name of the texture, as created by [`glGenTextures`](https://registry.khronos.org/OpenGL-Refpages/gl4/html/glGenTextures.xhtml).
1789    pub texture_id: core::num::NonZeroU32,
1790    /// The size of the texture in pixels.
1791    pub size: IntSize,
1792    /// Origin of the texture when rendering.
1793    pub origin: BorrowedOpenGLTextureOrigin,
1794}
1795
1796#[cfg(test)]
1797mod tests {
1798    use crate::graphics::Rgba8Pixel;
1799
1800    use super::Image;
1801
1802    #[test]
1803    fn test_premultiplied_to_rgb_zero_alpha() {
1804        let pixel = Rgba8Pixel::new(5, 10, 15, 0);
1805        let converted = Image::premultiplied_rgba_to_rgba(pixel);
1806        assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1807    }
1808
1809    #[test]
1810    fn test_premultiplied_to_rgb_full_alpha() {
1811        let pixel = Rgba8Pixel::new(5, 10, 15, 255);
1812        let converted = Image::premultiplied_rgba_to_rgba(pixel);
1813        assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 255));
1814    }
1815
1816    #[test]
1817    fn test_premultiplied_to_rgb() {
1818        let pixel = Rgba8Pixel::new(5, 10, 15, 128);
1819        let converted = Image::premultiplied_rgba_to_rgba(pixel);
1820        assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 128));
1821    }
1822
1823    #[test]
1824    fn test_rgb_to_premultiplied_zero_alpha() {
1825        let pixel = Rgba8Pixel::new(10, 20, 30, 0);
1826        let converted = Image::rgba_to_premultiplied_rgba(pixel);
1827        assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1828    }
1829
1830    #[test]
1831    fn test_rgb_to_premultiplied_full_alpha() {
1832        let pixel = Rgba8Pixel::new(10, 20, 30, 255);
1833        let converted = Image::rgba_to_premultiplied_rgba(pixel);
1834        assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 255));
1835    }
1836
1837    #[test]
1838    fn test_rgb_to_premultiplied() {
1839        let pixel = Rgba8Pixel::new(10, 20, 30, 128);
1840        let converted = Image::rgba_to_premultiplied_rgba(pixel);
1841        assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 128));
1842    }
1843}