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