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