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