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