Skip to main content

i_slint_core/graphics/
image.rs

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