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(Pixel::default())
126 .take(width as usize * height as usize)
127 .collect(),
128 }
129 }
130}
131
132impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
133 pub fn clone_from_slice<SourcePixelType>(
137 pixel_slice: &[SourcePixelType],
138 width: u32,
139 height: u32,
140 ) -> Self
141 where
142 [SourcePixelType]: rgb::AsPixels<Pixel>,
143 {
144 use rgb::AsPixels;
145 Self { width, height, data: pixel_slice.as_pixels().into() }
146 }
147}
148
149pub type Rgb8Pixel = rgb::RGB8;
152pub type Rgba8Pixel = rgb::RGBA8;
155
156#[derive(Clone, Debug)]
161#[repr(C)]
162pub enum SharedImageBuffer {
164 RGB8(SharedPixelBuffer<Rgb8Pixel>),
167 RGBA8(SharedPixelBuffer<Rgba8Pixel>),
170 RGBA8Premultiplied(SharedPixelBuffer<Rgba8Pixel>),
177}
178
179impl SharedImageBuffer {
180 #[inline]
182 pub fn width(&self) -> u32 {
183 match self {
184 Self::RGB8(buffer) => buffer.width(),
185 Self::RGBA8(buffer) => buffer.width(),
186 Self::RGBA8Premultiplied(buffer) => buffer.width(),
187 }
188 }
189
190 #[inline]
192 pub fn height(&self) -> u32 {
193 match self {
194 Self::RGB8(buffer) => buffer.height(),
195 Self::RGBA8(buffer) => buffer.height(),
196 Self::RGBA8Premultiplied(buffer) => buffer.height(),
197 }
198 }
199
200 #[inline]
202 pub fn size(&self) -> IntSize {
203 match self {
204 Self::RGB8(buffer) => buffer.size(),
205 Self::RGBA8(buffer) => buffer.size(),
206 Self::RGBA8Premultiplied(buffer) => buffer.size(),
207 }
208 }
209}
210
211impl PartialEq for SharedImageBuffer {
212 fn eq(&self, other: &Self) -> bool {
213 match self {
214 Self::RGB8(lhs_buffer) => {
215 matches!(other, Self::RGB8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
216 }
217 Self::RGBA8(lhs_buffer) => {
218 matches!(other, Self::RGBA8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
219 }
220 Self::RGBA8Premultiplied(lhs_buffer) => {
221 matches!(other, Self::RGBA8Premultiplied(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
222 }
223 }
224 }
225}
226
227#[repr(u8)]
228#[derive(Clone, PartialEq, Debug, Copy)]
229pub enum TexturePixelFormat {
231 Rgb,
233 Rgba,
235 RgbaPremultiplied,
237 AlphaMap,
239 SignedDistanceField,
244}
245
246impl TexturePixelFormat {
247 pub fn bpp(self) -> usize {
249 match self {
250 TexturePixelFormat::Rgb => 3,
251 TexturePixelFormat::Rgba => 4,
252 TexturePixelFormat::RgbaPremultiplied => 4,
253 TexturePixelFormat::AlphaMap => 1,
254 TexturePixelFormat::SignedDistanceField => 1,
255 }
256 }
257}
258
259#[repr(C)]
260#[derive(Clone, PartialEq, Debug)]
261pub struct StaticTexture {
263 pub rect: IntRect,
265 pub format: TexturePixelFormat,
267 pub color: crate::Color,
269 pub index: usize,
271}
272
273#[repr(C)]
275#[derive(Clone, PartialEq, Debug)]
276pub struct StaticTextures {
277 pub size: IntSize,
280 pub original_size: IntSize,
282 pub data: Slice<'static, u8>,
284 pub textures: Slice<'static, StaticTexture>,
286}
287
288#[derive(PartialEq, Eq, Debug, Hash, Clone)]
291#[repr(C)]
292#[cfg(feature = "std")]
293pub struct CachedPath {
294 path: SharedString,
295 last_modified: u32,
297}
298
299#[cfg(feature = "std")]
300impl CachedPath {
301 fn new<P: AsRef<std::path::Path>>(path: P) -> Self {
302 let path_str = path.as_ref().to_string_lossy().as_ref().into();
303 let timestamp = std::fs::metadata(path)
304 .and_then(|md| md.modified())
305 .unwrap_or(std::time::UNIX_EPOCH)
306 .duration_since(std::time::UNIX_EPOCH)
307 .map(|t| t.as_secs() as u32)
308 .unwrap_or_default();
309 Self { path: path_str, last_modified: timestamp }
310 }
311}
312
313#[derive(PartialEq, Eq, Debug, Hash, Clone)]
316#[repr(u8)]
317pub enum ImageCacheKey {
318 Invalid = 0,
321 #[cfg(feature = "std")]
322 Path(CachedPath) = 1,
324 #[cfg(target_arch = "wasm32")]
326 URL(SharedString) = 2,
327 EmbeddedData(usize) = 3,
329}
330
331impl ImageCacheKey {
332 pub fn new(resource: &ImageInner) -> Option<Self> {
335 let key = match resource {
336 ImageInner::None => return None,
337 ImageInner::EmbeddedImage { cache_key, .. } => cache_key.clone(),
338 ImageInner::StaticTextures(textures) => {
339 Self::from_embedded_image_data(textures.data.as_slice())
340 }
341 #[cfg(feature = "svg")]
342 ImageInner::Svg(parsed_svg) => parsed_svg.cache_key(),
343 #[cfg(target_arch = "wasm32")]
344 ImageInner::HTMLImage(htmlimage) => Self::URL(htmlimage.source().into()),
345 ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).cache_key(),
346 #[cfg(not(target_arch = "wasm32"))]
347 ImageInner::BorrowedOpenGLTexture(..) => return None,
348 ImageInner::NineSlice(nine) => vtable::VRc::borrow(nine).cache_key(),
349 #[cfg(any(feature = "unstable-wgpu-26", feature = "unstable-wgpu-27"))]
350 ImageInner::WGPUTexture(..) => return None,
351 };
352 if matches!(key, ImageCacheKey::Invalid) {
353 None
354 } else {
355 Some(key)
356 }
357 }
358
359 pub fn from_embedded_image_data(data: &'static [u8]) -> Self {
361 Self::EmbeddedData(data.as_ptr() as usize)
362 }
363}
364
365pub struct NineSliceImage(pub ImageInner, pub [u16; 4]);
367
368impl NineSliceImage {
369 pub fn image(&self) -> Image {
371 Image(self.0.clone())
372 }
373}
374
375impl OpaqueImage for NineSliceImage {
376 fn size(&self) -> IntSize {
377 self.0.size()
378 }
379 fn cache_key(&self) -> ImageCacheKey {
380 ImageCacheKey::new(&self.0).unwrap_or(ImageCacheKey::Invalid)
381 }
382}
383
384#[cfg(any(feature = "unstable-wgpu-26", feature = "unstable-wgpu-27"))]
386#[derive(Clone, Debug)]
387pub enum WGPUTexture {
388 #[cfg(feature = "unstable-wgpu-26")]
390 WGPU26Texture(wgpu_26::Texture),
391 #[cfg(feature = "unstable-wgpu-27")]
393 WGPU27Texture(wgpu_27::Texture),
394}
395
396#[cfg(any(feature = "unstable-wgpu-26", feature = "unstable-wgpu-27"))]
397impl OpaqueImage for WGPUTexture {
398 fn size(&self) -> IntSize {
399 match self {
400 #[cfg(feature = "unstable-wgpu-26")]
401 Self::WGPU26Texture(texture) => {
402 let size = texture.size();
403 (size.width, size.height).into()
404 }
405 #[cfg(feature = "unstable-wgpu-27")]
406 Self::WGPU27Texture(texture) => {
407 let size = texture.size();
408 (size.width, size.height).into()
409 }
410 }
411 }
412 fn cache_key(&self) -> ImageCacheKey {
413 ImageCacheKey::Invalid
414 }
415}
416
417#[derive(Clone, Debug, Default)]
422#[repr(u8)]
423#[allow(missing_docs)]
424pub enum ImageInner {
425 #[default]
427 None = 0,
428 EmbeddedImage {
429 cache_key: ImageCacheKey,
430 buffer: SharedImageBuffer,
431 } = 1,
432 #[cfg(feature = "svg")]
433 Svg(vtable::VRc<OpaqueImageVTable, svg::ParsedSVG>) = 2,
434 StaticTextures(&'static StaticTextures) = 3,
435 #[cfg(target_arch = "wasm32")]
436 HTMLImage(vtable::VRc<OpaqueImageVTable, htmlimage::HTMLImage>) = 4,
437 BackendStorage(vtable::VRc<OpaqueImageVTable>) = 5,
438 #[cfg(not(target_arch = "wasm32"))]
439 BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6,
440 NineSlice(vtable::VRc<OpaqueImageVTable, NineSliceImage>) = 7,
441 #[cfg(any(feature = "unstable-wgpu-26", feature = "unstable-wgpu-27"))]
442 WGPUTexture(WGPUTexture) = 8,
443}
444
445impl ImageInner {
446 pub fn render_to_buffer(
453 &self,
454 _target_size_for_scalable_source: Option<euclid::Size2D<u32, PhysicalPx>>,
455 ) -> Option<SharedImageBuffer> {
456 match self {
457 ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()),
458 #[cfg(feature = "svg")]
459 ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) {
460 Ok(b) => Some(b),
461 Err(resvg::usvg::Error::InvalidSize) => None,
463 Err(err) => {
464 std::eprintln!("Error rendering SVG: {err}");
465 None
466 }
467 },
468 ImageInner::StaticTextures(ts) => {
469 let mut buffer =
470 SharedPixelBuffer::<Rgba8Pixel>::new(ts.size.width, ts.size.height);
471 let stride = buffer.width() as usize;
472 let slice = buffer.make_mut_slice();
473 for t in ts.textures.iter() {
474 let rect = t.rect.to_usize();
475 for y in 0..rect.height() {
476 let slice = &mut slice[(rect.min_y() + y) * stride..][rect.x_range()];
477 let source = &ts.data[t.index + y * rect.width() * t.format.bpp()..];
478 match t.format {
479 TexturePixelFormat::Rgb => {
480 let mut iter = source.chunks_exact(3).map(|p| Rgba8Pixel {
481 r: p[0],
482 g: p[1],
483 b: p[2],
484 a: 255,
485 });
486 slice.fill_with(|| iter.next().unwrap());
487 }
488 TexturePixelFormat::RgbaPremultiplied => {
489 let mut iter = source.chunks_exact(4).map(|p| Rgba8Pixel {
490 r: p[0],
491 g: p[1],
492 b: p[2],
493 a: p[3],
494 });
495 slice.fill_with(|| iter.next().unwrap());
496 }
497 TexturePixelFormat::Rgba => {
498 let mut iter = source.chunks_exact(4).map(|p| {
499 let a = p[3];
500 Rgba8Pixel {
501 r: (p[0] as u16 * a as u16 / 255) as u8,
502 g: (p[1] as u16 * a as u16 / 255) as u8,
503 b: (p[2] as u16 * a as u16 / 255) as u8,
504 a,
505 }
506 });
507 slice.fill_with(|| iter.next().unwrap());
508 }
509 TexturePixelFormat::AlphaMap => {
510 let col = t.color.to_argb_u8();
511 let mut iter = source.iter().map(|p| {
512 let a = *p as u32 * col.alpha as u32;
513 Rgba8Pixel {
514 r: (col.red as u32 * a / (255 * 255)) as u8,
515 g: (col.green as u32 * a / (255 * 255)) as u8,
516 b: (col.blue as u32 * a / (255 * 255)) as u8,
517 a: (a / 255) as u8,
518 }
519 });
520 slice.fill_with(|| iter.next().unwrap());
521 }
522 TexturePixelFormat::SignedDistanceField => {
523 todo!("converting from a signed distance field to an image")
524 }
525 };
526 }
527 }
528 Some(SharedImageBuffer::RGBA8Premultiplied(buffer))
529 }
530 ImageInner::NineSlice(nine) => nine.0.render_to_buffer(None),
531 _ => None,
532 }
533 }
534
535 pub fn is_svg(&self) -> bool {
537 match self {
538 #[cfg(feature = "svg")]
539 Self::Svg(_) => true,
540 #[cfg(target_arch = "wasm32")]
541 Self::HTMLImage(html_image) => html_image.is_svg(),
542 _ => false,
543 }
544 }
545
546 pub fn size(&self) -> IntSize {
548 match self {
549 ImageInner::None => Default::default(),
550 ImageInner::EmbeddedImage { buffer, .. } => buffer.size(),
551 ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
552 #[cfg(feature = "svg")]
553 ImageInner::Svg(svg) => svg.size(),
554 #[cfg(target_arch = "wasm32")]
555 ImageInner::HTMLImage(htmlimage) => htmlimage.size().unwrap_or_default(),
556 ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size(),
557 #[cfg(not(target_arch = "wasm32"))]
558 ImageInner::BorrowedOpenGLTexture(BorrowedOpenGLTexture { size, .. }) => *size,
559 ImageInner::NineSlice(nine) => nine.0.size(),
560 #[cfg(any(feature = "unstable-wgpu-26", feature = "unstable-wgpu-27"))]
561 ImageInner::WGPUTexture(texture) => texture.size(),
562 }
563 }
564}
565
566impl PartialEq for ImageInner {
567 fn eq(&self, other: &Self) -> bool {
568 match (self, other) {
569 (
570 Self::EmbeddedImage { cache_key: l_cache_key, buffer: l_buffer },
571 Self::EmbeddedImage { cache_key: r_cache_key, buffer: r_buffer },
572 ) => l_cache_key == r_cache_key && l_buffer == r_buffer,
573 #[cfg(feature = "svg")]
574 (Self::Svg(l0), Self::Svg(r0)) => vtable::VRc::ptr_eq(l0, r0),
575 (Self::StaticTextures(l0), Self::StaticTextures(r0)) => l0 == r0,
576 #[cfg(target_arch = "wasm32")]
577 (Self::HTMLImage(l0), Self::HTMLImage(r0)) => vtable::VRc::ptr_eq(l0, r0),
578 (Self::BackendStorage(l0), Self::BackendStorage(r0)) => vtable::VRc::ptr_eq(l0, r0),
579 #[cfg(not(target_arch = "wasm32"))]
580 (Self::BorrowedOpenGLTexture(l0), Self::BorrowedOpenGLTexture(r0)) => l0 == r0,
581 (Self::NineSlice(l), Self::NineSlice(r)) => l.0 == r.0 && l.1 == r.1,
582 _ => false,
583 }
584 }
585}
586
587impl<'a> From<&'a Image> for &'a ImageInner {
588 fn from(other: &'a Image) -> Self {
589 &other.0
590 }
591}
592
593#[derive(Default, Debug, PartialEq)]
595pub struct LoadImageError(());
596
597impl core::fmt::Display for LoadImageError {
598 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
599 f.write_str("The image cannot be loaded")
600 }
601}
602
603#[cfg(feature = "std")]
604impl std::error::Error for LoadImageError {}
605
606#[repr(transparent)]
703#[derive(Default, Clone, Debug, PartialEq, derive_more::From)]
704pub struct Image(pub(crate) ImageInner);
705
706impl Image {
707 #[cfg(feature = "image-decoders")]
708 pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> {
715 self::cache::IMAGE_CACHE.with(|global_cache| {
716 let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into();
717 global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(()))
718 })
719 }
720
721 pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
724 Image(ImageInner::EmbeddedImage {
725 cache_key: ImageCacheKey::Invalid,
726 buffer: SharedImageBuffer::RGB8(buffer),
727 })
728 }
729
730 pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
733 Image(ImageInner::EmbeddedImage {
734 cache_key: ImageCacheKey::Invalid,
735 buffer: SharedImageBuffer::RGBA8(buffer),
736 })
737 }
738
739 pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
745 Image(ImageInner::EmbeddedImage {
746 cache_key: ImageCacheKey::Invalid,
747 buffer: SharedImageBuffer::RGBA8Premultiplied(buffer),
748 })
749 }
750
751 pub fn to_rgb8(&self) -> Option<SharedPixelBuffer<Rgb8Pixel>> {
754 self.0.render_to_buffer(None).and_then(|image| match image {
755 SharedImageBuffer::RGB8(buffer) => Some(buffer),
756 _ => None,
757 })
758 }
759
760 pub fn to_rgba8(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
763 self.0.render_to_buffer(None).map(|image| match image {
764 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
765 width: buffer.width,
766 height: buffer.height,
767 data: buffer.data.into_iter().map(Into::into).collect(),
768 },
769 SharedImageBuffer::RGBA8(buffer) => buffer,
770 SharedImageBuffer::RGBA8Premultiplied(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
771 width: buffer.width,
772 height: buffer.height,
773 data: buffer.data.into_iter().map(Image::premultiplied_rgba_to_rgba).collect(),
774 },
775 })
776 }
777
778 pub fn to_rgba8_premultiplied(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
782 self.0.render_to_buffer(None).map(|image| match image {
783 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
784 width: buffer.width,
785 height: buffer.height,
786 data: buffer.data.into_iter().map(Into::into).collect(),
787 },
788 SharedImageBuffer::RGBA8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
789 width: buffer.width,
790 height: buffer.height,
791 data: buffer.data.into_iter().map(Image::rgba_to_premultiplied_rgba).collect(),
792 },
793 SharedImageBuffer::RGBA8Premultiplied(buffer) => buffer,
794 })
795 }
796
797 fn premultiplied_rgba_to_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
799 if pixel.a == 0 {
800 Rgba8Pixel::new(0, 0, 0, 0)
801 } else {
802 let af = pixel.a as u32;
803 let round = (af / 2) as u32;
804 Rgba8Pixel {
805 r: ((pixel.r as u32 * 255 + round) / af).min(255) as u8,
806 g: ((pixel.g as u32 * 255 + round) / af).min(255) as u8,
807 b: ((pixel.b as u32 * 255 + round) / af).min(255) as u8,
808 a: pixel.a,
809 }
810 }
811 }
812
813 fn rgba_to_premultiplied_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
815 if pixel.a == 255 {
816 pixel
817 } else {
818 let af = pixel.a as u32;
819 Rgba8Pixel {
820 r: (((pixel.r as u32 * af + 128) * 257) >> 16) as u8,
821 g: (((pixel.g as u32 * af + 128) * 257) >> 16) as u8,
822 b: (((pixel.b as u32 * af + 128) * 257) >> 16) as u8,
823 a: pixel.a,
824 }
825 }
826 }
827
828 #[cfg(feature = "unstable-wgpu-26")]
834 pub fn to_wgpu_26_texture(&self) -> Option<wgpu_26::Texture> {
835 match &self.0 {
836 ImageInner::WGPUTexture(WGPUTexture::WGPU26Texture(texture)) => Some(texture.clone()),
837 _ => None,
838 }
839 }
840
841 #[cfg(feature = "unstable-wgpu-27")]
847 pub fn to_wgpu_27_texture(&self) -> Option<wgpu_27::Texture> {
848 match &self.0 {
849 ImageInner::WGPUTexture(WGPUTexture::WGPU27Texture(texture)) => Some(texture.clone()),
850 _ => None,
851 }
852 }
853
854 #[allow(unsafe_code)]
874 #[cfg(not(target_arch = "wasm32"))]
875 #[deprecated(since = "1.2.0", note = "Use BorrowedOpenGLTextureBuilder")]
876 pub unsafe fn from_borrowed_gl_2d_rgba_texture(
877 texture_id: core::num::NonZeroU32,
878 size: IntSize,
879 ) -> Self {
880 BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size).build()
881 }
882
883 #[cfg(feature = "svg")]
885 pub fn load_from_svg_data(buffer: &[u8]) -> Result<Self, LoadImageError> {
886 let cache_key = ImageCacheKey::Invalid;
887 Ok(Image(ImageInner::Svg(vtable::VRc::new(
888 svg::load_from_data(buffer, cache_key).map_err(|_| LoadImageError(()))?,
889 ))))
890 }
891
892 pub fn set_nine_slice_edges(&mut self, top: u16, right: u16, bottom: u16, left: u16) {
898 if top == 0 && left == 0 && right == 0 && bottom == 0 {
899 if let ImageInner::NineSlice(n) = &self.0 {
900 self.0 = n.0.clone();
901 }
902 } else {
903 let array = [top, right, bottom, left];
904 let inner = if let ImageInner::NineSlice(n) = &mut self.0 {
905 n.0.clone()
906 } else {
907 self.0.clone()
908 };
909 self.0 = ImageInner::NineSlice(vtable::VRc::new(NineSliceImage(inner, array)));
910 }
911 }
912
913 pub fn size(&self) -> IntSize {
915 self.0.size()
916 }
917
918 #[cfg(feature = "std")]
919 pub fn path(&self) -> Option<&std::path::Path> {
931 match &self.0 {
932 ImageInner::EmbeddedImage {
933 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
934 ..
935 } => Some(std::path::Path::new(path.as_str())),
936 ImageInner::NineSlice(nine) => match &nine.0 {
937 ImageInner::EmbeddedImage {
938 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
939 ..
940 } => Some(std::path::Path::new(path.as_str())),
941 _ => None,
942 },
943 _ => None,
944 }
945 }
946}
947
948#[derive(Copy, Clone, Debug, PartialEq, Default)]
951#[repr(u8)]
952#[non_exhaustive]
953pub enum BorrowedOpenGLTextureOrigin {
954 #[default]
956 TopLeft,
957 BottomLeft,
960}
961
962#[cfg(not(target_arch = "wasm32"))]
980pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture);
981
982#[cfg(not(target_arch = "wasm32"))]
983impl BorrowedOpenGLTextureBuilder {
984 #[allow(unsafe_code)]
1002 pub unsafe fn new_gl_2d_rgba_texture(texture_id: core::num::NonZeroU32, size: IntSize) -> Self {
1003 Self(BorrowedOpenGLTexture { texture_id, size, origin: Default::default() })
1004 }
1005
1006 pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self {
1008 self.0.origin = origin;
1009 self
1010 }
1011
1012 pub fn build(self) -> Image {
1014 Image(ImageInner::BorrowedOpenGLTexture(self.0))
1015 }
1016}
1017
1018#[cfg(feature = "image-decoders")]
1021pub fn load_image_from_embedded_data(data: Slice<'static, u8>, format: Slice<'_, u8>) -> Image {
1022 self::cache::IMAGE_CACHE.with(|global_cache| {
1023 global_cache.borrow_mut().load_image_from_embedded_data(data, format).unwrap_or_default()
1024 })
1025}
1026
1027#[test]
1028fn test_image_size_from_buffer_without_backend() {
1029 {
1030 assert_eq!(Image::default().size(), Default::default());
1031 assert!(Image::default().to_rgb8().is_none());
1032 assert!(Image::default().to_rgba8().is_none());
1033 assert!(Image::default().to_rgba8_premultiplied().is_none());
1034 }
1035 {
1036 let buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
1037 let image = Image::from_rgb8(buffer.clone());
1038 assert_eq!(image.size(), [320, 200].into());
1039 assert_eq!(image.to_rgb8().as_ref().map(|b| b.as_slice()), Some(buffer.as_slice()));
1040 }
1041}
1042
1043#[cfg(feature = "svg")]
1044#[test]
1045fn test_image_size_from_svg() {
1046 let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
1047 let image = Image::load_from_svg_data(simple_svg.as_bytes()).unwrap();
1048 assert_eq!(image.size(), [320, 200].into());
1049 assert_eq!(image.to_rgba8().unwrap().size(), image.size());
1050}
1051
1052#[cfg(feature = "svg")]
1053#[test]
1054fn test_image_invalid_svg() {
1055 let invalid_svg = r#"AaBbCcDd"#;
1056 let result = Image::load_from_svg_data(invalid_svg.as_bytes());
1057 assert!(result.is_err());
1058}
1059
1060#[derive(Debug)]
1062pub struct FitResult {
1063 pub clip_rect: IntRect,
1065 pub source_to_target_x: f32,
1067 pub source_to_target_y: f32,
1069 pub size: euclid::Size2D<f32, PhysicalPx>,
1071 pub offset: euclid::Point2D<f32, PhysicalPx>,
1073 pub tiled: Option<euclid::default::Point2D<u32>>,
1077}
1078
1079impl FitResult {
1080 fn adjust_for_tiling(
1081 self,
1082 ratio: f32,
1083 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1084 tiling: (ImageTiling, ImageTiling),
1085 ) -> Self {
1086 let mut r = self;
1087 let mut tiled = euclid::Point2D::default();
1088 let target = r.size;
1089 let o = r.clip_rect.size.cast::<f32>();
1090 match tiling.0 {
1091 ImageTiling::None => {
1092 r.size.width = o.width * r.source_to_target_x;
1093 if (o.width as f32) > target.width / r.source_to_target_x {
1094 let diff = (o.width as f32 - target.width / r.source_to_target_x) as i32;
1095 r.clip_rect.size.width -= diff;
1096 r.clip_rect.origin.x += match alignment.0 {
1097 ImageHorizontalAlignment::Center => diff / 2,
1098 ImageHorizontalAlignment::Left => 0,
1099 ImageHorizontalAlignment::Right => diff,
1100 };
1101 r.size.width = target.width;
1102 } else if (o.width as f32) < target.width / r.source_to_target_x {
1103 r.offset.x += match alignment.0 {
1104 ImageHorizontalAlignment::Center => {
1105 (target.width - o.width as f32 * r.source_to_target_x) / 2.
1106 }
1107 ImageHorizontalAlignment::Left => 0.,
1108 ImageHorizontalAlignment::Right => {
1109 target.width - o.width as f32 * r.source_to_target_x
1110 }
1111 };
1112 }
1113 }
1114 ImageTiling::Repeat => {
1115 tiled.x = match alignment.0 {
1116 ImageHorizontalAlignment::Left => 0,
1117 ImageHorizontalAlignment::Center => {
1118 ((o.width - target.width / ratio) / 2.).rem_euclid(o.width) as u32
1119 }
1120 ImageHorizontalAlignment::Right => {
1121 (-target.width / ratio).rem_euclid(o.width) as u32
1122 }
1123 };
1124 r.source_to_target_x = ratio;
1125 }
1126 ImageTiling::Round => {
1127 if target.width / ratio <= o.width * 1.5 {
1128 r.source_to_target_x = target.width / o.width;
1129 } else {
1130 let mut rem = (target.width / ratio).rem_euclid(o.width);
1131 if rem > o.width / 2. {
1132 rem -= o.width;
1133 }
1134 r.source_to_target_x = ratio * target.width / (target.width - rem * ratio);
1135 }
1136 }
1137 }
1138
1139 match tiling.1 {
1140 ImageTiling::None => {
1141 r.size.height = o.height * r.source_to_target_y;
1142 if (o.height as f32) > target.height / r.source_to_target_y {
1143 let diff = (o.height as f32 - target.height / r.source_to_target_y) as i32;
1144 r.clip_rect.size.height -= diff;
1145 r.clip_rect.origin.y += match alignment.1 {
1146 ImageVerticalAlignment::Center => diff / 2,
1147 ImageVerticalAlignment::Top => 0,
1148 ImageVerticalAlignment::Bottom => diff,
1149 };
1150 r.size.height = target.height;
1151 } else if (o.height as f32) < target.height / r.source_to_target_y {
1152 r.offset.y += match alignment.1 {
1153 ImageVerticalAlignment::Center => {
1154 (target.height - o.height as f32 * r.source_to_target_y) / 2.
1155 }
1156 ImageVerticalAlignment::Top => 0.,
1157 ImageVerticalAlignment::Bottom => {
1158 target.height - o.height as f32 * r.source_to_target_y
1159 }
1160 };
1161 }
1162 }
1163 ImageTiling::Repeat => {
1164 tiled.y = match alignment.1 {
1165 ImageVerticalAlignment::Top => 0,
1166 ImageVerticalAlignment::Center => {
1167 ((o.height - target.height / ratio) / 2.).rem_euclid(o.height) as u32
1168 }
1169 ImageVerticalAlignment::Bottom => {
1170 (-target.height / ratio).rem_euclid(o.height) as u32
1171 }
1172 };
1173 r.source_to_target_y = ratio;
1174 }
1175 ImageTiling::Round => {
1176 if target.height / ratio <= o.height * 1.5 {
1177 r.source_to_target_y = target.height / o.height;
1178 } else {
1179 let mut rem = (target.height / ratio).rem_euclid(o.height);
1180 if rem > o.height / 2. {
1181 rem -= o.height;
1182 }
1183 r.source_to_target_y = ratio * target.height / (target.height - rem * ratio);
1184 }
1185 }
1186 }
1187 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1188 r.tiled = has_tiling.then_some(tiled);
1189 r
1190 }
1191}
1192
1193#[cfg(not(feature = "std"))]
1194trait RemEuclid {
1195 fn rem_euclid(self, b: f32) -> f32;
1196}
1197#[cfg(not(feature = "std"))]
1198impl RemEuclid for f32 {
1199 fn rem_euclid(self, b: f32) -> f32 {
1200 return num_traits::Euclid::rem_euclid(&self, &b);
1201 }
1202}
1203
1204pub fn fit(
1206 image_fit: ImageFit,
1207 target: euclid::Size2D<f32, PhysicalPx>,
1208 source_rect: IntRect,
1209 scale_factor: ScaleFactor,
1210 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1211 tiling: (ImageTiling, ImageTiling),
1212) -> FitResult {
1213 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1214 let o = source_rect.size.cast::<f32>();
1215 let ratio = match image_fit {
1216 _ if has_tiling => scale_factor.get(),
1218 ImageFit::Fill => {
1219 return FitResult {
1220 clip_rect: source_rect,
1221 source_to_target_x: target.width / o.width,
1222 source_to_target_y: target.height / o.height,
1223 size: target,
1224 offset: Default::default(),
1225 tiled: None,
1226 }
1227 }
1228 ImageFit::Preserve => scale_factor.get(),
1229 ImageFit::Contain => f32::min(target.width / o.width, target.height / o.height),
1230 ImageFit::Cover => f32::max(target.width / o.width, target.height / o.height),
1231 };
1232
1233 FitResult {
1234 clip_rect: source_rect,
1235 source_to_target_x: ratio,
1236 source_to_target_y: ratio,
1237 size: target,
1238 offset: euclid::Point2D::default(),
1239 tiled: None,
1240 }
1241 .adjust_for_tiling(ratio, alignment, tiling)
1242}
1243
1244pub fn fit9slice(
1246 source_rect: IntSize,
1247 [t, r, b, l]: [u16; 4],
1248 target: euclid::Size2D<f32, PhysicalPx>,
1249 scale_factor: ScaleFactor,
1250 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1251 tiling: (ImageTiling, ImageTiling),
1252) -> impl Iterator<Item = FitResult> {
1253 let fit_to = |clip_rect: euclid::default::Rect<u16>, target: euclid::Rect<f32, PhysicalPx>| {
1254 (!clip_rect.is_empty() && !target.is_empty()).then(|| {
1255 FitResult {
1256 clip_rect: clip_rect.cast(),
1257 source_to_target_x: target.width() / clip_rect.width() as f32,
1258 source_to_target_y: target.height() / clip_rect.height() as f32,
1259 size: target.size,
1260 offset: target.origin,
1261 tiled: None,
1262 }
1263 .adjust_for_tiling(scale_factor.get(), alignment, tiling)
1264 })
1265 };
1266 use euclid::rect;
1267 let sf = |x| scale_factor.get() * x as f32;
1268 let source = source_rect.cast::<u16>();
1269 if t + b > source.height || l + r > source.width {
1270 [None, None, None, None, None, None, None, None, None]
1271 } else {
1272 [
1273 fit_to(rect(0, 0, l, t), rect(0., 0., sf(l), sf(t))),
1274 fit_to(
1275 rect(l, 0, source.width - l - r, t),
1276 rect(sf(l), 0., target.width - sf(l) - sf(r), sf(t)),
1277 ),
1278 fit_to(rect(source.width - r, 0, r, t), rect(target.width - sf(r), 0., sf(r), sf(t))),
1279 fit_to(
1280 rect(0, t, l, source.height - t - b),
1281 rect(0., sf(t), sf(l), target.height - sf(t) - sf(b)),
1282 ),
1283 fit_to(
1284 rect(l, t, source.width - l - r, source.height - t - b),
1285 rect(sf(l), sf(t), target.width - sf(l) - sf(r), target.height - sf(t) - sf(b)),
1286 ),
1287 fit_to(
1288 rect(source.width - r, t, r, source.height - t - b),
1289 rect(target.width - sf(r), sf(t), sf(r), target.height - sf(t) - sf(b)),
1290 ),
1291 fit_to(rect(0, source.height - b, l, b), rect(0., target.height - sf(b), sf(l), sf(b))),
1292 fit_to(
1293 rect(l, source.height - b, source.width - l - r, b),
1294 rect(sf(l), target.height - sf(b), target.width - sf(l) - sf(r), sf(b)),
1295 ),
1296 fit_to(
1297 rect(source.width - r, source.height - b, r, b),
1298 rect(target.width - sf(r), target.height - sf(b), sf(r), sf(b)),
1299 ),
1300 ]
1301 }
1302 .into_iter()
1303 .flatten()
1304}
1305
1306#[cfg(feature = "ffi")]
1307pub(crate) mod ffi {
1308 #![allow(unsafe_code)]
1309
1310 use super::*;
1311
1312 #[cfg(cbindgen)]
1315 #[repr(C)]
1316 struct Rgb8Pixel {
1317 r: u8,
1319 g: u8,
1321 b: u8,
1323 }
1324
1325 #[cfg(cbindgen)]
1328 #[repr(C)]
1329 struct Rgba8Pixel {
1330 r: u8,
1332 g: u8,
1334 b: u8,
1336 a: u8,
1338 }
1339
1340 #[cfg(feature = "image-decoders")]
1341 #[unsafe(no_mangle)]
1342 pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) {
1343 core::ptr::write(
1344 image,
1345 Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or(Image::default()),
1346 )
1347 }
1348
1349 #[cfg(feature = "std")]
1350 #[unsafe(no_mangle)]
1351 pub unsafe extern "C" fn slint_image_load_from_embedded_data(
1352 data: Slice<'static, u8>,
1353 format: Slice<'static, u8>,
1354 image: *mut Image,
1355 ) {
1356 core::ptr::write(image, super::load_image_from_embedded_data(data, format));
1357 }
1358
1359 #[unsafe(no_mangle)]
1360 pub unsafe extern "C" fn slint_image_size(image: &Image) -> IntSize {
1361 image.size()
1362 }
1363
1364 #[unsafe(no_mangle)]
1365 pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
1366 match &image.0 {
1367 ImageInner::EmbeddedImage { cache_key, .. } => match cache_key {
1368 #[cfg(feature = "std")]
1369 ImageCacheKey::Path(CachedPath { path, .. }) => Some(path),
1370 _ => None,
1371 },
1372 ImageInner::NineSlice(nine) => match &nine.0 {
1373 ImageInner::EmbeddedImage { cache_key, .. } => match cache_key {
1374 #[cfg(feature = "std")]
1375 ImageCacheKey::Path(CachedPath { path, .. }) => Some(path),
1376 _ => None,
1377 },
1378 _ => None,
1379 },
1380 _ => None,
1381 }
1382 }
1383
1384 #[unsafe(no_mangle)]
1385 pub unsafe extern "C" fn slint_image_from_embedded_textures(
1386 textures: &'static StaticTextures,
1387 image: *mut Image,
1388 ) {
1389 core::ptr::write(image, Image::from(ImageInner::StaticTextures(textures)));
1390 }
1391
1392 #[unsafe(no_mangle)]
1393 pub unsafe extern "C" fn slint_image_compare_equal(image1: &Image, image2: &Image) -> bool {
1394 image1.eq(image2)
1395 }
1396
1397 #[unsafe(no_mangle)]
1399 pub extern "C" fn slint_image_set_nine_slice_edges(
1400 image: &mut Image,
1401 top: u16,
1402 right: u16,
1403 bottom: u16,
1404 left: u16,
1405 ) {
1406 image.set_nine_slice_edges(top, right, bottom, left);
1407 }
1408
1409 #[unsafe(no_mangle)]
1410 pub extern "C" fn slint_image_to_rgb8(
1411 image: &Image,
1412 data: &mut SharedVector<Rgb8Pixel>,
1413 width: &mut u32,
1414 height: &mut u32,
1415 ) -> bool {
1416 image.to_rgb8().is_some_and(|pixel_buffer| {
1417 *data = pixel_buffer.data.clone();
1418 *width = pixel_buffer.width();
1419 *height = pixel_buffer.height();
1420 true
1421 })
1422 }
1423
1424 #[unsafe(no_mangle)]
1425 pub extern "C" fn slint_image_to_rgba8(
1426 image: &Image,
1427 data: &mut SharedVector<Rgba8Pixel>,
1428 width: &mut u32,
1429 height: &mut u32,
1430 ) -> bool {
1431 image.to_rgba8().is_some_and(|pixel_buffer| {
1432 *data = pixel_buffer.data.clone();
1433 *width = pixel_buffer.width();
1434 *height = pixel_buffer.height();
1435 true
1436 })
1437 }
1438
1439 #[unsafe(no_mangle)]
1440 pub extern "C" fn slint_image_to_rgba8_premultiplied(
1441 image: &Image,
1442 data: &mut SharedVector<Rgba8Pixel>,
1443 width: &mut u32,
1444 height: &mut u32,
1445 ) -> bool {
1446 image.to_rgba8_premultiplied().is_some_and(|pixel_buffer| {
1447 *data = pixel_buffer.data.clone();
1448 *width = pixel_buffer.width();
1449 *height = pixel_buffer.height();
1450 true
1451 })
1452 }
1453}
1454
1455#[derive(Clone, Debug, PartialEq)]
1463#[non_exhaustive]
1464#[cfg(not(target_arch = "wasm32"))]
1465#[repr(C)]
1466pub struct BorrowedOpenGLTexture {
1467 pub texture_id: core::num::NonZeroU32,
1469 pub size: IntSize,
1471 pub origin: BorrowedOpenGLTextureOrigin,
1473}
1474
1475#[cfg(test)]
1476mod tests {
1477 use crate::graphics::Rgba8Pixel;
1478
1479 use super::Image;
1480
1481 #[test]
1482 fn test_premultiplied_to_rgb_zero_alpha() {
1483 let pixel = Rgba8Pixel::new(5, 10, 15, 0);
1484 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1485 assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1486 }
1487
1488 #[test]
1489 fn test_premultiplied_to_rgb_full_alpha() {
1490 let pixel = Rgba8Pixel::new(5, 10, 15, 255);
1491 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1492 assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 255));
1493 }
1494
1495 #[test]
1496 fn test_premultiplied_to_rgb() {
1497 let pixel = Rgba8Pixel::new(5, 10, 15, 128);
1498 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1499 assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 128));
1500 }
1501
1502 #[test]
1503 fn test_rgb_to_premultiplied_zero_alpha() {
1504 let pixel = Rgba8Pixel::new(10, 20, 30, 0);
1505 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1506 assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1507 }
1508
1509 #[test]
1510 fn test_rgb_to_premultiplied_full_alpha() {
1511 let pixel = Rgba8Pixel::new(10, 20, 30, 255);
1512 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1513 assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 255));
1514 }
1515
1516 #[test]
1517 fn test_rgb_to_premultiplied() {
1518 let pixel = Rgba8Pixel::new(10, 20, 30, 128);
1519 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1520 assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 128));
1521 }
1522}