1use crate::lengths::{PhysicalPx, ScaleFactor};
10use crate::slice::Slice;
11#[allow(unused)]
12use crate::{SharedString, SharedVector};
13
14use super::{IntRect, IntSize};
15use crate::items::{ImageFit, ImageHorizontalAlignment, ImageTiling, ImageVerticalAlignment};
16
17#[cfg(feature = "image-decoders")]
18pub mod cache;
19#[cfg(target_arch = "wasm32")]
20mod htmlimage;
21#[cfg(feature = "svg")]
22mod svg;
23
24#[allow(missing_docs)]
25#[cfg_attr(not(feature = "ffi"), i_slint_core_macros::remove_extern)]
26#[vtable::vtable]
27#[repr(C)]
28pub struct OpaqueImageVTable {
29 drop_in_place: extern "C" fn(VRefMut<OpaqueImageVTable>) -> Layout,
30 dealloc: extern "C" fn(&OpaqueImageVTable, ptr: *mut u8, layout: Layout),
31 size: extern "C" fn(VRef<OpaqueImageVTable>) -> IntSize,
33 cache_key: extern "C" fn(VRef<OpaqueImageVTable>) -> ImageCacheKey,
35}
36
37#[cfg(feature = "svg")]
38OpaqueImageVTable_static! {
39 pub static PARSED_SVG_VT for svg::ParsedSVG
41}
42
43#[cfg(target_arch = "wasm32")]
44OpaqueImageVTable_static! {
45 pub static HTML_IMAGE_VT for htmlimage::HTMLImage
47}
48
49OpaqueImageVTable_static! {
50 pub static NINE_SLICE_VT for NineSliceImage
52}
53
54#[derive(Debug, Clone)]
64#[repr(C)]
65pub struct SharedPixelBuffer<Pixel> {
66 width: u32,
67 height: u32,
68 pub(crate) data: SharedVector<Pixel>,
69}
70
71impl<Pixel> SharedPixelBuffer<Pixel> {
72 pub fn width(&self) -> u32 {
74 self.width
75 }
76
77 pub fn height(&self) -> u32 {
79 self.height
80 }
81
82 pub fn size(&self) -> IntSize {
84 [self.width, self.height].into()
85 }
86}
87
88impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
89 pub fn make_mut_slice(&mut self) -> &mut [Pixel] {
91 self.data.make_mut_slice()
92 }
93}
94
95impl<Pixel: Clone + rgb::Pod> SharedPixelBuffer<Pixel>
96where
97 [Pixel]: rgb::ComponentBytes<u8>,
98{
99 pub fn as_bytes(&self) -> &[u8] {
101 use rgb::ComponentBytes;
102 self.data.as_slice().as_bytes()
103 }
104
105 pub fn make_mut_bytes(&mut self) -> &mut [u8] {
107 use rgb::ComponentBytes;
108 self.data.make_mut_slice().as_bytes_mut()
109 }
110}
111
112impl<Pixel> SharedPixelBuffer<Pixel> {
113 pub fn as_slice(&self) -> &[Pixel] {
115 self.data.as_slice()
116 }
117}
118
119impl<Pixel: Clone + Default> SharedPixelBuffer<Pixel> {
120 pub fn new(width: u32, height: u32) -> Self {
123 Self {
124 width,
125 height,
126 data: core::iter::repeat_n(Pixel::default(), 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(any(feature = "std", feature = "ffi"))]
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(any(feature = "std", feature = "ffi"))]
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-28", feature = "unstable-wgpu-29"))]
350 ImageInner::WGPUTexture(..) => return None,
351 };
352 if matches!(key, ImageCacheKey::Invalid) { None } else { Some(key) }
353 }
354
355 pub fn from_embedded_image_data(data: &'static [u8]) -> Self {
357 Self::EmbeddedData(data.as_ptr() as usize)
358 }
359}
360
361pub struct NineSliceImage(pub ImageInner, pub [u16; 4]);
363
364impl NineSliceImage {
365 pub fn image(&self) -> Image {
367 Image(self.0.clone())
368 }
369}
370
371impl OpaqueImage for NineSliceImage {
372 fn size(&self) -> IntSize {
373 self.0.size()
374 }
375 fn cache_key(&self) -> ImageCacheKey {
376 ImageCacheKey::new(&self.0).unwrap_or(ImageCacheKey::Invalid)
377 }
378}
379
380#[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))]
383#[derive(Clone, Debug)]
384pub enum WGPUTexture {
385 #[cfg(feature = "unstable-wgpu-28")]
387 WGPU28Texture(wgpu_28::Texture),
388 #[cfg(feature = "unstable-wgpu-29")]
390 WGPU29Texture(wgpu_29::Texture),
391}
392
393#[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))]
394impl OpaqueImage for WGPUTexture {
395 fn size(&self) -> IntSize {
396 match self {
397 #[cfg(feature = "unstable-wgpu-28")]
398 Self::WGPU28Texture(texture) => {
399 let size = texture.size();
400 (size.width, size.height).into()
401 }
402 #[cfg(feature = "unstable-wgpu-29")]
403 Self::WGPU29Texture(texture) => {
404 let size = texture.size();
405 (size.width, size.height).into()
406 }
407 }
408 }
409 fn cache_key(&self) -> ImageCacheKey {
410 ImageCacheKey::Invalid
411 }
412}
413
414#[derive(Clone, Debug, Default)]
419#[repr(u8)]
420#[allow(missing_docs)]
421pub enum ImageInner {
422 #[default]
424 None = 0,
425 EmbeddedImage {
426 cache_key: ImageCacheKey,
427 buffer: SharedImageBuffer,
428 } = 1,
429 #[cfg(feature = "svg")]
430 Svg(vtable::VRc<OpaqueImageVTable, svg::ParsedSVG>) = 2,
431 StaticTextures(&'static StaticTextures) = 3,
432 #[cfg(target_arch = "wasm32")]
433 HTMLImage(vtable::VRc<OpaqueImageVTable, htmlimage::HTMLImage>) = 4,
434 BackendStorage(vtable::VRc<OpaqueImageVTable>) = 5,
435 #[cfg(not(target_arch = "wasm32"))]
436 BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6,
437 NineSlice(vtable::VRc<OpaqueImageVTable, NineSliceImage>) = 7,
438 #[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))]
439 WGPUTexture(WGPUTexture) = 8,
440}
441
442impl ImageInner {
443 pub fn render_to_buffer(
450 &self,
451 _target_size_for_scalable_source: Option<euclid::Size2D<u32, PhysicalPx>>,
452 ) -> Option<SharedImageBuffer> {
453 match self {
454 ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()),
455 #[cfg(feature = "svg")]
456 ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) {
457 Ok(b) => Some(b),
458 Err(resvg::usvg::Error::InvalidSize) => None,
460 Err(err) => {
461 std::eprintln!("Error rendering SVG: {err}");
462 None
463 }
464 },
465 ImageInner::StaticTextures(ts) => {
466 let mut buffer =
467 SharedPixelBuffer::<Rgba8Pixel>::new(ts.size.width, ts.size.height);
468 let stride = buffer.width() as usize;
469 let slice = buffer.make_mut_slice();
470 for t in ts.textures.iter() {
471 let rect = t.rect.to_usize();
472 for y in 0..rect.height() {
473 let slice = &mut slice[(rect.min_y() + y) * stride..][rect.x_range()];
474 let source = &ts.data[t.index + y * rect.width() * t.format.bpp()..];
475 match t.format {
476 TexturePixelFormat::Rgb => {
477 let mut iter = source.chunks_exact(3).map(|p| Rgba8Pixel {
478 r: p[0],
479 g: p[1],
480 b: p[2],
481 a: 255,
482 });
483 slice.fill_with(|| iter.next().unwrap());
484 }
485 TexturePixelFormat::RgbaPremultiplied => {
486 let mut iter = source.chunks_exact(4).map(|p| Rgba8Pixel {
487 r: p[0],
488 g: p[1],
489 b: p[2],
490 a: p[3],
491 });
492 slice.fill_with(|| iter.next().unwrap());
493 }
494 TexturePixelFormat::Rgba => {
495 let mut iter = source.chunks_exact(4).map(|p| {
496 let a = p[3];
497 Rgba8Pixel {
498 r: (p[0] as u16 * a as u16 / 255) as u8,
499 g: (p[1] as u16 * a as u16 / 255) as u8,
500 b: (p[2] as u16 * a as u16 / 255) as u8,
501 a,
502 }
503 });
504 slice.fill_with(|| iter.next().unwrap());
505 }
506 TexturePixelFormat::AlphaMap => {
507 let col = t.color.to_argb_u8();
508 let mut iter = source.iter().map(|p| {
509 let a = *p as u32 * col.alpha as u32;
510 Rgba8Pixel {
511 r: (col.red as u32 * a / (255 * 255)) as u8,
512 g: (col.green as u32 * a / (255 * 255)) as u8,
513 b: (col.blue as u32 * a / (255 * 255)) as u8,
514 a: (a / 255) as u8,
515 }
516 });
517 slice.fill_with(|| iter.next().unwrap());
518 }
519 TexturePixelFormat::SignedDistanceField => {
520 todo!("converting from a signed distance field to an image")
521 }
522 };
523 }
524 }
525 Some(SharedImageBuffer::RGBA8Premultiplied(buffer))
526 }
527 ImageInner::NineSlice(nine) => nine.0.render_to_buffer(None),
528 _ => None,
529 }
530 }
531
532 pub fn is_svg(&self) -> bool {
534 match self {
535 #[cfg(feature = "svg")]
536 Self::Svg(_) => true,
537 #[cfg(target_arch = "wasm32")]
538 Self::HTMLImage(html_image) => html_image.is_svg(),
539 _ => false,
540 }
541 }
542
543 pub fn size(&self) -> IntSize {
545 match self {
546 ImageInner::None => Default::default(),
547 ImageInner::EmbeddedImage { buffer, .. } => buffer.size(),
548 ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
549 #[cfg(feature = "svg")]
550 ImageInner::Svg(svg) => svg.size(),
551 #[cfg(target_arch = "wasm32")]
552 ImageInner::HTMLImage(htmlimage) => htmlimage.size().unwrap_or_default(),
553 ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size(),
554 #[cfg(not(target_arch = "wasm32"))]
555 ImageInner::BorrowedOpenGLTexture(BorrowedOpenGLTexture { size, .. }) => *size,
556 ImageInner::NineSlice(nine) => nine.0.size(),
557 #[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))]
558 ImageInner::WGPUTexture(texture) => texture.size(),
559 }
560 }
561
562 #[cfg(feature = "image-decoders")]
569 pub(crate) fn load_from_data_with_cache_key(
570 cache_key: ImageCacheKey,
571 data: Slice<'_, u8>,
572 format: Slice<'_, u8>,
573 ) -> Option<Self> {
574 #[cfg(feature = "svg")]
575 if format.as_slice() == b"svg" || format.as_slice() == b"svgz" {
576 return Some(ImageInner::Svg(vtable::VRc::new(
577 svg::load_from_data(data.as_slice(), cache_key).map_or_else(
578 |svg_err| {
579 crate::debug_log!("Error loading SVG: {}", svg_err);
580 None
581 },
582 Some,
583 )?,
584 )));
585 }
586
587 let format = std::str::from_utf8(format.as_slice())
588 .ok()
589 .and_then(image::ImageFormat::from_extension);
590 let maybe_image = if let Some(format) = format {
591 image::load_from_memory_with_format(data.as_slice(), format)
592 } else {
593 image::load_from_memory(data.as_slice())
594 };
595
596 match maybe_image {
597 Ok(image) => Some(ImageInner::EmbeddedImage {
598 cache_key,
599 buffer: dynamic_image_to_shared_image_buffer(image),
600 }),
601 Err(decode_err) => {
602 crate::debug_log!("Error decoding embedded image: {}", decode_err);
603 None
604 }
605 }
606 }
607}
608
609#[cfg(feature = "image-decoders")]
611fn dynamic_image_to_shared_image_buffer(dynamic_image: image::DynamicImage) -> SharedImageBuffer {
612 use rgb::AsPixels;
613
614 if dynamic_image.color().has_alpha() {
615 let rgba8image = dynamic_image.to_rgba8();
616 SharedImageBuffer::RGBA8Premultiplied(SharedPixelBuffer {
619 width: rgba8image.width(),
620 height: rgba8image.height(),
621 data: rgba8image
622 .as_pixels()
623 .iter()
624 .map(|pixel| Image::rgba_to_premultiplied_rgba(*pixel))
625 .collect(),
626 })
627 } else {
628 let rgb8image = dynamic_image.to_rgb8();
629 SharedImageBuffer::RGB8(SharedPixelBuffer::clone_from_slice(
630 rgb8image.as_raw(),
631 rgb8image.width(),
632 rgb8image.height(),
633 ))
634 }
635}
636
637impl PartialEq for ImageInner {
638 fn eq(&self, other: &Self) -> bool {
639 match (self, other) {
640 (
641 Self::EmbeddedImage { cache_key: l_cache_key, buffer: l_buffer },
642 Self::EmbeddedImage { cache_key: r_cache_key, buffer: r_buffer },
643 ) => l_cache_key == r_cache_key && l_buffer == r_buffer,
644 #[cfg(feature = "svg")]
645 (Self::Svg(l0), Self::Svg(r0)) => vtable::VRc::ptr_eq(l0, r0),
646 (Self::StaticTextures(l0), Self::StaticTextures(r0)) => l0 == r0,
647 #[cfg(target_arch = "wasm32")]
648 (Self::HTMLImage(l0), Self::HTMLImage(r0)) => vtable::VRc::ptr_eq(l0, r0),
649 (Self::BackendStorage(l0), Self::BackendStorage(r0)) => vtable::VRc::ptr_eq(l0, r0),
650 #[cfg(not(target_arch = "wasm32"))]
651 (Self::BorrowedOpenGLTexture(l0), Self::BorrowedOpenGLTexture(r0)) => l0 == r0,
652 (Self::NineSlice(l), Self::NineSlice(r)) => l.0 == r.0 && l.1 == r.1,
653 _ => false,
654 }
655 }
656}
657
658impl<'a> From<&'a Image> for &'a ImageInner {
659 fn from(other: &'a Image) -> Self {
660 &other.0
661 }
662}
663
664#[derive(Default, Debug, PartialEq)]
666pub struct LoadImageError(());
667
668impl core::fmt::Display for LoadImageError {
669 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
670 f.write_str("The image cannot be loaded")
671 }
672}
673
674#[cfg(feature = "std")]
675impl std::error::Error for LoadImageError {}
676
677#[repr(transparent)]
774#[derive(Default, Clone, Debug, PartialEq, derive_more::From)]
775pub struct Image(pub(crate) ImageInner);
776
777impl Image {
778 #[cfg(feature = "image-decoders")]
779 pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> {
786 self::cache::IMAGE_CACHE.with(|global_cache| {
787 let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into();
788 global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(()))
789 })
790 }
791
792 pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
795 Image(ImageInner::EmbeddedImage {
796 cache_key: ImageCacheKey::Invalid,
797 buffer: SharedImageBuffer::RGB8(buffer),
798 })
799 }
800
801 pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
804 Image(ImageInner::EmbeddedImage {
805 cache_key: ImageCacheKey::Invalid,
806 buffer: SharedImageBuffer::RGBA8(buffer),
807 })
808 }
809
810 pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
816 Image(ImageInner::EmbeddedImage {
817 cache_key: ImageCacheKey::Invalid,
818 buffer: SharedImageBuffer::RGBA8Premultiplied(buffer),
819 })
820 }
821
822 pub fn to_rgb8(&self) -> Option<SharedPixelBuffer<Rgb8Pixel>> {
825 self.0.render_to_buffer(None).and_then(|image| match image {
826 SharedImageBuffer::RGB8(buffer) => Some(buffer),
827 _ => None,
828 })
829 }
830
831 pub fn to_rgba8(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
834 self.0.render_to_buffer(None).map(|image| match image {
835 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
836 width: buffer.width,
837 height: buffer.height,
838 data: buffer.data.into_iter().map(Into::into).collect(),
839 },
840 SharedImageBuffer::RGBA8(buffer) => buffer,
841 SharedImageBuffer::RGBA8Premultiplied(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
842 width: buffer.width,
843 height: buffer.height,
844 data: buffer.data.into_iter().map(Image::premultiplied_rgba_to_rgba).collect(),
845 },
846 })
847 }
848
849 pub fn to_rgba8_premultiplied(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
853 self.0.render_to_buffer(None).map(|image| match image {
854 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
855 width: buffer.width,
856 height: buffer.height,
857 data: buffer.data.into_iter().map(Into::into).collect(),
858 },
859 SharedImageBuffer::RGBA8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
860 width: buffer.width,
861 height: buffer.height,
862 data: buffer.data.into_iter().map(Image::rgba_to_premultiplied_rgba).collect(),
863 },
864 SharedImageBuffer::RGBA8Premultiplied(buffer) => buffer,
865 })
866 }
867
868 fn premultiplied_rgba_to_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
870 if pixel.a == 0 {
871 Rgba8Pixel::new(0, 0, 0, 0)
872 } else {
873 let af = pixel.a as u32;
874 let round = (af / 2) as u32;
875 Rgba8Pixel {
876 r: ((pixel.r as u32 * 255 + round) / af).min(255) as u8,
877 g: ((pixel.g as u32 * 255 + round) / af).min(255) as u8,
878 b: ((pixel.b as u32 * 255 + round) / af).min(255) as u8,
879 a: pixel.a,
880 }
881 }
882 }
883
884 fn rgba_to_premultiplied_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
886 if pixel.a == 255 {
887 pixel
888 } else {
889 let af = pixel.a as u32;
890 Rgba8Pixel {
891 r: (((pixel.r as u32 * af + 128) * 257) >> 16) as u8,
892 g: (((pixel.g as u32 * af + 128) * 257) >> 16) as u8,
893 b: (((pixel.b as u32 * af + 128) * 257) >> 16) as u8,
894 a: pixel.a,
895 }
896 }
897 }
898
899 #[cfg(feature = "unstable-wgpu-28")]
905 pub fn to_wgpu_28_texture(&self) -> Option<wgpu_28::Texture> {
906 match &self.0 {
907 ImageInner::WGPUTexture(WGPUTexture::WGPU28Texture(texture)) => Some(texture.clone()),
908 _ => None,
909 }
910 }
911
912 #[cfg(feature = "unstable-wgpu-29")]
918 pub fn to_wgpu_29_texture(&self) -> Option<wgpu_29::Texture> {
919 match &self.0 {
920 ImageInner::WGPUTexture(WGPUTexture::WGPU29Texture(texture)) => Some(texture.clone()),
921 _ => None,
922 }
923 }
924
925 #[allow(unsafe_code)]
945 #[cfg(not(target_arch = "wasm32"))]
946 #[deprecated(since = "1.2.0", note = "Use BorrowedOpenGLTextureBuilder")]
947 pub unsafe fn from_borrowed_gl_2d_rgba_texture(
948 texture_id: core::num::NonZeroU32,
949 size: IntSize,
950 ) -> Self {
951 unsafe { BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size).build() }
952 }
953
954 #[cfg(feature = "svg")]
956 pub fn load_from_svg_data(buffer: &[u8]) -> Result<Self, LoadImageError> {
957 let cache_key = ImageCacheKey::Invalid;
958 Ok(Image(ImageInner::Svg(vtable::VRc::new(
959 svg::load_from_data(buffer, cache_key).map_err(|_| LoadImageError(()))?,
960 ))))
961 }
962
963 pub fn set_nine_slice_edges(&mut self, top: u16, right: u16, bottom: u16, left: u16) {
969 if top == 0 && left == 0 && right == 0 && bottom == 0 {
970 if let ImageInner::NineSlice(n) = &self.0 {
971 self.0 = n.0.clone();
972 }
973 } else {
974 let array = [top, right, bottom, left];
975 let inner = if let ImageInner::NineSlice(n) = &mut self.0 {
976 n.0.clone()
977 } else {
978 self.0.clone()
979 };
980 self.0 = ImageInner::NineSlice(vtable::VRc::new(NineSliceImage(inner, array)));
981 }
982 }
983
984 pub fn size(&self) -> IntSize {
986 self.0.size()
987 }
988
989 #[cfg(feature = "std")]
990 pub fn path(&self) -> Option<&std::path::Path> {
1002 match &self.0 {
1003 ImageInner::EmbeddedImage {
1004 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1005 ..
1006 } => Some(std::path::Path::new(path.as_str())),
1007 ImageInner::NineSlice(nine) => match &nine.0 {
1008 ImageInner::EmbeddedImage {
1009 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1010 ..
1011 } => Some(std::path::Path::new(path.as_str())),
1012 _ => None,
1013 },
1014 _ => None,
1015 }
1016 }
1017}
1018
1019#[cfg(feature = "image-decoders")]
1020pub fn load_image_from_dynamic_data(bytes: &[u8], format: &str) -> Result<Image, LoadImageError> {
1027 ImageInner::load_from_data_with_cache_key(
1028 ImageCacheKey::Invalid,
1029 bytes.into(),
1030 format.as_bytes().into(),
1031 )
1032 .map(Image)
1033 .ok_or(Default::default())
1034}
1035
1036#[derive(Copy, Clone, Debug, PartialEq, Default)]
1039#[repr(u8)]
1040#[non_exhaustive]
1041pub enum BorrowedOpenGLTextureOrigin {
1042 #[default]
1044 TopLeft,
1045 BottomLeft,
1048}
1049
1050#[cfg(not(target_arch = "wasm32"))]
1068pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture);
1069
1070#[cfg(not(target_arch = "wasm32"))]
1071impl BorrowedOpenGLTextureBuilder {
1072 #[allow(unsafe_code)]
1090 pub unsafe fn new_gl_2d_rgba_texture(texture_id: core::num::NonZeroU32, size: IntSize) -> Self {
1091 Self(BorrowedOpenGLTexture { texture_id, size, origin: Default::default() })
1092 }
1093
1094 pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self {
1096 self.0.origin = origin;
1097 self
1098 }
1099
1100 pub fn build(self) -> Image {
1102 Image(ImageInner::BorrowedOpenGLTexture(self.0))
1103 }
1104}
1105
1106#[cfg(feature = "image-decoders")]
1109pub fn load_image_from_embedded_data(data: Slice<'static, u8>, format: Slice<'_, u8>) -> Image {
1110 self::cache::IMAGE_CACHE.with(|global_cache| {
1111 global_cache.borrow_mut().load_image_from_embedded_data(data, format).unwrap_or_default()
1112 })
1113}
1114
1115#[test]
1116fn test_image_size_from_buffer_without_backend() {
1117 {
1118 assert_eq!(Image::default().size(), Default::default());
1119 assert!(Image::default().to_rgb8().is_none());
1120 assert!(Image::default().to_rgba8().is_none());
1121 assert!(Image::default().to_rgba8_premultiplied().is_none());
1122 }
1123 {
1124 let buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
1125 let image = Image::from_rgb8(buffer.clone());
1126 assert_eq!(image.size(), [320, 200].into());
1127 assert_eq!(image.to_rgb8().as_ref().map(|b| b.as_slice()), Some(buffer.as_slice()));
1128 }
1129}
1130
1131#[cfg(feature = "svg")]
1132#[test]
1133fn test_image_size_from_svg() {
1134 let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
1135 let image = Image::load_from_svg_data(simple_svg.as_bytes()).unwrap();
1136 assert_eq!(image.size(), [320, 200].into());
1137 assert_eq!(image.to_rgba8().unwrap().size(), image.size());
1138}
1139
1140#[cfg(feature = "svg")]
1141#[test]
1142fn test_image_invalid_svg() {
1143 let invalid_svg = r#"AaBbCcDd"#;
1144 let result = Image::load_from_svg_data(invalid_svg.as_bytes());
1145 assert!(result.is_err());
1146}
1147
1148#[derive(Debug)]
1150pub struct FitResult {
1151 pub clip_rect: IntRect,
1153 pub source_to_target_x: f32,
1155 pub source_to_target_y: f32,
1157 pub size: euclid::Size2D<f32, PhysicalPx>,
1159 pub offset: euclid::Point2D<f32, PhysicalPx>,
1161 pub tiled: Option<euclid::default::Point2D<u32>>,
1165}
1166
1167impl FitResult {
1168 fn adjust_for_tiling(
1169 self,
1170 ratio: f32,
1171 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1172 tiling: (ImageTiling, ImageTiling),
1173 ) -> Self {
1174 let mut r = self;
1175 let mut tiled = euclid::Point2D::default();
1176 let target = r.size;
1177 let o = r.clip_rect.size.cast::<f32>();
1178 match tiling.0 {
1179 ImageTiling::None => {
1180 r.size.width = o.width * r.source_to_target_x;
1181 if (o.width as f32) > target.width / r.source_to_target_x {
1182 let diff = (o.width as f32 - target.width / r.source_to_target_x) as i32;
1183 r.clip_rect.size.width -= diff;
1184 r.clip_rect.origin.x += match alignment.0 {
1185 ImageHorizontalAlignment::Center => diff / 2,
1186 ImageHorizontalAlignment::Left => 0,
1187 ImageHorizontalAlignment::Right => diff,
1188 };
1189 r.size.width = target.width;
1190 } else if (o.width as f32) < target.width / r.source_to_target_x {
1191 r.offset.x += match alignment.0 {
1192 ImageHorizontalAlignment::Center => {
1193 (target.width - o.width as f32 * r.source_to_target_x) / 2.
1194 }
1195 ImageHorizontalAlignment::Left => 0.,
1196 ImageHorizontalAlignment::Right => {
1197 target.width - o.width as f32 * r.source_to_target_x
1198 }
1199 };
1200 }
1201 }
1202 ImageTiling::Repeat => {
1203 tiled.x = match alignment.0 {
1204 ImageHorizontalAlignment::Left => 0,
1205 ImageHorizontalAlignment::Center => {
1206 ((o.width - target.width / ratio) / 2.).rem_euclid(o.width) as u32
1207 }
1208 ImageHorizontalAlignment::Right => {
1209 (-target.width / ratio).rem_euclid(o.width) as u32
1210 }
1211 };
1212 r.source_to_target_x = ratio;
1213 }
1214 ImageTiling::Round => {
1215 if target.width / ratio <= o.width * 1.5 {
1216 r.source_to_target_x = target.width / o.width;
1217 } else {
1218 let mut rem = (target.width / ratio).rem_euclid(o.width);
1219 if rem > o.width / 2. {
1220 rem -= o.width;
1221 }
1222 r.source_to_target_x = ratio * target.width / (target.width - rem * ratio);
1223 }
1224 }
1225 }
1226
1227 match tiling.1 {
1228 ImageTiling::None => {
1229 r.size.height = o.height * r.source_to_target_y;
1230 if (o.height as f32) > target.height / r.source_to_target_y {
1231 let diff = (o.height as f32 - target.height / r.source_to_target_y) as i32;
1232 r.clip_rect.size.height -= diff;
1233 r.clip_rect.origin.y += match alignment.1 {
1234 ImageVerticalAlignment::Center => diff / 2,
1235 ImageVerticalAlignment::Top => 0,
1236 ImageVerticalAlignment::Bottom => diff,
1237 };
1238 r.size.height = target.height;
1239 } else if (o.height as f32) < target.height / r.source_to_target_y {
1240 r.offset.y += match alignment.1 {
1241 ImageVerticalAlignment::Center => {
1242 (target.height - o.height as f32 * r.source_to_target_y) / 2.
1243 }
1244 ImageVerticalAlignment::Top => 0.,
1245 ImageVerticalAlignment::Bottom => {
1246 target.height - o.height as f32 * r.source_to_target_y
1247 }
1248 };
1249 }
1250 }
1251 ImageTiling::Repeat => {
1252 tiled.y = match alignment.1 {
1253 ImageVerticalAlignment::Top => 0,
1254 ImageVerticalAlignment::Center => {
1255 ((o.height - target.height / ratio) / 2.).rem_euclid(o.height) as u32
1256 }
1257 ImageVerticalAlignment::Bottom => {
1258 (-target.height / ratio).rem_euclid(o.height) as u32
1259 }
1260 };
1261 r.source_to_target_y = ratio;
1262 }
1263 ImageTiling::Round => {
1264 if target.height / ratio <= o.height * 1.5 {
1265 r.source_to_target_y = target.height / o.height;
1266 } else {
1267 let mut rem = (target.height / ratio).rem_euclid(o.height);
1268 if rem > o.height / 2. {
1269 rem -= o.height;
1270 }
1271 r.source_to_target_y = ratio * target.height / (target.height - rem * ratio);
1272 }
1273 }
1274 }
1275 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1276 r.tiled = has_tiling.then_some(tiled);
1277 r
1278 }
1279}
1280
1281#[cfg(not(feature = "std"))]
1282trait RemEuclid {
1283 fn rem_euclid(self, b: f32) -> f32;
1284}
1285#[cfg(not(feature = "std"))]
1286impl RemEuclid for f32 {
1287 fn rem_euclid(self, b: f32) -> f32 {
1288 num_traits::Euclid::rem_euclid(&self, &b)
1289 }
1290}
1291
1292pub fn fit(
1294 image_fit: ImageFit,
1295 target: euclid::Size2D<f32, PhysicalPx>,
1296 source_rect: IntRect,
1297 scale_factor: ScaleFactor,
1298 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1299 tiling: (ImageTiling, ImageTiling),
1300) -> FitResult {
1301 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1302 let o = source_rect.size.cast::<f32>();
1303 let ratio = match image_fit {
1304 _ if has_tiling => scale_factor.get(),
1306 ImageFit::Fill => {
1307 return FitResult {
1308 clip_rect: source_rect,
1309 source_to_target_x: target.width / o.width,
1310 source_to_target_y: target.height / o.height,
1311 size: target,
1312 offset: Default::default(),
1313 tiled: None,
1314 };
1315 }
1316 ImageFit::Preserve => scale_factor.get(),
1317 ImageFit::Contain => f32::min(target.width / o.width, target.height / o.height),
1318 ImageFit::Cover => f32::max(target.width / o.width, target.height / o.height),
1319 };
1320
1321 FitResult {
1322 clip_rect: source_rect,
1323 source_to_target_x: ratio,
1324 source_to_target_y: ratio,
1325 size: target,
1326 offset: euclid::Point2D::default(),
1327 tiled: None,
1328 }
1329 .adjust_for_tiling(ratio, alignment, tiling)
1330}
1331
1332pub fn fit9slice(
1334 source_rect: IntSize,
1335 [t, r, b, l]: [u16; 4],
1336 target: euclid::Size2D<f32, PhysicalPx>,
1337 scale_factor: ScaleFactor,
1338 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1339 tiling: (ImageTiling, ImageTiling),
1340) -> impl Iterator<Item = FitResult> {
1341 let fit_to = |clip_rect: euclid::default::Rect<u16>, target: euclid::Rect<f32, PhysicalPx>| {
1342 (!clip_rect.is_empty() && !target.is_empty()).then(|| {
1343 FitResult {
1344 clip_rect: clip_rect.cast(),
1345 source_to_target_x: target.width() / clip_rect.width() as f32,
1346 source_to_target_y: target.height() / clip_rect.height() as f32,
1347 size: target.size,
1348 offset: target.origin,
1349 tiled: None,
1350 }
1351 .adjust_for_tiling(scale_factor.get(), alignment, tiling)
1352 })
1353 };
1354 use euclid::rect;
1355 let sf = |x| scale_factor.get() * x as f32;
1356 let source = source_rect.cast::<u16>();
1357 if t + b > source.height || l + r > source.width {
1358 [None, None, None, None, None, None, None, None, None]
1359 } else {
1360 [
1361 fit_to(rect(0, 0, l, t), rect(0., 0., sf(l), sf(t))),
1362 fit_to(
1363 rect(l, 0, source.width - l - r, t),
1364 rect(sf(l), 0., target.width - sf(l) - sf(r), sf(t)),
1365 ),
1366 fit_to(rect(source.width - r, 0, r, t), rect(target.width - sf(r), 0., sf(r), sf(t))),
1367 fit_to(
1368 rect(0, t, l, source.height - t - b),
1369 rect(0., sf(t), sf(l), target.height - sf(t) - sf(b)),
1370 ),
1371 fit_to(
1372 rect(l, t, source.width - l - r, source.height - t - b),
1373 rect(sf(l), sf(t), target.width - sf(l) - sf(r), target.height - sf(t) - sf(b)),
1374 ),
1375 fit_to(
1376 rect(source.width - r, t, r, source.height - t - b),
1377 rect(target.width - sf(r), sf(t), sf(r), target.height - sf(t) - sf(b)),
1378 ),
1379 fit_to(rect(0, source.height - b, l, b), rect(0., target.height - sf(b), sf(l), sf(b))),
1380 fit_to(
1381 rect(l, source.height - b, source.width - l - r, b),
1382 rect(sf(l), target.height - sf(b), target.width - sf(l) - sf(r), sf(b)),
1383 ),
1384 fit_to(
1385 rect(source.width - r, source.height - b, r, b),
1386 rect(target.width - sf(r), target.height - sf(b), sf(r), sf(b)),
1387 ),
1388 ]
1389 }
1390 .into_iter()
1391 .flatten()
1392}
1393
1394#[cfg(feature = "ffi")]
1395pub(crate) mod ffi {
1396 #![allow(unsafe_code)]
1397
1398 use super::*;
1399
1400 #[cfg(cbindgen)]
1403 #[repr(C)]
1404 struct Rgb8Pixel {
1405 r: u8,
1407 g: u8,
1409 b: u8,
1411 }
1412
1413 #[cfg(cbindgen)]
1416 #[repr(C)]
1417 struct Rgba8Pixel {
1418 r: u8,
1420 g: u8,
1422 b: u8,
1424 a: u8,
1426 }
1427
1428 #[cfg(feature = "image-decoders")]
1429 #[unsafe(no_mangle)]
1430 pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) {
1431 unsafe {
1432 core::ptr::write(
1433 image,
1434 Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or_default(),
1435 )
1436 }
1437 }
1438
1439 #[cfg(feature = "std")]
1440 #[unsafe(no_mangle)]
1441 pub unsafe extern "C" fn slint_image_load_from_embedded_data(
1442 data: Slice<'static, u8>,
1443 format: Slice<'static, u8>,
1444 image: *mut Image,
1445 ) {
1446 unsafe { core::ptr::write(image, super::load_image_from_embedded_data(data, format)) };
1447 }
1448
1449 #[unsafe(no_mangle)]
1450 pub extern "C" fn slint_image_size(image: &Image) -> IntSize {
1451 image.size()
1452 }
1453
1454 #[unsafe(no_mangle)]
1455 pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
1456 match &image.0 {
1457 #[cfg(feature = "std")]
1458 ImageInner::EmbeddedImage {
1459 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1460 ..
1461 } => Some(path),
1462 ImageInner::NineSlice(nine) => match &nine.0 {
1463 #[cfg(feature = "std")]
1464 ImageInner::EmbeddedImage {
1465 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1466 ..
1467 } => Some(path),
1468 _ => None,
1469 },
1470 _ => None,
1471 }
1472 }
1473
1474 #[unsafe(no_mangle)]
1475 pub unsafe extern "C" fn slint_image_from_embedded_textures(
1476 textures: &'static StaticTextures,
1477 image: *mut Image,
1478 ) {
1479 unsafe { core::ptr::write(image, Image::from(ImageInner::StaticTextures(textures))) };
1480 }
1481
1482 #[unsafe(no_mangle)]
1483 pub extern "C" fn slint_image_compare_equal(image1: &Image, image2: &Image) -> bool {
1484 image1.eq(image2)
1485 }
1486
1487 #[unsafe(no_mangle)]
1489 pub extern "C" fn slint_image_set_nine_slice_edges(
1490 image: &mut Image,
1491 top: u16,
1492 right: u16,
1493 bottom: u16,
1494 left: u16,
1495 ) {
1496 image.set_nine_slice_edges(top, right, bottom, left);
1497 }
1498
1499 #[unsafe(no_mangle)]
1500 pub extern "C" fn slint_image_to_rgb8(
1501 image: &Image,
1502 data: &mut SharedVector<Rgb8Pixel>,
1503 width: &mut u32,
1504 height: &mut u32,
1505 ) -> bool {
1506 image.to_rgb8().is_some_and(|pixel_buffer| {
1507 *data = pixel_buffer.data.clone();
1508 *width = pixel_buffer.width();
1509 *height = pixel_buffer.height();
1510 true
1511 })
1512 }
1513
1514 #[unsafe(no_mangle)]
1515 pub extern "C" fn slint_image_to_rgba8(
1516 image: &Image,
1517 data: &mut SharedVector<Rgba8Pixel>,
1518 width: &mut u32,
1519 height: &mut u32,
1520 ) -> bool {
1521 image.to_rgba8().is_some_and(|pixel_buffer| {
1522 *data = pixel_buffer.data.clone();
1523 *width = pixel_buffer.width();
1524 *height = pixel_buffer.height();
1525 true
1526 })
1527 }
1528
1529 #[unsafe(no_mangle)]
1530 pub extern "C" fn slint_image_to_rgba8_premultiplied(
1531 image: &Image,
1532 data: &mut SharedVector<Rgba8Pixel>,
1533 width: &mut u32,
1534 height: &mut u32,
1535 ) -> bool {
1536 image.to_rgba8_premultiplied().is_some_and(|pixel_buffer| {
1537 *data = pixel_buffer.data.clone();
1538 *width = pixel_buffer.width();
1539 *height = pixel_buffer.height();
1540 true
1541 })
1542 }
1543}
1544
1545#[derive(Clone, Debug, PartialEq)]
1553#[non_exhaustive]
1554#[cfg(not(target_arch = "wasm32"))]
1555#[repr(C)]
1556pub struct BorrowedOpenGLTexture {
1557 pub texture_id: core::num::NonZeroU32,
1559 pub size: IntSize,
1561 pub origin: BorrowedOpenGLTextureOrigin,
1563}
1564
1565#[cfg(test)]
1566mod tests {
1567 use crate::graphics::Rgba8Pixel;
1568
1569 use super::Image;
1570
1571 #[test]
1572 fn test_premultiplied_to_rgb_zero_alpha() {
1573 let pixel = Rgba8Pixel::new(5, 10, 15, 0);
1574 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1575 assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1576 }
1577
1578 #[test]
1579 fn test_premultiplied_to_rgb_full_alpha() {
1580 let pixel = Rgba8Pixel::new(5, 10, 15, 255);
1581 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1582 assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 255));
1583 }
1584
1585 #[test]
1586 fn test_premultiplied_to_rgb() {
1587 let pixel = Rgba8Pixel::new(5, 10, 15, 128);
1588 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1589 assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 128));
1590 }
1591
1592 #[test]
1593 fn test_rgb_to_premultiplied_zero_alpha() {
1594 let pixel = Rgba8Pixel::new(10, 20, 30, 0);
1595 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1596 assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1597 }
1598
1599 #[test]
1600 fn test_rgb_to_premultiplied_full_alpha() {
1601 let pixel = Rgba8Pixel::new(10, 20, 30, 255);
1602 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1603 assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 255));
1604 }
1605
1606 #[test]
1607 fn test_rgb_to_premultiplied() {
1608 let pixel = Rgba8Pixel::new(10, 20, 30, 128);
1609 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1610 assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 128));
1611 }
1612}