1use crate::lengths::{PhysicalPx, ScaleFactor};
9use crate::slice::Slice;
10use crate::{SharedString, SharedVector};
11
12use super::{IntRect, IntSize};
13use crate::items::{ImageFit, ImageHorizontalAlignment, ImageTiling, ImageVerticalAlignment};
14
15#[cfg(feature = "image-decoders")]
16pub mod cache;
17#[cfg(target_arch = "wasm32")]
18mod htmlimage;
19#[cfg(feature = "svg")]
20mod svg;
21
22#[allow(missing_docs)]
23#[cfg_attr(not(feature = "ffi"), i_slint_core_macros::remove_extern)]
24#[vtable::vtable]
25#[repr(C)]
26pub struct OpaqueImageVTable {
27 drop_in_place: extern "C" fn(VRefMut<OpaqueImageVTable>) -> Layout,
28 dealloc: extern "C" fn(&OpaqueImageVTable, ptr: *mut u8, layout: Layout),
29 size: extern "C" fn(VRef<OpaqueImageVTable>) -> IntSize,
31 cache_key: extern "C" fn(VRef<OpaqueImageVTable>) -> ImageCacheKey,
33}
34
35#[cfg(feature = "svg")]
36OpaqueImageVTable_static! {
37 pub static PARSED_SVG_VT for svg::ParsedSVG
39}
40
41#[cfg(target_arch = "wasm32")]
42OpaqueImageVTable_static! {
43 pub static HTML_IMAGE_VT for htmlimage::HTMLImage
45}
46
47OpaqueImageVTable_static! {
48 pub static NINE_SLICE_VT for NineSliceImage
50}
51
52#[derive(Debug, Clone)]
62#[repr(C)]
63pub struct SharedPixelBuffer<Pixel> {
64 width: u32,
65 height: u32,
66 pub(crate) data: SharedVector<Pixel>,
67}
68
69impl<Pixel> SharedPixelBuffer<Pixel> {
70 pub fn width(&self) -> u32 {
72 self.width
73 }
74
75 pub fn height(&self) -> u32 {
77 self.height
78 }
79
80 pub fn size(&self) -> IntSize {
82 [self.width, self.height].into()
83 }
84}
85
86impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
87 pub fn make_mut_slice(&mut self) -> &mut [Pixel] {
89 self.data.make_mut_slice()
90 }
91}
92
93impl<Pixel: Clone + rgb::Pod> SharedPixelBuffer<Pixel>
94where
95 [Pixel]: rgb::ComponentBytes<u8>,
96{
97 pub fn as_bytes(&self) -> &[u8] {
99 use rgb::ComponentBytes;
100 self.data.as_slice().as_bytes()
101 }
102
103 pub fn make_mut_bytes(&mut self) -> &mut [u8] {
105 use rgb::ComponentBytes;
106 self.data.make_mut_slice().as_bytes_mut()
107 }
108}
109
110impl<Pixel> SharedPixelBuffer<Pixel> {
111 pub fn as_slice(&self) -> &[Pixel] {
113 self.data.as_slice()
114 }
115}
116
117impl<Pixel: Clone + Default> SharedPixelBuffer<Pixel> {
118 pub fn new(width: u32, height: u32) -> Self {
121 Self {
122 width,
123 height,
124 data: core::iter::repeat(Pixel::default())
125 .take(width as usize * height as usize)
126 .collect(),
127 }
128 }
129}
130
131impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
132 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)]
291pub struct CachedPath {
292 path: SharedString,
293 last_modified: u64,
295}
296
297impl CachedPath {
298 #[cfg(feature = "std")]
299 fn new<P: AsRef<std::path::Path>>(path: P) -> Self {
300 let path_str = SharedString::from(path.as_ref().to_string_lossy().as_ref());
301 let timestamp = std::fs::metadata(path)
302 .and_then(|md| md.modified())
303 .unwrap_or(std::time::UNIX_EPOCH)
304 .duration_since(std::time::UNIX_EPOCH)
305 .map(|t| t.as_secs())
306 .unwrap_or_default();
307 Self { path: path_str, last_modified: timestamp }
308 }
309}
310
311#[derive(PartialEq, Eq, Debug, Hash, Clone)]
314#[repr(u8)]
315pub enum ImageCacheKey {
316 Invalid = 0,
319 Path(CachedPath) = 1,
321 #[cfg(target_arch = "wasm32")]
323 URL(SharedString) = 2,
324 EmbeddedData(usize) = 3,
326}
327
328impl ImageCacheKey {
329 pub fn new(resource: &ImageInner) -> Option<Self> {
332 let key = match resource {
333 ImageInner::None => return None,
334 ImageInner::EmbeddedImage { cache_key, .. } => cache_key.clone(),
335 ImageInner::StaticTextures(textures) => {
336 Self::from_embedded_image_data(textures.data.as_slice())
337 }
338 #[cfg(feature = "svg")]
339 ImageInner::Svg(parsed_svg) => parsed_svg.cache_key(),
340 #[cfg(target_arch = "wasm32")]
341 ImageInner::HTMLImage(htmlimage) => Self::URL(htmlimage.source().into()),
342 ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).cache_key(),
343 #[cfg(not(target_arch = "wasm32"))]
344 ImageInner::BorrowedOpenGLTexture(..) => return None,
345 ImageInner::NineSlice(nine) => vtable::VRc::borrow(nine).cache_key(),
346 #[cfg(feature = "unstable-wgpu-24")]
347 ImageInner::WGPUTexture(..) => return None,
348 };
349 if matches!(key, ImageCacheKey::Invalid) {
350 None
351 } else {
352 Some(key)
353 }
354 }
355
356 pub fn from_embedded_image_data(data: &'static [u8]) -> Self {
358 Self::EmbeddedData(data.as_ptr() as usize)
359 }
360}
361
362pub struct NineSliceImage(pub ImageInner, pub [u16; 4]);
364
365impl NineSliceImage {
366 pub fn image(&self) -> Image {
368 Image(self.0.clone())
369 }
370}
371
372impl OpaqueImage for NineSliceImage {
373 fn size(&self) -> IntSize {
374 self.0.size()
375 }
376 fn cache_key(&self) -> ImageCacheKey {
377 ImageCacheKey::new(&self.0).unwrap_or(ImageCacheKey::Invalid)
378 }
379}
380
381#[cfg(feature = "unstable-wgpu-24")]
383#[derive(Clone, Debug)]
384pub enum WGPUTexture {
385 #[cfg(feature = "unstable-wgpu-24")]
387 WGPU24Texture(wgpu_24::Texture),
388}
389
390#[cfg(feature = "unstable-wgpu-24")]
391impl OpaqueImage for WGPUTexture {
392 fn size(&self) -> IntSize {
393 match self {
394 Self::WGPU24Texture(texture) => {
395 let size = texture.size();
396 (size.width, size.height).into()
397 }
398 }
399 }
400 fn cache_key(&self) -> ImageCacheKey {
401 ImageCacheKey::Invalid
402 }
403}
404
405#[derive(Clone, Debug, Default)]
410#[repr(u8)]
411#[allow(missing_docs)]
412pub enum ImageInner {
413 #[default]
415 None = 0,
416 EmbeddedImage {
417 cache_key: ImageCacheKey,
418 buffer: SharedImageBuffer,
419 } = 1,
420 #[cfg(feature = "svg")]
421 Svg(vtable::VRc<OpaqueImageVTable, svg::ParsedSVG>) = 2,
422 StaticTextures(&'static StaticTextures) = 3,
423 #[cfg(target_arch = "wasm32")]
424 HTMLImage(vtable::VRc<OpaqueImageVTable, htmlimage::HTMLImage>) = 4,
425 BackendStorage(vtable::VRc<OpaqueImageVTable>) = 5,
426 #[cfg(not(target_arch = "wasm32"))]
427 BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6,
428 NineSlice(vtable::VRc<OpaqueImageVTable, NineSliceImage>) = 7,
429 #[cfg(feature = "unstable-wgpu-24")]
430 WGPUTexture(WGPUTexture) = 8,
431}
432
433impl ImageInner {
434 pub fn render_to_buffer(
441 &self,
442 _target_size_for_scalable_source: Option<euclid::Size2D<u32, PhysicalPx>>,
443 ) -> Option<SharedImageBuffer> {
444 match self {
445 ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()),
446 #[cfg(feature = "svg")]
447 ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) {
448 Ok(b) => Some(b),
449 Err(resvg::usvg::Error::InvalidSize) => None,
451 Err(err) => {
452 std::eprintln!("Error rendering SVG: {err}");
453 None
454 }
455 },
456 ImageInner::StaticTextures(ts) => {
457 let mut buffer =
458 SharedPixelBuffer::<Rgba8Pixel>::new(ts.size.width, ts.size.height);
459 let stride = buffer.width() as usize;
460 let slice = buffer.make_mut_slice();
461 for t in ts.textures.iter() {
462 let rect = t.rect.to_usize();
463 for y in 0..rect.height() {
464 let slice = &mut slice[(rect.min_y() + y) * stride..][rect.x_range()];
465 let source = &ts.data[t.index + y * rect.width() * t.format.bpp()..];
466 match t.format {
467 TexturePixelFormat::Rgb => {
468 let mut iter = source.chunks_exact(3).map(|p| Rgba8Pixel {
469 r: p[0],
470 g: p[1],
471 b: p[2],
472 a: 255,
473 });
474 slice.fill_with(|| iter.next().unwrap());
475 }
476 TexturePixelFormat::RgbaPremultiplied => {
477 let mut iter = source.chunks_exact(4).map(|p| Rgba8Pixel {
478 r: p[0],
479 g: p[1],
480 b: p[2],
481 a: p[3],
482 });
483 slice.fill_with(|| iter.next().unwrap());
484 }
485 TexturePixelFormat::Rgba => {
486 let mut iter = source.chunks_exact(4).map(|p| {
487 let a = p[3];
488 Rgba8Pixel {
489 r: (p[0] as u16 * a as u16 / 255) as u8,
490 g: (p[1] as u16 * a as u16 / 255) as u8,
491 b: (p[2] as u16 * a as u16 / 255) as u8,
492 a,
493 }
494 });
495 slice.fill_with(|| iter.next().unwrap());
496 }
497 TexturePixelFormat::AlphaMap => {
498 let col = t.color.to_argb_u8();
499 let mut iter = source.iter().map(|p| {
500 let a = *p as u32 * col.alpha as u32;
501 Rgba8Pixel {
502 r: (col.red as u32 * a / (255 * 255)) as u8,
503 g: (col.green as u32 * a / (255 * 255)) as u8,
504 b: (col.blue as u32 * a / (255 * 255)) as u8,
505 a: (a / 255) as u8,
506 }
507 });
508 slice.fill_with(|| iter.next().unwrap());
509 }
510 TexturePixelFormat::SignedDistanceField => {
511 todo!("converting from a signed distance field to an image")
512 }
513 };
514 }
515 }
516 Some(SharedImageBuffer::RGBA8Premultiplied(buffer))
517 }
518 ImageInner::NineSlice(nine) => nine.0.render_to_buffer(None),
519 _ => None,
520 }
521 }
522
523 pub fn is_svg(&self) -> bool {
525 match self {
526 #[cfg(feature = "svg")]
527 Self::Svg(_) => true,
528 #[cfg(target_arch = "wasm32")]
529 Self::HTMLImage(html_image) => html_image.is_svg(),
530 _ => false,
531 }
532 }
533
534 pub fn size(&self) -> IntSize {
536 match self {
537 ImageInner::None => Default::default(),
538 ImageInner::EmbeddedImage { buffer, .. } => buffer.size(),
539 ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
540 #[cfg(feature = "svg")]
541 ImageInner::Svg(svg) => svg.size(),
542 #[cfg(target_arch = "wasm32")]
543 ImageInner::HTMLImage(htmlimage) => htmlimage.size().unwrap_or_default(),
544 ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size(),
545 #[cfg(not(target_arch = "wasm32"))]
546 ImageInner::BorrowedOpenGLTexture(BorrowedOpenGLTexture { size, .. }) => *size,
547 ImageInner::NineSlice(nine) => nine.0.size(),
548 #[cfg(feature = "unstable-wgpu-24")]
549 ImageInner::WGPUTexture(texture) => texture.size(),
550 }
551 }
552}
553
554impl PartialEq for ImageInner {
555 fn eq(&self, other: &Self) -> bool {
556 match (self, other) {
557 (
558 Self::EmbeddedImage { cache_key: l_cache_key, buffer: l_buffer },
559 Self::EmbeddedImage { cache_key: r_cache_key, buffer: r_buffer },
560 ) => l_cache_key == r_cache_key && l_buffer == r_buffer,
561 #[cfg(feature = "svg")]
562 (Self::Svg(l0), Self::Svg(r0)) => vtable::VRc::ptr_eq(l0, r0),
563 (Self::StaticTextures(l0), Self::StaticTextures(r0)) => l0 == r0,
564 #[cfg(target_arch = "wasm32")]
565 (Self::HTMLImage(l0), Self::HTMLImage(r0)) => vtable::VRc::ptr_eq(l0, r0),
566 (Self::BackendStorage(l0), Self::BackendStorage(r0)) => vtable::VRc::ptr_eq(l0, r0),
567 #[cfg(not(target_arch = "wasm32"))]
568 (Self::BorrowedOpenGLTexture(l0), Self::BorrowedOpenGLTexture(r0)) => l0 == r0,
569 (Self::NineSlice(l), Self::NineSlice(r)) => l.0 == r.0 && l.1 == r.1,
570 _ => false,
571 }
572 }
573}
574
575impl<'a> From<&'a Image> for &'a ImageInner {
576 fn from(other: &'a Image) -> Self {
577 &other.0
578 }
579}
580
581#[derive(Default, Debug, PartialEq)]
583pub struct LoadImageError(());
584
585impl core::fmt::Display for LoadImageError {
586 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
587 f.write_str("The image cannot be loaded")
588 }
589}
590
591#[cfg(feature = "std")]
592impl std::error::Error for LoadImageError {}
593
594#[repr(transparent)]
691#[derive(Default, Clone, Debug, PartialEq, derive_more::From)]
692pub struct Image(pub(crate) ImageInner);
693
694impl Image {
695 #[cfg(feature = "image-decoders")]
696 pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> {
703 self::cache::IMAGE_CACHE.with(|global_cache| {
704 let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into();
705 global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(()))
706 })
707 }
708
709 pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
712 Image(ImageInner::EmbeddedImage {
713 cache_key: ImageCacheKey::Invalid,
714 buffer: SharedImageBuffer::RGB8(buffer),
715 })
716 }
717
718 pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
721 Image(ImageInner::EmbeddedImage {
722 cache_key: ImageCacheKey::Invalid,
723 buffer: SharedImageBuffer::RGBA8(buffer),
724 })
725 }
726
727 pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
733 Image(ImageInner::EmbeddedImage {
734 cache_key: ImageCacheKey::Invalid,
735 buffer: SharedImageBuffer::RGBA8Premultiplied(buffer),
736 })
737 }
738
739 pub fn to_rgb8(&self) -> Option<SharedPixelBuffer<Rgb8Pixel>> {
742 self.0.render_to_buffer(None).and_then(|image| match image {
743 SharedImageBuffer::RGB8(buffer) => Some(buffer),
744 _ => None,
745 })
746 }
747
748 pub fn to_rgba8(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
751 self.0.render_to_buffer(None).map(|image| match image {
752 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
753 width: buffer.width,
754 height: buffer.height,
755 data: buffer.data.into_iter().map(Into::into).collect(),
756 },
757 SharedImageBuffer::RGBA8(buffer) => buffer,
758 SharedImageBuffer::RGBA8Premultiplied(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
759 width: buffer.width,
760 height: buffer.height,
761 data: buffer
762 .data
763 .into_iter()
764 .map(|rgba_premul| {
765 if rgba_premul.a == 0 {
766 Rgba8Pixel::new(0, 0, 0, 0)
767 } else {
768 let af = rgba_premul.a as f32 / 255.0;
769 Rgba8Pixel {
770 r: (rgba_premul.r as f32 * 255. / af) as u8,
771 g: (rgba_premul.g as f32 * 255. / af) as u8,
772 b: (rgba_premul.b as f32 * 255. / af) as u8,
773 a: rgba_premul.a,
774 }
775 }
776 })
777 .collect(),
778 },
779 })
780 }
781
782 pub fn to_rgba8_premultiplied(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
786 self.0.render_to_buffer(None).map(|image| match image {
787 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
788 width: buffer.width,
789 height: buffer.height,
790 data: buffer.data.into_iter().map(Into::into).collect(),
791 },
792 SharedImageBuffer::RGBA8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
793 width: buffer.width,
794 height: buffer.height,
795 data: buffer
796 .data
797 .into_iter()
798 .map(|rgba| {
799 if rgba.a == 255 {
800 rgba
801 } else {
802 let af = rgba.a as f32 / 255.0;
803 Rgba8Pixel {
804 r: (rgba.r as f32 * af / 255.) as u8,
805 g: (rgba.g as f32 * af / 255.) as u8,
806 b: (rgba.b as f32 * af / 255.) as u8,
807 a: rgba.a,
808 }
809 }
810 })
811 .collect(),
812 },
813 SharedImageBuffer::RGBA8Premultiplied(buffer) => buffer,
814 })
815 }
816
817 #[cfg(feature = "unstable-wgpu-24")]
823 pub fn to_wgpu_24_texture(&self) -> Option<wgpu_24::Texture> {
824 match &self.0 {
825 ImageInner::WGPUTexture(WGPUTexture::WGPU24Texture(texture)) => Some(texture.clone()),
826 _ => None,
827 }
828 }
829
830 #[allow(unsafe_code)]
850 #[cfg(not(target_arch = "wasm32"))]
851 #[deprecated(since = "1.2.0", note = "Use BorrowedOpenGLTextureBuilder")]
852 pub unsafe fn from_borrowed_gl_2d_rgba_texture(
853 texture_id: core::num::NonZeroU32,
854 size: IntSize,
855 ) -> Self {
856 BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size).build()
857 }
858
859 #[cfg(feature = "svg")]
861 pub fn load_from_svg_data(buffer: &[u8]) -> Result<Self, LoadImageError> {
862 let cache_key = ImageCacheKey::Invalid;
863 Ok(Image(ImageInner::Svg(vtable::VRc::new(
864 svg::load_from_data(buffer, cache_key).map_err(|_| LoadImageError(()))?,
865 ))))
866 }
867
868 pub fn set_nine_slice_edges(&mut self, top: u16, right: u16, bottom: u16, left: u16) {
874 if top == 0 && left == 0 && right == 0 && bottom == 0 {
875 if let ImageInner::NineSlice(n) = &self.0 {
876 self.0 = n.0.clone();
877 }
878 } else {
879 let array = [top, right, bottom, left];
880 let inner = if let ImageInner::NineSlice(n) = &mut self.0 {
881 n.0.clone()
882 } else {
883 self.0.clone()
884 };
885 self.0 = ImageInner::NineSlice(vtable::VRc::new(NineSliceImage(inner, array)));
886 }
887 }
888
889 pub fn size(&self) -> IntSize {
891 self.0.size()
892 }
893
894 #[cfg(feature = "std")]
895 pub fn path(&self) -> Option<&std::path::Path> {
907 match &self.0 {
908 ImageInner::EmbeddedImage {
909 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
910 ..
911 } => Some(std::path::Path::new(path.as_str())),
912 ImageInner::NineSlice(nine) => match &nine.0 {
913 ImageInner::EmbeddedImage {
914 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
915 ..
916 } => Some(std::path::Path::new(path.as_str())),
917 _ => None,
918 },
919 _ => None,
920 }
921 }
922}
923
924#[derive(Copy, Clone, Debug, PartialEq, Default)]
927#[repr(u8)]
928#[non_exhaustive]
929pub enum BorrowedOpenGLTextureOrigin {
930 #[default]
932 TopLeft,
933 BottomLeft,
936}
937
938#[cfg(not(target_arch = "wasm32"))]
956pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture);
957
958#[cfg(not(target_arch = "wasm32"))]
959impl BorrowedOpenGLTextureBuilder {
960 #[allow(unsafe_code)]
978 pub unsafe fn new_gl_2d_rgba_texture(texture_id: core::num::NonZeroU32, size: IntSize) -> Self {
979 Self(BorrowedOpenGLTexture { texture_id, size, origin: Default::default() })
980 }
981
982 pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self {
984 self.0.origin = origin;
985 self
986 }
987
988 pub fn build(self) -> Image {
990 Image(ImageInner::BorrowedOpenGLTexture(self.0))
991 }
992}
993
994#[cfg(feature = "image-decoders")]
997pub fn load_image_from_embedded_data(data: Slice<'static, u8>, format: Slice<'_, u8>) -> Image {
998 self::cache::IMAGE_CACHE.with(|global_cache| {
999 global_cache.borrow_mut().load_image_from_embedded_data(data, format).unwrap_or_default()
1000 })
1001}
1002
1003#[test]
1004fn test_image_size_from_buffer_without_backend() {
1005 {
1006 assert_eq!(Image::default().size(), Default::default());
1007 assert!(Image::default().to_rgb8().is_none());
1008 assert!(Image::default().to_rgba8().is_none());
1009 assert!(Image::default().to_rgba8_premultiplied().is_none());
1010 }
1011 {
1012 let buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
1013 let image = Image::from_rgb8(buffer.clone());
1014 assert_eq!(image.size(), [320, 200].into());
1015 assert_eq!(image.to_rgb8().as_ref().map(|b| b.as_slice()), Some(buffer.as_slice()));
1016 }
1017}
1018
1019#[cfg(feature = "svg")]
1020#[test]
1021fn test_image_size_from_svg() {
1022 let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
1023 let image = Image::load_from_svg_data(simple_svg.as_bytes()).unwrap();
1024 assert_eq!(image.size(), [320, 200].into());
1025 assert_eq!(image.to_rgba8().unwrap().size(), image.size());
1026}
1027
1028#[cfg(feature = "svg")]
1029#[test]
1030fn test_image_invalid_svg() {
1031 let invalid_svg = r#"AaBbCcDd"#;
1032 let result = Image::load_from_svg_data(invalid_svg.as_bytes());
1033 assert!(result.is_err());
1034}
1035
1036#[derive(Debug)]
1038pub struct FitResult {
1039 pub clip_rect: IntRect,
1041 pub source_to_target_x: f32,
1043 pub source_to_target_y: f32,
1045 pub size: euclid::Size2D<f32, PhysicalPx>,
1047 pub offset: euclid::Point2D<f32, PhysicalPx>,
1049 pub tiled: Option<euclid::default::Point2D<u32>>,
1053}
1054
1055impl FitResult {
1056 fn adjust_for_tiling(
1057 self,
1058 ratio: f32,
1059 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1060 tiling: (ImageTiling, ImageTiling),
1061 ) -> Self {
1062 let mut r = self;
1063 let mut tiled = euclid::Point2D::default();
1064 let target = r.size;
1065 let o = r.clip_rect.size.cast::<f32>();
1066 match tiling.0 {
1067 ImageTiling::None => {
1068 r.size.width = o.width * r.source_to_target_x;
1069 if (o.width as f32) > target.width / r.source_to_target_x {
1070 let diff = (o.width as f32 - target.width / r.source_to_target_x) as i32;
1071 r.clip_rect.size.width -= diff;
1072 r.clip_rect.origin.x += match alignment.0 {
1073 ImageHorizontalAlignment::Center => diff / 2,
1074 ImageHorizontalAlignment::Left => 0,
1075 ImageHorizontalAlignment::Right => diff,
1076 };
1077 r.size.width = target.width;
1078 } else if (o.width as f32) < target.width / r.source_to_target_x {
1079 r.offset.x += match alignment.0 {
1080 ImageHorizontalAlignment::Center => {
1081 (target.width - o.width as f32 * r.source_to_target_x) / 2.
1082 }
1083 ImageHorizontalAlignment::Left => 0.,
1084 ImageHorizontalAlignment::Right => {
1085 target.width - o.width as f32 * r.source_to_target_x
1086 }
1087 };
1088 }
1089 }
1090 ImageTiling::Repeat => {
1091 tiled.x = match alignment.0 {
1092 ImageHorizontalAlignment::Left => 0,
1093 ImageHorizontalAlignment::Center => {
1094 ((o.width - target.width / ratio) / 2.).rem_euclid(o.width) as u32
1095 }
1096 ImageHorizontalAlignment::Right => {
1097 (-target.width / ratio).rem_euclid(o.width) as u32
1098 }
1099 };
1100 r.source_to_target_x = ratio;
1101 }
1102 ImageTiling::Round => {
1103 if target.width / ratio <= o.width * 1.5 {
1104 r.source_to_target_x = target.width / o.width;
1105 } else {
1106 let mut rem = (target.width / ratio).rem_euclid(o.width);
1107 if rem > o.width / 2. {
1108 rem -= o.width;
1109 }
1110 r.source_to_target_x = ratio * target.width / (target.width - rem * ratio);
1111 }
1112 }
1113 }
1114
1115 match tiling.1 {
1116 ImageTiling::None => {
1117 r.size.height = o.height * r.source_to_target_y;
1118 if (o.height as f32) > target.height / r.source_to_target_y {
1119 let diff = (o.height as f32 - target.height / r.source_to_target_y) as i32;
1120 r.clip_rect.size.height -= diff;
1121 r.clip_rect.origin.y += match alignment.1 {
1122 ImageVerticalAlignment::Center => diff / 2,
1123 ImageVerticalAlignment::Top => 0,
1124 ImageVerticalAlignment::Bottom => diff,
1125 };
1126 r.size.height = target.height;
1127 } else if (o.height as f32) < target.height / r.source_to_target_y {
1128 r.offset.y += match alignment.1 {
1129 ImageVerticalAlignment::Center => {
1130 (target.height - o.height as f32 * r.source_to_target_y) / 2.
1131 }
1132 ImageVerticalAlignment::Top => 0.,
1133 ImageVerticalAlignment::Bottom => {
1134 target.height - o.height as f32 * r.source_to_target_y
1135 }
1136 };
1137 }
1138 }
1139 ImageTiling::Repeat => {
1140 tiled.y = match alignment.1 {
1141 ImageVerticalAlignment::Top => 0,
1142 ImageVerticalAlignment::Center => {
1143 ((o.height - target.height / ratio) / 2.).rem_euclid(o.height) as u32
1144 }
1145 ImageVerticalAlignment::Bottom => {
1146 (-target.height / ratio).rem_euclid(o.height) as u32
1147 }
1148 };
1149 r.source_to_target_y = ratio;
1150 }
1151 ImageTiling::Round => {
1152 if target.height / ratio <= o.height * 1.5 {
1153 r.source_to_target_y = target.height / o.height;
1154 } else {
1155 let mut rem = (target.height / ratio).rem_euclid(o.height);
1156 if rem > o.height / 2. {
1157 rem -= o.height;
1158 }
1159 r.source_to_target_y = ratio * target.height / (target.height - rem * ratio);
1160 }
1161 }
1162 }
1163 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1164 r.tiled = has_tiling.then_some(tiled);
1165 r
1166 }
1167}
1168
1169#[cfg(not(feature = "std"))]
1170trait RemEuclid {
1171 fn rem_euclid(self, b: f32) -> f32;
1172}
1173#[cfg(not(feature = "std"))]
1174impl RemEuclid for f32 {
1175 fn rem_euclid(self, b: f32) -> f32 {
1176 return num_traits::Euclid::rem_euclid(&self, &b);
1177 }
1178}
1179
1180pub fn fit(
1182 image_fit: ImageFit,
1183 target: euclid::Size2D<f32, PhysicalPx>,
1184 source_rect: IntRect,
1185 scale_factor: ScaleFactor,
1186 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1187 tiling: (ImageTiling, ImageTiling),
1188) -> FitResult {
1189 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1190 let o = source_rect.size.cast::<f32>();
1191 let ratio = match image_fit {
1192 _ if has_tiling => scale_factor.get(),
1194 ImageFit::Fill => {
1195 return FitResult {
1196 clip_rect: source_rect,
1197 source_to_target_x: target.width / o.width,
1198 source_to_target_y: target.height / o.height,
1199 size: target,
1200 offset: Default::default(),
1201 tiled: None,
1202 }
1203 }
1204 ImageFit::Preserve => scale_factor.get(),
1205 ImageFit::Contain => f32::min(target.width / o.width, target.height / o.height),
1206 ImageFit::Cover => f32::max(target.width / o.width, target.height / o.height),
1207 };
1208
1209 FitResult {
1210 clip_rect: source_rect,
1211 source_to_target_x: ratio,
1212 source_to_target_y: ratio,
1213 size: target,
1214 offset: euclid::Point2D::default(),
1215 tiled: None,
1216 }
1217 .adjust_for_tiling(ratio, alignment, tiling)
1218}
1219
1220pub fn fit9slice(
1222 source_rect: IntSize,
1223 [t, r, b, l]: [u16; 4],
1224 target: euclid::Size2D<f32, PhysicalPx>,
1225 scale_factor: ScaleFactor,
1226 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1227 tiling: (ImageTiling, ImageTiling),
1228) -> impl Iterator<Item = FitResult> {
1229 let fit_to = |clip_rect: euclid::default::Rect<u16>, target: euclid::Rect<f32, PhysicalPx>| {
1230 (!clip_rect.is_empty() && !target.is_empty()).then(|| {
1231 FitResult {
1232 clip_rect: clip_rect.cast(),
1233 source_to_target_x: target.width() / clip_rect.width() as f32,
1234 source_to_target_y: target.height() / clip_rect.height() as f32,
1235 size: target.size,
1236 offset: target.origin,
1237 tiled: None,
1238 }
1239 .adjust_for_tiling(scale_factor.get(), alignment, tiling)
1240 })
1241 };
1242 use euclid::rect;
1243 let sf = |x| scale_factor.get() * x as f32;
1244 let source = source_rect.cast::<u16>();
1245 if t + b > source.height || l + r > source.width {
1246 [None, None, None, None, None, None, None, None, None]
1247 } else {
1248 [
1249 fit_to(rect(0, 0, l, t), rect(0., 0., sf(l), sf(t))),
1250 fit_to(
1251 rect(l, 0, source.width - l - r, t),
1252 rect(sf(l), 0., target.width - sf(l) - sf(r), sf(t)),
1253 ),
1254 fit_to(rect(source.width - r, 0, r, t), rect(target.width - sf(r), 0., sf(r), sf(t))),
1255 fit_to(
1256 rect(0, t, l, source.height - t - b),
1257 rect(0., sf(t), sf(l), target.height - sf(t) - sf(b)),
1258 ),
1259 fit_to(
1260 rect(l, t, source.width - l - r, source.height - t - b),
1261 rect(sf(l), sf(t), target.width - sf(l) - sf(r), target.height - sf(t) - sf(b)),
1262 ),
1263 fit_to(
1264 rect(source.width - r, t, r, source.height - t - b),
1265 rect(target.width - sf(r), sf(t), sf(r), target.height - sf(t) - sf(b)),
1266 ),
1267 fit_to(rect(0, source.height - b, l, b), rect(0., target.height - sf(b), sf(l), sf(b))),
1268 fit_to(
1269 rect(l, source.height - b, source.width - l - r, b),
1270 rect(sf(l), target.height - sf(b), target.width - sf(l) - sf(r), sf(b)),
1271 ),
1272 fit_to(
1273 rect(source.width - r, source.height - b, r, b),
1274 rect(target.width - sf(r), target.height - sf(b), sf(r), sf(b)),
1275 ),
1276 ]
1277 }
1278 .into_iter()
1279 .flatten()
1280}
1281
1282#[cfg(feature = "ffi")]
1283pub(crate) mod ffi {
1284 #![allow(unsafe_code)]
1285
1286 use super::*;
1287
1288 #[cfg(cbindgen)]
1291 #[repr(C)]
1292 struct Rgb8Pixel {
1293 r: u8,
1295 g: u8,
1297 b: u8,
1299 }
1300
1301 #[cfg(cbindgen)]
1304 #[repr(C)]
1305 struct Rgba8Pixel {
1306 r: u8,
1308 g: u8,
1310 b: u8,
1312 a: u8,
1314 }
1315
1316 #[cfg(feature = "image-decoders")]
1317 #[unsafe(no_mangle)]
1318 pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) {
1319 core::ptr::write(
1320 image,
1321 Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or(Image::default()),
1322 )
1323 }
1324
1325 #[cfg(feature = "std")]
1326 #[unsafe(no_mangle)]
1327 pub unsafe extern "C" fn slint_image_load_from_embedded_data(
1328 data: Slice<'static, u8>,
1329 format: Slice<'static, u8>,
1330 image: *mut Image,
1331 ) {
1332 core::ptr::write(image, super::load_image_from_embedded_data(data, format));
1333 }
1334
1335 #[unsafe(no_mangle)]
1336 pub unsafe extern "C" fn slint_image_size(image: &Image) -> IntSize {
1337 image.size()
1338 }
1339
1340 #[unsafe(no_mangle)]
1341 pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
1342 match &image.0 {
1343 ImageInner::EmbeddedImage { cache_key, .. } => match cache_key {
1344 ImageCacheKey::Path(CachedPath { path, .. }) => Some(path),
1345 _ => None,
1346 },
1347 ImageInner::NineSlice(nine) => match &nine.0 {
1348 ImageInner::EmbeddedImage { cache_key, .. } => match cache_key {
1349 ImageCacheKey::Path(CachedPath { path, .. }) => Some(path),
1350 _ => None,
1351 },
1352 _ => None,
1353 },
1354 _ => None,
1355 }
1356 }
1357
1358 #[unsafe(no_mangle)]
1359 pub unsafe extern "C" fn slint_image_from_embedded_textures(
1360 textures: &'static StaticTextures,
1361 image: *mut Image,
1362 ) {
1363 core::ptr::write(image, Image::from(ImageInner::StaticTextures(textures)));
1364 }
1365
1366 #[unsafe(no_mangle)]
1367 pub unsafe extern "C" fn slint_image_compare_equal(image1: &Image, image2: &Image) -> bool {
1368 image1.eq(image2)
1369 }
1370
1371 #[unsafe(no_mangle)]
1373 pub extern "C" fn slint_image_set_nine_slice_edges(
1374 image: &mut Image,
1375 top: u16,
1376 right: u16,
1377 bottom: u16,
1378 left: u16,
1379 ) {
1380 image.set_nine_slice_edges(top, right, bottom, left);
1381 }
1382
1383 #[unsafe(no_mangle)]
1384 pub extern "C" fn slint_image_to_rgb8(
1385 image: &Image,
1386 data: &mut SharedVector<Rgb8Pixel>,
1387 width: &mut u32,
1388 height: &mut u32,
1389 ) -> bool {
1390 image.to_rgb8().is_some_and(|pixel_buffer| {
1391 *data = pixel_buffer.data.clone();
1392 *width = pixel_buffer.width();
1393 *height = pixel_buffer.height();
1394 true
1395 })
1396 }
1397
1398 #[unsafe(no_mangle)]
1399 pub extern "C" fn slint_image_to_rgba8(
1400 image: &Image,
1401 data: &mut SharedVector<Rgba8Pixel>,
1402 width: &mut u32,
1403 height: &mut u32,
1404 ) -> bool {
1405 image.to_rgba8().is_some_and(|pixel_buffer| {
1406 *data = pixel_buffer.data.clone();
1407 *width = pixel_buffer.width();
1408 *height = pixel_buffer.height();
1409 true
1410 })
1411 }
1412
1413 #[unsafe(no_mangle)]
1414 pub extern "C" fn slint_image_to_rgba8_premultiplied(
1415 image: &Image,
1416 data: &mut SharedVector<Rgba8Pixel>,
1417 width: &mut u32,
1418 height: &mut u32,
1419 ) -> bool {
1420 image.to_rgba8_premultiplied().is_some_and(|pixel_buffer| {
1421 *data = pixel_buffer.data.clone();
1422 *width = pixel_buffer.width();
1423 *height = pixel_buffer.height();
1424 true
1425 })
1426 }
1427}
1428
1429#[derive(Clone, Debug, PartialEq)]
1437#[non_exhaustive]
1438#[cfg(not(target_arch = "wasm32"))]
1439#[repr(C)]
1440pub struct BorrowedOpenGLTexture {
1441 pub texture_id: core::num::NonZeroU32,
1443 pub size: IntSize,
1445 pub origin: BorrowedOpenGLTextureOrigin,
1447}