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#[vtable::vtable]
24#[repr(C)]
25pub struct OpaqueImageVTable {
26 drop_in_place: fn(VRefMut<OpaqueImageVTable>) -> Layout,
27 dealloc: fn(&OpaqueImageVTable, ptr: *mut u8, layout: Layout),
28 size: fn(VRef<OpaqueImageVTable>) -> IntSize,
30 cache_key: fn(VRef<OpaqueImageVTable>) -> ImageCacheKey,
32}
33
34#[cfg(feature = "svg")]
35OpaqueImageVTable_static! {
36 pub static PARSED_SVG_VT for svg::ParsedSVG
38}
39
40#[cfg(target_arch = "wasm32")]
41OpaqueImageVTable_static! {
42 pub static HTML_IMAGE_VT for htmlimage::HTMLImage
44}
45
46OpaqueImageVTable_static! {
47 pub static NINE_SLICE_VT for NineSliceImage
49}
50
51#[derive(Debug, Clone)]
61#[repr(C)]
62pub struct SharedPixelBuffer<Pixel> {
63 width: u32,
64 height: u32,
65 pub(crate) data: SharedVector<Pixel>,
66}
67
68impl<Pixel> SharedPixelBuffer<Pixel> {
69 pub fn width(&self) -> u32 {
71 self.width
72 }
73
74 pub fn height(&self) -> u32 {
76 self.height
77 }
78
79 pub fn size(&self) -> IntSize {
81 [self.width, self.height].into()
82 }
83}
84
85impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
86 pub fn make_mut_slice(&mut self) -> &mut [Pixel] {
88 self.data.make_mut_slice()
89 }
90}
91
92impl<Pixel: Clone + rgb::Pod> SharedPixelBuffer<Pixel>
93where
94 [Pixel]: rgb::ComponentBytes<u8>,
95{
96 pub fn as_bytes(&self) -> &[u8] {
98 use rgb::ComponentBytes;
99 self.data.as_slice().as_bytes()
100 }
101
102 pub fn make_mut_bytes(&mut self) -> &mut [u8] {
104 use rgb::ComponentBytes;
105 self.data.make_mut_slice().as_bytes_mut()
106 }
107}
108
109impl<Pixel> SharedPixelBuffer<Pixel> {
110 pub fn as_slice(&self) -> &[Pixel] {
112 self.data.as_slice()
113 }
114}
115
116impl<Pixel: Clone + Default> SharedPixelBuffer<Pixel> {
117 pub fn new(width: u32, height: u32) -> Self {
120 Self {
121 width,
122 height,
123 data: core::iter::repeat(Pixel::default())
124 .take(width as usize * height as usize)
125 .collect(),
126 }
127 }
128}
129
130impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
131 pub fn clone_from_slice<SourcePixelType>(
135 pixel_slice: &[SourcePixelType],
136 width: u32,
137 height: u32,
138 ) -> Self
139 where
140 [SourcePixelType]: rgb::AsPixels<Pixel>,
141 {
142 use rgb::AsPixels;
143 Self { width, height, data: pixel_slice.as_pixels().into() }
144 }
145}
146
147pub type Rgb8Pixel = rgb::RGB8;
150pub type Rgba8Pixel = rgb::RGBA8;
153
154#[derive(Clone, Debug)]
159#[repr(C)]
160pub enum SharedImageBuffer {
162 RGB8(SharedPixelBuffer<Rgb8Pixel>),
165 RGBA8(SharedPixelBuffer<Rgba8Pixel>),
168 RGBA8Premultiplied(SharedPixelBuffer<Rgba8Pixel>),
175}
176
177impl SharedImageBuffer {
178 #[inline]
180 pub fn width(&self) -> u32 {
181 match self {
182 Self::RGB8(buffer) => buffer.width(),
183 Self::RGBA8(buffer) => buffer.width(),
184 Self::RGBA8Premultiplied(buffer) => buffer.width(),
185 }
186 }
187
188 #[inline]
190 pub fn height(&self) -> u32 {
191 match self {
192 Self::RGB8(buffer) => buffer.height(),
193 Self::RGBA8(buffer) => buffer.height(),
194 Self::RGBA8Premultiplied(buffer) => buffer.height(),
195 }
196 }
197
198 #[inline]
200 pub fn size(&self) -> IntSize {
201 match self {
202 Self::RGB8(buffer) => buffer.size(),
203 Self::RGBA8(buffer) => buffer.size(),
204 Self::RGBA8Premultiplied(buffer) => buffer.size(),
205 }
206 }
207}
208
209impl PartialEq for SharedImageBuffer {
210 fn eq(&self, other: &Self) -> bool {
211 match self {
212 Self::RGB8(lhs_buffer) => {
213 matches!(other, Self::RGB8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
214 }
215 Self::RGBA8(lhs_buffer) => {
216 matches!(other, Self::RGBA8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
217 }
218 Self::RGBA8Premultiplied(lhs_buffer) => {
219 matches!(other, Self::RGBA8Premultiplied(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
220 }
221 }
222 }
223}
224
225#[repr(u8)]
226#[derive(Clone, PartialEq, Debug, Copy)]
227pub enum PixelFormat {
229 Rgb,
231 Rgba,
233 RgbaPremultiplied,
235 AlphaMap,
237 SignedDistanceField,
242}
243
244impl PixelFormat {
245 pub fn bpp(self) -> usize {
247 match self {
248 PixelFormat::Rgb => 3,
249 PixelFormat::Rgba => 4,
250 PixelFormat::RgbaPremultiplied => 4,
251 PixelFormat::AlphaMap => 1,
252 PixelFormat::SignedDistanceField => 1,
253 }
254 }
255}
256
257#[repr(C)]
258#[derive(Clone, PartialEq, Debug)]
259pub struct StaticTexture {
261 pub rect: IntRect,
263 pub format: PixelFormat,
265 pub color: crate::Color,
267 pub index: usize,
269}
270
271#[repr(C)]
272#[derive(Clone, PartialEq, Debug)]
273pub 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 };
347 if matches!(key, ImageCacheKey::Invalid) {
348 None
349 } else {
350 Some(key)
351 }
352 }
353
354 pub fn from_embedded_image_data(data: &'static [u8]) -> Self {
356 Self::EmbeddedData(data.as_ptr() as usize)
357 }
358}
359
360pub struct NineSliceImage(pub ImageInner, pub [u16; 4]);
362
363impl NineSliceImage {
364 pub fn image(&self) -> Image {
366 Image(self.0.clone())
367 }
368}
369
370impl OpaqueImage for NineSliceImage {
371 fn size(&self) -> IntSize {
372 self.0.size()
373 }
374 fn cache_key(&self) -> ImageCacheKey {
375 ImageCacheKey::new(&self.0).unwrap_or(ImageCacheKey::Invalid)
376 }
377}
378
379#[derive(Clone, Debug, Default)]
384#[repr(u8)]
385#[allow(missing_docs)]
386pub enum ImageInner {
387 #[default]
389 None = 0,
390 EmbeddedImage {
391 cache_key: ImageCacheKey,
392 buffer: SharedImageBuffer,
393 } = 1,
394 #[cfg(feature = "svg")]
395 Svg(vtable::VRc<OpaqueImageVTable, svg::ParsedSVG>) = 2,
396 StaticTextures(&'static StaticTextures) = 3,
397 #[cfg(target_arch = "wasm32")]
398 HTMLImage(vtable::VRc<OpaqueImageVTable, htmlimage::HTMLImage>) = 4,
399 BackendStorage(vtable::VRc<OpaqueImageVTable>) = 5,
400 #[cfg(not(target_arch = "wasm32"))]
401 BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6,
402 NineSlice(vtable::VRc<OpaqueImageVTable, NineSliceImage>) = 7,
403}
404
405impl ImageInner {
406 pub fn render_to_buffer(
413 &self,
414 _target_size_for_scalable_source: Option<euclid::Size2D<u32, PhysicalPx>>,
415 ) -> Option<SharedImageBuffer> {
416 match self {
417 ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()),
418 #[cfg(feature = "svg")]
419 ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) {
420 Ok(b) => Some(b),
421 Err(resvg::usvg::Error::InvalidSize) => None,
423 Err(err) => {
424 eprintln!("Error rendering SVG: {err}");
425 None
426 }
427 },
428 ImageInner::StaticTextures(ts) => {
429 let mut buffer =
430 SharedPixelBuffer::<Rgba8Pixel>::new(ts.size.width, ts.size.height);
431 let stride = buffer.width() as usize;
432 let slice = buffer.make_mut_slice();
433 for t in ts.textures.iter() {
434 let rect = t.rect.to_usize();
435 for y in 0..rect.height() {
436 let slice = &mut slice[(rect.min_y() + y) * stride..][rect.x_range()];
437 let source = &ts.data[t.index + y * rect.width() * t.format.bpp()..];
438 match t.format {
439 PixelFormat::Rgb => {
440 let mut iter = source.chunks_exact(3).map(|p| Rgba8Pixel {
441 r: p[0],
442 g: p[1],
443 b: p[2],
444 a: 255,
445 });
446 slice.fill_with(|| iter.next().unwrap());
447 }
448 PixelFormat::RgbaPremultiplied => {
449 let mut iter = source.chunks_exact(4).map(|p| Rgba8Pixel {
450 r: p[0],
451 g: p[1],
452 b: p[2],
453 a: p[3],
454 });
455 slice.fill_with(|| iter.next().unwrap());
456 }
457 PixelFormat::Rgba => {
458 let mut iter = source.chunks_exact(4).map(|p| {
459 let a = p[3];
460 Rgba8Pixel {
461 r: (p[0] as u16 * a as u16 / 255) as u8,
462 g: (p[1] as u16 * a as u16 / 255) as u8,
463 b: (p[2] as u16 * a as u16 / 255) as u8,
464 a,
465 }
466 });
467 slice.fill_with(|| iter.next().unwrap());
468 }
469 PixelFormat::AlphaMap => {
470 let col = t.color.to_argb_u8();
471 let mut iter = source.iter().map(|p| {
472 let a = *p as u32 * col.alpha as u32;
473 Rgba8Pixel {
474 r: (col.red as u32 * a / (255 * 255)) as u8,
475 g: (col.green as u32 * a / (255 * 255)) as u8,
476 b: (col.blue as u32 * a / (255 * 255)) as u8,
477 a: (a / 255) as u8,
478 }
479 });
480 slice.fill_with(|| iter.next().unwrap());
481 }
482 PixelFormat::SignedDistanceField => {
483 todo!("converting from a signed distance field to an image")
484 }
485 };
486 }
487 }
488 Some(SharedImageBuffer::RGBA8Premultiplied(buffer))
489 }
490 ImageInner::NineSlice(nine) => nine.0.render_to_buffer(None),
491 _ => None,
492 }
493 }
494
495 pub fn is_svg(&self) -> bool {
497 match self {
498 #[cfg(feature = "svg")]
499 Self::Svg(_) => true,
500 #[cfg(target_arch = "wasm32")]
501 Self::HTMLImage(html_image) => html_image.is_svg(),
502 _ => false,
503 }
504 }
505
506 pub fn size(&self) -> IntSize {
508 match self {
509 ImageInner::None => Default::default(),
510 ImageInner::EmbeddedImage { buffer, .. } => buffer.size(),
511 ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
512 #[cfg(feature = "svg")]
513 ImageInner::Svg(svg) => svg.size(),
514 #[cfg(target_arch = "wasm32")]
515 ImageInner::HTMLImage(htmlimage) => htmlimage.size().unwrap_or_default(),
516 ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size(),
517 #[cfg(not(target_arch = "wasm32"))]
518 ImageInner::BorrowedOpenGLTexture(BorrowedOpenGLTexture { size, .. }) => *size,
519 ImageInner::NineSlice(nine) => nine.0.size(),
520 }
521 }
522}
523
524impl PartialEq for ImageInner {
525 fn eq(&self, other: &Self) -> bool {
526 match (self, other) {
527 (
528 Self::EmbeddedImage { cache_key: l_cache_key, buffer: l_buffer },
529 Self::EmbeddedImage { cache_key: r_cache_key, buffer: r_buffer },
530 ) => l_cache_key == r_cache_key && l_buffer == r_buffer,
531 #[cfg(feature = "svg")]
532 (Self::Svg(l0), Self::Svg(r0)) => vtable::VRc::ptr_eq(l0, r0),
533 (Self::StaticTextures(l0), Self::StaticTextures(r0)) => l0 == r0,
534 #[cfg(target_arch = "wasm32")]
535 (Self::HTMLImage(l0), Self::HTMLImage(r0)) => vtable::VRc::ptr_eq(l0, r0),
536 (Self::BackendStorage(l0), Self::BackendStorage(r0)) => vtable::VRc::ptr_eq(l0, r0),
537 #[cfg(not(target_arch = "wasm32"))]
538 (Self::BorrowedOpenGLTexture(l0), Self::BorrowedOpenGLTexture(r0)) => l0 == r0,
539 (Self::NineSlice(l), Self::NineSlice(r)) => l.0 == r.0 && l.1 == r.1,
540 _ => false,
541 }
542 }
543}
544
545impl<'a> From<&'a Image> for &'a ImageInner {
546 fn from(other: &'a Image) -> Self {
547 &other.0
548 }
549}
550
551#[derive(Default, Debug, PartialEq)]
553pub struct LoadImageError(());
554
555impl core::fmt::Display for LoadImageError {
556 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
557 f.write_str("The image cannot be loaded")
558 }
559}
560
561#[cfg(feature = "std")]
562impl std::error::Error for LoadImageError {}
563
564#[repr(transparent)]
661#[derive(Default, Clone, Debug, PartialEq, derive_more::From)]
662pub struct Image(ImageInner);
663
664impl Image {
665 #[cfg(feature = "image-decoders")]
666 pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> {
668 self::cache::IMAGE_CACHE.with(|global_cache| {
669 let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into();
670 global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(()))
671 })
672 }
673
674 pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
677 Image(ImageInner::EmbeddedImage {
678 cache_key: ImageCacheKey::Invalid,
679 buffer: SharedImageBuffer::RGB8(buffer),
680 })
681 }
682
683 pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
686 Image(ImageInner::EmbeddedImage {
687 cache_key: ImageCacheKey::Invalid,
688 buffer: SharedImageBuffer::RGBA8(buffer),
689 })
690 }
691
692 pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
698 Image(ImageInner::EmbeddedImage {
699 cache_key: ImageCacheKey::Invalid,
700 buffer: SharedImageBuffer::RGBA8Premultiplied(buffer),
701 })
702 }
703
704 pub fn to_rgb8(&self) -> Option<SharedPixelBuffer<Rgb8Pixel>> {
707 self.0.render_to_buffer(None).and_then(|image| match image {
708 SharedImageBuffer::RGB8(buffer) => Some(buffer),
709 _ => None,
710 })
711 }
712
713 pub fn to_rgba8(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
716 self.0.render_to_buffer(None).map(|image| match image {
717 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
718 width: buffer.width,
719 height: buffer.height,
720 data: buffer.data.into_iter().map(Into::into).collect(),
721 },
722 SharedImageBuffer::RGBA8(buffer) => buffer,
723 SharedImageBuffer::RGBA8Premultiplied(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
724 width: buffer.width,
725 height: buffer.height,
726 data: buffer
727 .data
728 .into_iter()
729 .map(|rgba_premul| {
730 if rgba_premul.a == 0 {
731 Rgba8Pixel::new(0, 0, 0, 0)
732 } else {
733 let af = rgba_premul.a as f32 / 255.0;
734 Rgba8Pixel {
735 r: (rgba_premul.r as f32 * 255. / af) as u8,
736 g: (rgba_premul.g as f32 * 255. / af) as u8,
737 b: (rgba_premul.b as f32 * 255. / af) as u8,
738 a: rgba_premul.a,
739 }
740 }
741 })
742 .collect(),
743 },
744 })
745 }
746
747 pub fn to_rgba8_premultiplied(&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) => SharedPixelBuffer::<Rgba8Pixel> {
758 width: buffer.width,
759 height: buffer.height,
760 data: buffer
761 .data
762 .into_iter()
763 .map(|rgba| {
764 if rgba.a == 255 {
765 rgba
766 } else {
767 let af = rgba.a as f32 / 255.0;
768 Rgba8Pixel {
769 r: (rgba.r as f32 * af / 255.) as u8,
770 g: (rgba.g as f32 * af / 255.) as u8,
771 b: (rgba.b as f32 * af / 255.) as u8,
772 a: rgba.a,
773 }
774 }
775 })
776 .collect(),
777 },
778 SharedImageBuffer::RGBA8Premultiplied(buffer) => buffer,
779 })
780 }
781
782 #[allow(unsafe_code)]
802 #[cfg(not(target_arch = "wasm32"))]
803 #[deprecated(since = "1.2.0", note = "Use BorrowedOpenGLTextureBuilder")]
804 pub unsafe fn from_borrowed_gl_2d_rgba_texture(
805 texture_id: core::num::NonZeroU32,
806 size: IntSize,
807 ) -> Self {
808 BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size).build()
809 }
810
811 #[cfg(feature = "svg")]
813 pub fn load_from_svg_data(buffer: &[u8]) -> Result<Self, LoadImageError> {
814 let cache_key = ImageCacheKey::Invalid;
815 Ok(Image(ImageInner::Svg(vtable::VRc::new(
816 svg::load_from_data(buffer, cache_key).map_err(|_| LoadImageError(()))?,
817 ))))
818 }
819
820 pub fn set_nine_slice_edges(&mut self, top: u16, right: u16, bottom: u16, left: u16) {
826 if top == 0 && left == 0 && right == 0 && bottom == 0 {
827 if let ImageInner::NineSlice(n) = &self.0 {
828 self.0 = n.0.clone();
829 }
830 } else {
831 let array = [top, right, bottom, left];
832 let inner = if let ImageInner::NineSlice(n) = &mut self.0 {
833 n.0.clone()
834 } else {
835 self.0.clone()
836 };
837 self.0 = ImageInner::NineSlice(vtable::VRc::new(NineSliceImage(inner, array)));
838 }
839 }
840
841 pub fn size(&self) -> IntSize {
843 self.0.size()
844 }
845
846 #[cfg(feature = "std")]
847 pub fn path(&self) -> Option<&std::path::Path> {
859 match &self.0 {
860 ImageInner::EmbeddedImage {
861 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
862 ..
863 } => Some(std::path::Path::new(path.as_str())),
864 ImageInner::NineSlice(nine) => match &nine.0 {
865 ImageInner::EmbeddedImage {
866 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
867 ..
868 } => Some(std::path::Path::new(path.as_str())),
869 _ => None,
870 },
871 _ => None,
872 }
873 }
874}
875
876#[derive(Copy, Clone, Debug, PartialEq, Default)]
879#[repr(u8)]
880#[non_exhaustive]
881pub enum BorrowedOpenGLTextureOrigin {
882 #[default]
884 TopLeft,
885 BottomLeft,
888}
889
890#[cfg(not(target_arch = "wasm32"))]
908pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture);
909
910#[cfg(not(target_arch = "wasm32"))]
911impl BorrowedOpenGLTextureBuilder {
912 #[allow(unsafe_code)]
930 pub unsafe fn new_gl_2d_rgba_texture(texture_id: core::num::NonZeroU32, size: IntSize) -> Self {
931 Self(BorrowedOpenGLTexture { texture_id, size, origin: Default::default() })
932 }
933
934 pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self {
936 self.0.origin = origin;
937 self
938 }
939
940 pub fn build(self) -> Image {
942 Image(ImageInner::BorrowedOpenGLTexture(self.0))
943 }
944}
945
946#[cfg(feature = "image-decoders")]
949pub fn load_image_from_embedded_data(data: Slice<'static, u8>, format: Slice<'_, u8>) -> Image {
950 self::cache::IMAGE_CACHE.with(|global_cache| {
951 global_cache.borrow_mut().load_image_from_embedded_data(data, format).unwrap_or_default()
952 })
953}
954
955#[test]
956fn test_image_size_from_buffer_without_backend() {
957 {
958 assert_eq!(Image::default().size(), Default::default());
959 assert!(Image::default().to_rgb8().is_none());
960 assert!(Image::default().to_rgba8().is_none());
961 assert!(Image::default().to_rgba8_premultiplied().is_none());
962 }
963 {
964 let buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
965 let image = Image::from_rgb8(buffer.clone());
966 assert_eq!(image.size(), [320, 200].into());
967 assert_eq!(image.to_rgb8().as_ref().map(|b| b.as_slice()), Some(buffer.as_slice()));
968 }
969}
970
971#[cfg(feature = "svg")]
972#[test]
973fn test_image_size_from_svg() {
974 let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
975 let image = Image::load_from_svg_data(simple_svg.as_bytes()).unwrap();
976 assert_eq!(image.size(), [320, 200].into());
977 assert_eq!(image.to_rgba8().unwrap().size(), image.size());
978}
979
980#[cfg(feature = "svg")]
981#[test]
982fn test_image_invalid_svg() {
983 let invalid_svg = r#"AaBbCcDd"#;
984 let result = Image::load_from_svg_data(invalid_svg.as_bytes());
985 assert!(result.is_err());
986}
987
988#[derive(Debug)]
990pub struct FitResult {
991 pub clip_rect: IntRect,
993 pub source_to_target_x: f32,
995 pub source_to_target_y: f32,
997 pub size: euclid::Size2D<f32, PhysicalPx>,
999 pub offset: euclid::Point2D<f32, PhysicalPx>,
1001 pub tiled: Option<euclid::default::Point2D<u32>>,
1005}
1006
1007impl FitResult {
1008 fn adjust_for_tiling(
1009 self,
1010 ratio: f32,
1011 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1012 tiling: (ImageTiling, ImageTiling),
1013 ) -> Self {
1014 let mut r = self;
1015 let mut tiled = euclid::Point2D::default();
1016 let target = r.size;
1017 let o = r.clip_rect.size.cast::<f32>();
1018 match tiling.0 {
1019 ImageTiling::None => {
1020 r.size.width = o.width * r.source_to_target_x;
1021 if (o.width as f32) > target.width / r.source_to_target_x {
1022 let diff = (o.width as f32 - target.width / r.source_to_target_x) as i32;
1023 r.clip_rect.size.width -= diff;
1024 r.clip_rect.origin.x += match alignment.0 {
1025 ImageHorizontalAlignment::Center => diff / 2,
1026 ImageHorizontalAlignment::Left => 0,
1027 ImageHorizontalAlignment::Right => diff,
1028 };
1029 r.size.width = target.width;
1030 } else if (o.width as f32) < target.width / r.source_to_target_x {
1031 r.offset.x += match alignment.0 {
1032 ImageHorizontalAlignment::Center => {
1033 (target.width - o.width as f32 * r.source_to_target_x) / 2.
1034 }
1035 ImageHorizontalAlignment::Left => 0.,
1036 ImageHorizontalAlignment::Right => {
1037 target.width - o.width as f32 * r.source_to_target_x
1038 }
1039 };
1040 }
1041 }
1042 ImageTiling::Repeat => {
1043 tiled.x = match alignment.0 {
1044 ImageHorizontalAlignment::Left => 0,
1045 ImageHorizontalAlignment::Center => {
1046 ((o.width - target.width / ratio) / 2.).rem_euclid(o.width) as u32
1047 }
1048 ImageHorizontalAlignment::Right => {
1049 (-target.width / ratio).rem_euclid(o.width) as u32
1050 }
1051 };
1052 r.source_to_target_x = ratio;
1053 }
1054 ImageTiling::Round => {
1055 if target.width / ratio <= o.width * 1.5 {
1056 r.source_to_target_x = target.width / o.width;
1057 } else {
1058 let mut rem = (target.width / ratio).rem_euclid(o.width);
1059 if rem > o.width / 2. {
1060 rem -= o.width;
1061 }
1062 r.source_to_target_x = ratio * target.width / (target.width - rem * ratio);
1063 }
1064 }
1065 }
1066
1067 match tiling.1 {
1068 ImageTiling::None => {
1069 r.size.height = o.height * r.source_to_target_y;
1070 if (o.height as f32) > target.height / r.source_to_target_y {
1071 let diff = (o.height as f32 - target.height / r.source_to_target_y) as i32;
1072 r.clip_rect.size.height -= diff;
1073 r.clip_rect.origin.y += match alignment.1 {
1074 ImageVerticalAlignment::Center => diff / 2,
1075 ImageVerticalAlignment::Top => 0,
1076 ImageVerticalAlignment::Bottom => diff,
1077 };
1078 r.size.height = target.height;
1079 } else if (o.height as f32) < target.height / r.source_to_target_y {
1080 r.offset.y += match alignment.1 {
1081 ImageVerticalAlignment::Center => {
1082 (target.height - o.height as f32 * r.source_to_target_y) / 2.
1083 }
1084 ImageVerticalAlignment::Top => 0.,
1085 ImageVerticalAlignment::Bottom => {
1086 target.height - o.height as f32 * r.source_to_target_y
1087 }
1088 };
1089 }
1090 }
1091 ImageTiling::Repeat => {
1092 tiled.y = match alignment.1 {
1093 ImageVerticalAlignment::Top => 0,
1094 ImageVerticalAlignment::Center => {
1095 ((o.height - target.height / ratio) / 2.).rem_euclid(o.height) as u32
1096 }
1097 ImageVerticalAlignment::Bottom => {
1098 (-target.height / ratio).rem_euclid(o.height) as u32
1099 }
1100 };
1101 r.source_to_target_y = ratio;
1102 }
1103 ImageTiling::Round => {
1104 if target.height / ratio <= o.height * 1.5 {
1105 r.source_to_target_y = target.height / o.height;
1106 } else {
1107 let mut rem = (target.height / ratio).rem_euclid(o.height);
1108 if rem > o.height / 2. {
1109 rem -= o.height;
1110 }
1111 r.source_to_target_y = ratio * target.height / (target.height - rem * ratio);
1112 }
1113 }
1114 }
1115 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1116 r.tiled = has_tiling.then_some(tiled);
1117 r
1118 }
1119}
1120
1121#[cfg(not(feature = "std"))]
1122trait RemEuclid {
1123 fn rem_euclid(self, b: f32) -> f32;
1124}
1125#[cfg(not(feature = "std"))]
1126impl RemEuclid for f32 {
1127 fn rem_euclid(self, b: f32) -> f32 {
1128 return num_traits::Euclid::rem_euclid(&self, &b);
1129 }
1130}
1131
1132pub fn fit(
1134 image_fit: ImageFit,
1135 target: euclid::Size2D<f32, PhysicalPx>,
1136 source_rect: IntRect,
1137 scale_factor: ScaleFactor,
1138 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1139 tiling: (ImageTiling, ImageTiling),
1140) -> FitResult {
1141 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1142 let o = source_rect.size.cast::<f32>();
1143 let ratio = match image_fit {
1144 _ if has_tiling => scale_factor.get(),
1146 ImageFit::Fill => {
1147 return FitResult {
1148 clip_rect: source_rect,
1149 source_to_target_x: target.width / o.width,
1150 source_to_target_y: target.height / o.height,
1151 size: target,
1152 offset: Default::default(),
1153 tiled: None,
1154 }
1155 }
1156 ImageFit::Preserve => scale_factor.get(),
1157 ImageFit::Contain => f32::min(target.width / o.width, target.height / o.height),
1158 ImageFit::Cover => f32::max(target.width / o.width, target.height / o.height),
1159 };
1160
1161 FitResult {
1162 clip_rect: source_rect,
1163 source_to_target_x: ratio,
1164 source_to_target_y: ratio,
1165 size: target,
1166 offset: euclid::Point2D::default(),
1167 tiled: None,
1168 }
1169 .adjust_for_tiling(ratio, alignment, tiling)
1170}
1171
1172pub fn fit9slice(
1174 source_rect: IntSize,
1175 [t, r, b, l]: [u16; 4],
1176 target: euclid::Size2D<f32, PhysicalPx>,
1177 scale_factor: ScaleFactor,
1178 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1179 tiling: (ImageTiling, ImageTiling),
1180) -> impl Iterator<Item = FitResult> {
1181 let fit_to = |clip_rect: euclid::default::Rect<u16>, target: euclid::Rect<f32, PhysicalPx>| {
1182 (!clip_rect.is_empty() && !target.is_empty()).then(|| {
1183 FitResult {
1184 clip_rect: clip_rect.cast(),
1185 source_to_target_x: target.width() / clip_rect.width() as f32,
1186 source_to_target_y: target.height() / clip_rect.height() as f32,
1187 size: target.size,
1188 offset: target.origin,
1189 tiled: None,
1190 }
1191 .adjust_for_tiling(scale_factor.get(), alignment, tiling)
1192 })
1193 };
1194 use euclid::rect;
1195 let sf = |x| scale_factor.get() * x as f32;
1196 let source = source_rect.cast::<u16>();
1197 if t + b > source.height || l + r > source.width {
1198 [None, None, None, None, None, None, None, None, None]
1199 } else {
1200 [
1201 fit_to(rect(0, 0, l, t), rect(0., 0., sf(l), sf(t))),
1202 fit_to(
1203 rect(l, 0, source.width - l - r, t),
1204 rect(sf(l), 0., target.width - sf(l) - sf(r), sf(t)),
1205 ),
1206 fit_to(rect(source.width - r, 0, r, t), rect(target.width - sf(r), 0., sf(r), sf(t))),
1207 fit_to(
1208 rect(0, t, l, source.height - t - b),
1209 rect(0., sf(t), sf(l), target.height - sf(t) - sf(b)),
1210 ),
1211 fit_to(
1212 rect(l, t, source.width - l - r, source.height - t - b),
1213 rect(sf(l), sf(t), target.width - sf(l) - sf(r), target.height - sf(t) - sf(b)),
1214 ),
1215 fit_to(
1216 rect(source.width - r, t, r, source.height - t - b),
1217 rect(target.width - sf(r), sf(t), sf(r), target.height - sf(t) - sf(b)),
1218 ),
1219 fit_to(rect(0, source.height - b, l, b), rect(0., target.height - sf(b), sf(l), sf(b))),
1220 fit_to(
1221 rect(l, source.height - b, source.width - l - r, b),
1222 rect(sf(l), target.height - sf(b), target.width - sf(l) - sf(r), sf(b)),
1223 ),
1224 fit_to(
1225 rect(source.width - r, source.height - b, r, b),
1226 rect(target.width - sf(r), target.height - sf(b), sf(r), sf(b)),
1227 ),
1228 ]
1229 }
1230 .into_iter()
1231 .flatten()
1232}
1233
1234#[cfg(feature = "ffi")]
1235pub(crate) mod ffi {
1236 #![allow(unsafe_code)]
1237
1238 use super::*;
1239
1240 #[cfg(cbindgen)]
1243 #[repr(C)]
1244 struct Rgb8Pixel {
1245 r: u8,
1247 g: u8,
1249 b: u8,
1251 }
1252
1253 #[cfg(cbindgen)]
1256 #[repr(C)]
1257 struct Rgba8Pixel {
1258 r: u8,
1260 g: u8,
1262 b: u8,
1264 a: u8,
1266 }
1267
1268 #[cfg(feature = "image-decoders")]
1269 #[no_mangle]
1270 pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) {
1271 core::ptr::write(
1272 image,
1273 Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or(Image::default()),
1274 )
1275 }
1276
1277 #[cfg(feature = "std")]
1278 #[no_mangle]
1279 pub unsafe extern "C" fn slint_image_load_from_embedded_data(
1280 data: Slice<'static, u8>,
1281 format: Slice<'static, u8>,
1282 image: *mut Image,
1283 ) {
1284 core::ptr::write(image, super::load_image_from_embedded_data(data, format));
1285 }
1286
1287 #[no_mangle]
1288 pub unsafe extern "C" fn slint_image_size(image: &Image) -> IntSize {
1289 image.size()
1290 }
1291
1292 #[no_mangle]
1293 pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
1294 match &image.0 {
1295 ImageInner::EmbeddedImage { cache_key, .. } => match cache_key {
1296 ImageCacheKey::Path(CachedPath { path, .. }) => Some(path),
1297 _ => None,
1298 },
1299 ImageInner::NineSlice(nine) => match &nine.0 {
1300 ImageInner::EmbeddedImage { cache_key, .. } => match cache_key {
1301 ImageCacheKey::Path(CachedPath { path, .. }) => Some(path),
1302 _ => None,
1303 },
1304 _ => None,
1305 },
1306 _ => None,
1307 }
1308 }
1309
1310 #[no_mangle]
1311 pub unsafe extern "C" fn slint_image_from_embedded_textures(
1312 textures: &'static StaticTextures,
1313 image: *mut Image,
1314 ) {
1315 core::ptr::write(image, Image::from(ImageInner::StaticTextures(textures)));
1316 }
1317
1318 #[no_mangle]
1319 pub unsafe extern "C" fn slint_image_compare_equal(image1: &Image, image2: &Image) -> bool {
1320 return image1.eq(image2);
1321 }
1322
1323 #[no_mangle]
1325 pub extern "C" fn slint_image_set_nine_slice_edges(
1326 image: &mut Image,
1327 top: u16,
1328 right: u16,
1329 bottom: u16,
1330 left: u16,
1331 ) {
1332 image.set_nine_slice_edges(top, right, bottom, left);
1333 }
1334
1335 #[no_mangle]
1336 pub extern "C" fn slint_image_to_rgb8(
1337 image: &Image,
1338 data: &mut SharedVector<Rgb8Pixel>,
1339 width: &mut u32,
1340 height: &mut u32,
1341 ) -> bool {
1342 image.to_rgb8().map_or(false, |pixel_buffer| {
1343 *data = pixel_buffer.data.clone();
1344 *width = pixel_buffer.width();
1345 *height = pixel_buffer.height();
1346 true
1347 })
1348 }
1349
1350 #[no_mangle]
1351 pub extern "C" fn slint_image_to_rgba8(
1352 image: &Image,
1353 data: &mut SharedVector<Rgba8Pixel>,
1354 width: &mut u32,
1355 height: &mut u32,
1356 ) -> bool {
1357 image.to_rgba8().map_or(false, |pixel_buffer| {
1358 *data = pixel_buffer.data.clone();
1359 *width = pixel_buffer.width();
1360 *height = pixel_buffer.height();
1361 true
1362 })
1363 }
1364
1365 #[no_mangle]
1366 pub extern "C" fn slint_image_to_rgba8_premultiplied(
1367 image: &Image,
1368 data: &mut SharedVector<Rgba8Pixel>,
1369 width: &mut u32,
1370 height: &mut u32,
1371 ) -> bool {
1372 image.to_rgba8_premultiplied().map_or(false, |pixel_buffer| {
1373 *data = pixel_buffer.data.clone();
1374 *width = pixel_buffer.width();
1375 *height = pixel_buffer.height();
1376 true
1377 })
1378 }
1379}
1380
1381#[derive(Clone, Debug, PartialEq)]
1389#[non_exhaustive]
1390#[cfg(not(target_arch = "wasm32"))]
1391#[repr(C)]
1392pub struct BorrowedOpenGLTexture {
1393 pub texture_id: core::num::NonZeroU32,
1395 pub size: IntSize,
1397 pub origin: BorrowedOpenGLTextureOrigin,
1399}