1use 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 size: extern "C" fn(VRef<OpaqueImageVTable>) -> IntSize,
32 cache_key: extern "C" fn(VRef<OpaqueImageVTable>) -> ImageCacheKey,
34}
35
36#[cfg(feature = "svg")]
37OpaqueImageVTable_static! {
38 pub static PARSED_SVG_VT for svg::ParsedSVG
40}
41
42#[cfg(target_arch = "wasm32")]
43OpaqueImageVTable_static! {
44 pub static HTML_IMAGE_VT for htmlimage::HTMLImage
46}
47
48OpaqueImageVTable_static! {
49 pub static NINE_SLICE_VT for NineSliceImage
51}
52
53#[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 pub fn width(&self) -> u32 {
73 self.width
74 }
75
76 pub fn height(&self) -> u32 {
78 self.height
79 }
80
81 pub fn size(&self) -> IntSize {
83 [self.width, self.height].into()
84 }
85}
86
87impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
88 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 pub fn as_bytes(&self) -> &[u8] {
100 use rgb::ComponentBytes;
101 self.data.as_slice().as_bytes()
102 }
103
104 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 pub fn as_slice(&self) -> &[Pixel] {
114 self.data.as_slice()
115 }
116}
117
118impl<Pixel: Clone + Default> SharedPixelBuffer<Pixel> {
119 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 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
148pub type Rgb8Pixel = rgb::RGB8;
151pub type Rgba8Pixel = rgb::RGBA8;
154
155#[derive(Clone, Debug)]
160#[repr(C)]
161pub enum SharedImageBuffer {
163 RGB8(SharedPixelBuffer<Rgb8Pixel>),
166 RGBA8(SharedPixelBuffer<Rgba8Pixel>),
169 RGBA8Premultiplied(SharedPixelBuffer<Rgba8Pixel>),
176}
177
178impl SharedImageBuffer {
179 #[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 #[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 #[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)]
228pub enum TexturePixelFormat {
230 Rgb,
232 Rgba,
234 RgbaPremultiplied,
236 AlphaMap,
238 SignedDistanceField,
243}
244
245impl TexturePixelFormat {
246 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)]
260pub struct StaticTexture {
262 pub rect: IntRect,
264 pub format: TexturePixelFormat,
266 pub color: crate::Color,
268 pub index: usize,
270}
271
272#[repr(C)]
274#[derive(Clone, PartialEq, Debug)]
275pub struct StaticTextures {
276 pub size: IntSize,
279 pub original_size: IntSize,
281 pub data: Slice<'static, u8>,
283 pub textures: Slice<'static, StaticTexture>,
285}
286
287#[derive(PartialEq, Eq, Debug, Hash, Clone)]
290#[repr(C)]
291#[cfg(any(feature = "std", feature = "ffi"))]
292pub struct CachedPath {
293 path: SharedString,
294 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#[derive(PartialEq, Eq, Debug, Hash, Clone)]
315#[repr(u8)]
316pub enum ImageCacheKey {
317 Invalid = 0,
320 #[cfg(any(feature = "std", feature = "ffi"))]
321 Path(CachedPath) = 1,
323 #[cfg(target_arch = "wasm32")]
325 URL(SharedString) = 2,
326 EmbeddedData(usize) = 3,
328}
329
330impl ImageCacheKey {
331 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 pub fn from_embedded_image_data(data: &'static [u8]) -> Self {
356 Self::EmbeddedData(data.as_ptr() as usize)
357 }
358}
359
360pub struct NineSliceImage(pub ImageInner, pub [u16; 4]);
362
363impl NineSliceImage {
364 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#[cfg(any(feature = "unstable-wgpu-27", feature = "unstable-wgpu-28"))]
381#[derive(Clone, Debug)]
382pub enum WGPUTexture {
383 #[cfg(feature = "unstable-wgpu-27")]
385 WGPU27Texture(wgpu_27::Texture),
386 #[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#[derive(Clone, Debug, Default)]
417#[repr(u8)]
418#[allow(missing_docs)]
419pub enum ImageInner {
420 #[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 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 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 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 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#[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#[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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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 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 pub fn size(&self) -> IntSize {
910 self.0.size()
911 }
912
913 #[cfg(feature = "std")]
914 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#[derive(Copy, Clone, Debug, PartialEq, Default)]
946#[repr(u8)]
947#[non_exhaustive]
948pub enum BorrowedOpenGLTextureOrigin {
949 #[default]
951 TopLeft,
952 BottomLeft,
955}
956
957#[cfg(not(target_arch = "wasm32"))]
975pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture);
976
977#[cfg(not(target_arch = "wasm32"))]
978impl BorrowedOpenGLTextureBuilder {
979 #[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 pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self {
1003 self.0.origin = origin;
1004 self
1005 }
1006
1007 pub fn build(self) -> Image {
1009 Image(ImageInner::BorrowedOpenGLTexture(self.0))
1010 }
1011}
1012
1013#[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#[derive(Debug)]
1057pub struct FitResult {
1058 pub clip_rect: IntRect,
1060 pub source_to_target_x: f32,
1062 pub source_to_target_y: f32,
1064 pub size: euclid::Size2D<f32, PhysicalPx>,
1066 pub offset: euclid::Point2D<f32, PhysicalPx>,
1068 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
1199pub 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 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
1239pub 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 #[cfg(cbindgen)]
1310 #[repr(C)]
1311 struct Rgb8Pixel {
1312 r: u8,
1314 g: u8,
1316 b: u8,
1318 }
1319
1320 #[cfg(cbindgen)]
1323 #[repr(C)]
1324 struct Rgba8Pixel {
1325 r: u8,
1327 g: u8,
1329 b: u8,
1331 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 #[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#[derive(Clone, Debug, PartialEq)]
1461#[non_exhaustive]
1462#[cfg(not(target_arch = "wasm32"))]
1463#[repr(C)]
1464pub struct BorrowedOpenGLTexture {
1465 pub texture_id: core::num::NonZeroU32,
1467 pub size: IntSize,
1469 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}