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(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
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(all(feature = "image-decoders", not(target_arch = "wasm32")))]
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-29", feature = "unstable-wgpu-30"))]
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-29", feature = "unstable-wgpu-30"))]
382#[derive(Clone, Debug)]
383pub enum WGPUTexture {
384 #[cfg(feature = "unstable-wgpu-29")]
386 WGPU29Texture(wgpu_29::Texture),
387 #[cfg(feature = "unstable-wgpu-30")]
389 WGPU30Texture(wgpu_30::Texture),
390}
391
392#[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
393impl OpaqueImage for WGPUTexture {
394 fn size(&self) -> IntSize {
395 match self {
396 #[cfg(feature = "unstable-wgpu-29")]
397 Self::WGPU29Texture(texture) => {
398 let size = texture.size();
399 (size.width, size.height).into()
400 }
401 #[cfg(feature = "unstable-wgpu-30")]
402 Self::WGPU30Texture(texture) => {
403 let size = texture.size();
404 (size.width, size.height).into()
405 }
406 }
407 }
408 fn cache_key(&self) -> ImageCacheKey {
409 ImageCacheKey::Invalid
410 }
411}
412
413#[derive(Clone, Debug, Default)]
418#[repr(u8)]
419#[allow(missing_docs)]
420pub enum ImageInner {
421 #[default]
423 None = 0,
424 EmbeddedImage {
425 cache_key: ImageCacheKey,
426 buffer: SharedImageBuffer,
427 } = 1,
428 #[cfg(feature = "svg")]
429 Svg(vtable::VRc<OpaqueImageVTable, svg::ParsedSVG>) = 2,
430 StaticTextures(&'static StaticTextures) = 3,
431 #[cfg(target_arch = "wasm32")]
432 HTMLImage(vtable::VRc<OpaqueImageVTable, htmlimage::HTMLImage>) = 4,
433 BackendStorage(vtable::VRc<OpaqueImageVTable>) = 5,
434 #[cfg(not(target_arch = "wasm32"))]
435 BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6,
436 NineSlice(vtable::VRc<OpaqueImageVTable, NineSliceImage>) = 7,
437 #[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
438 WGPUTexture(WGPUTexture) = 8,
439}
440
441impl ImageInner {
442 pub fn render_to_buffer(
449 &self,
450 _target_size_for_scalable_source: Option<euclid::Size2D<u32, PhysicalPx>>,
451 ) -> Option<SharedImageBuffer> {
452 match self {
453 ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()),
454 #[cfg(feature = "svg")]
455 ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) {
456 Ok(b) => Some(b),
457 Err(resvg::usvg::Error::InvalidSize) => None,
459 Err(err) => {
460 std::eprintln!("Error rendering SVG: {err}");
461 None
462 }
463 },
464 ImageInner::StaticTextures(ts) => {
465 let mut buffer =
466 SharedPixelBuffer::<Rgba8Pixel>::new(ts.size.width, ts.size.height);
467 let stride = buffer.width() as usize;
468 let slice = buffer.make_mut_slice();
469 for t in ts.textures.iter() {
470 let rect = t.rect.to_usize();
471 for y in 0..rect.height() {
472 let slice = &mut slice[(rect.min_y() + y) * stride..][rect.x_range()];
473 let source = &ts.data[t.index + y * rect.width() * t.format.bpp()..];
474 match t.format {
475 TexturePixelFormat::Rgb => {
476 let mut iter = source
477 .as_chunks::<3>()
478 .0
479 .iter()
480 .map(|p| Rgba8Pixel { r: p[0], g: p[1], b: p[2], a: 255 });
481 slice.fill_with(|| iter.next().unwrap());
482 }
483 TexturePixelFormat::RgbaPremultiplied => {
484 let mut iter = source
485 .as_chunks::<4>()
486 .0
487 .iter()
488 .map(|p| Rgba8Pixel { r: p[0], g: p[1], b: p[2], a: p[3] });
489 slice.fill_with(|| iter.next().unwrap());
490 }
491 TexturePixelFormat::Rgba => {
492 let mut iter = source.as_chunks::<4>().0.iter().map(|p| {
493 let a = p[3];
494 Rgba8Pixel {
495 r: (p[0] as u16 * a as u16 / 255) as u8,
496 g: (p[1] as u16 * a as u16 / 255) as u8,
497 b: (p[2] as u16 * a as u16 / 255) as u8,
498 a,
499 }
500 });
501 slice.fill_with(|| iter.next().unwrap());
502 }
503 TexturePixelFormat::AlphaMap => {
504 let col = t.color.to_argb_u8();
505 let mut iter = source.iter().map(|p| {
506 let a = *p as u32 * col.alpha as u32;
507 Rgba8Pixel {
508 r: (col.red as u32 * a / (255 * 255)) as u8,
509 g: (col.green as u32 * a / (255 * 255)) as u8,
510 b: (col.blue as u32 * a / (255 * 255)) as u8,
511 a: (a / 255) as u8,
512 }
513 });
514 slice.fill_with(|| iter.next().unwrap());
515 }
516 TexturePixelFormat::SignedDistanceField => {
517 todo!("converting from a signed distance field to an image")
518 }
519 };
520 }
521 }
522 Some(SharedImageBuffer::RGBA8Premultiplied(buffer))
523 }
524 ImageInner::NineSlice(nine) => nine.0.render_to_buffer(None),
525 _ => None,
526 }
527 }
528
529 pub fn is_svg(&self) -> bool {
531 match self {
532 #[cfg(feature = "svg")]
533 Self::Svg(_) => true,
534 #[cfg(target_arch = "wasm32")]
535 Self::HTMLImage(html_image) => html_image.is_svg(),
536 _ => false,
537 }
538 }
539
540 pub fn size(&self) -> IntSize {
542 match self {
543 ImageInner::None => Default::default(),
544 ImageInner::EmbeddedImage { buffer, .. } => buffer.size(),
545 ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
546 #[cfg(feature = "svg")]
547 ImageInner::Svg(svg) => svg.size(),
548 #[cfg(target_arch = "wasm32")]
549 ImageInner::HTMLImage(htmlimage) => htmlimage.size().unwrap_or_default(),
550 ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size(),
551 #[cfg(not(target_arch = "wasm32"))]
552 ImageInner::BorrowedOpenGLTexture(BorrowedOpenGLTexture { size, .. }) => *size,
553 ImageInner::NineSlice(nine) => nine.0.size(),
554 #[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
555 ImageInner::WGPUTexture(texture) => texture.size(),
556 }
557 }
558
559 #[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
566 pub(crate) fn load_from_data_with_cache_key(
567 cache_key: ImageCacheKey,
568 data: Slice<'_, u8>,
569 format: Slice<'_, u8>,
570 ) -> Option<Self> {
571 #[cfg(target_arch = "wasm32")]
573 {
574 let _ = cache_key;
575 let mime_type = core::str::from_utf8(format.as_slice())
576 .ok()
577 .and_then(image_mime_type_from_extension)
578 .unwrap_or_else(|| {
579 if data.starts_with(b"<?xml") || data.starts_with(b"<svg") {
580 "image/svg+xml"
581 } else {
582 ""
584 }
585 });
586 if mime_type == "image/svg+xml" && data.starts_with(&[0x1f, 0x8b]) {
587 crate::debug_log!("Compressed SVG (.svgz) is not supported on the web");
588 return None;
589 }
590 return htmlimage::HTMLImage::new_from_data(data.as_slice(), mime_type)
591 .map(|html_image| ImageInner::HTMLImage(vtable::VRc::new(html_image)));
592 }
593
594 #[cfg(not(target_arch = "wasm32"))]
595 {
596 #[cfg(feature = "svg")]
597 if format.as_slice() == b"svg"
598 || format.as_slice() == b"svgz"
599 || (format.is_empty() && (data.starts_with(b"<?xml") || data.starts_with(b"<svg")))
600 {
601 return Some(ImageInner::Svg(vtable::VRc::new(
602 svg::load_from_data(data.as_slice(), cache_key).map_or_else(
603 |svg_err| {
604 crate::debug_log!("Error loading SVG: {}", svg_err);
605 None
606 },
607 Some,
608 )?,
609 )));
610 }
611
612 let format = std::str::from_utf8(format.as_slice())
613 .ok()
614 .and_then(image::ImageFormat::from_extension);
615 let maybe_image = if let Some(format) = format {
616 image::load_from_memory_with_format(data.as_slice(), format)
617 } else {
618 image::load_from_memory(data.as_slice())
619 };
620
621 match maybe_image {
622 Ok(image) => Some(ImageInner::EmbeddedImage {
623 cache_key,
624 buffer: dynamic_image_to_shared_image_buffer(image),
625 }),
626 Err(decode_err) => {
627 crate::debug_log!("Error decoding embedded image: {}", decode_err);
628 None
629 }
630 }
631 }
632 }
633}
634
635#[cfg(all(feature = "image-decoders", not(target_arch = "wasm32")))]
637fn dynamic_image_to_shared_image_buffer(dynamic_image: image::DynamicImage) -> SharedImageBuffer {
638 use rgb::AsPixels;
639
640 if dynamic_image.color().has_alpha() {
641 let rgba8image = dynamic_image.to_rgba8();
642 SharedImageBuffer::RGBA8Premultiplied(SharedPixelBuffer {
645 width: rgba8image.width(),
646 height: rgba8image.height(),
647 data: rgba8image
648 .as_pixels()
649 .iter()
650 .map(|pixel| Image::rgba_to_premultiplied_rgba(*pixel))
651 .collect(),
652 })
653 } else {
654 let rgb8image = dynamic_image.to_rgb8();
655 SharedImageBuffer::RGB8(SharedPixelBuffer::clone_from_slice(
656 rgb8image.as_raw(),
657 rgb8image.width(),
658 rgb8image.height(),
659 ))
660 }
661}
662
663impl PartialEq for ImageInner {
664 fn eq(&self, other: &Self) -> bool {
665 match (self, other) {
666 (
667 Self::EmbeddedImage { cache_key: l_cache_key, buffer: l_buffer },
668 Self::EmbeddedImage { cache_key: r_cache_key, buffer: r_buffer },
669 ) => l_cache_key == r_cache_key && l_buffer == r_buffer,
670 #[cfg(feature = "svg")]
671 (Self::Svg(l0), Self::Svg(r0)) => vtable::VRc::ptr_eq(l0, r0),
672 (Self::StaticTextures(l0), Self::StaticTextures(r0)) => l0 == r0,
673 #[cfg(target_arch = "wasm32")]
674 (Self::HTMLImage(l0), Self::HTMLImage(r0)) => vtable::VRc::ptr_eq(l0, r0),
675 (Self::BackendStorage(l0), Self::BackendStorage(r0)) => vtable::VRc::ptr_eq(l0, r0),
676 #[cfg(not(target_arch = "wasm32"))]
677 (Self::BorrowedOpenGLTexture(l0), Self::BorrowedOpenGLTexture(r0)) => l0 == r0,
678 (Self::NineSlice(l), Self::NineSlice(r)) => l.0 == r.0 && l.1 == r.1,
679 _ => false,
680 }
681 }
682}
683
684impl<'a> From<&'a Image> for &'a ImageInner {
685 fn from(other: &'a Image) -> Self {
686 &other.0
687 }
688}
689
690#[derive(Default, Debug, PartialEq)]
692pub struct LoadImageError(());
693
694impl core::fmt::Display for LoadImageError {
695 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
696 f.write_str("The image cannot be loaded")
697 }
698}
699
700#[cfg(feature = "std")]
701impl std::error::Error for LoadImageError {}
702
703#[repr(transparent)]
800#[derive(Default, Clone, Debug, PartialEq, derive_more::From)]
801pub struct Image(pub(crate) ImageInner);
802
803impl Image {
804 #[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
805 pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> {
814 self::cache::IMAGE_CACHE.with(|global_cache| {
815 let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into();
816 global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(()))
817 })
818 }
819
820 pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
823 Image(ImageInner::EmbeddedImage {
824 cache_key: ImageCacheKey::Invalid,
825 buffer: SharedImageBuffer::RGB8(buffer),
826 })
827 }
828
829 pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
832 Image(ImageInner::EmbeddedImage {
833 cache_key: ImageCacheKey::Invalid,
834 buffer: SharedImageBuffer::RGBA8(buffer),
835 })
836 }
837
838 pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
844 Image(ImageInner::EmbeddedImage {
845 cache_key: ImageCacheKey::Invalid,
846 buffer: SharedImageBuffer::RGBA8Premultiplied(buffer),
847 })
848 }
849
850 pub fn to_rgb8(&self) -> Option<SharedPixelBuffer<Rgb8Pixel>> {
853 self.0.render_to_buffer(None).and_then(|image| match image {
854 SharedImageBuffer::RGB8(buffer) => Some(buffer),
855 _ => None,
856 })
857 }
858
859 pub fn to_rgba8(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
862 self.render_to_rgba8(None)
863 }
864
865 fn render_to_rgba8(
866 &self,
867 target_size: Option<euclid::Size2D<u32, PhysicalPx>>,
868 ) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
869 self.0.render_to_buffer(target_size).map(|image| match image {
870 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
871 width: buffer.width,
872 height: buffer.height,
873 data: buffer.data.into_iter().map(Into::into).collect(),
874 },
875 SharedImageBuffer::RGBA8(buffer) => buffer,
876 SharedImageBuffer::RGBA8Premultiplied(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
877 width: buffer.width,
878 height: buffer.height,
879 data: buffer.data.into_iter().map(Image::premultiplied_rgba_to_rgba).collect(),
880 },
881 })
882 }
883
884 pub fn to_rgba8_premultiplied(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
888 self.0.render_to_buffer(None).map(|image| match image {
889 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
890 width: buffer.width,
891 height: buffer.height,
892 data: buffer.data.into_iter().map(Into::into).collect(),
893 },
894 SharedImageBuffer::RGBA8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
895 width: buffer.width,
896 height: buffer.height,
897 data: buffer.data.into_iter().map(Image::rgba_to_premultiplied_rgba).collect(),
898 },
899 SharedImageBuffer::RGBA8Premultiplied(buffer) => buffer,
900 })
901 }
902
903 fn premultiplied_rgba_to_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
905 if pixel.a == 0 {
906 Rgba8Pixel::new(0, 0, 0, 0)
907 } else {
908 let af = pixel.a as u32;
909 let round = (af / 2) as u32;
910 Rgba8Pixel {
911 r: ((pixel.r as u32 * 255 + round) / af).min(255) as u8,
912 g: ((pixel.g as u32 * 255 + round) / af).min(255) as u8,
913 b: ((pixel.b as u32 * 255 + round) / af).min(255) as u8,
914 a: pixel.a,
915 }
916 }
917 }
918
919 fn rgba_to_premultiplied_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
921 if pixel.a == 255 {
922 pixel
923 } else {
924 let af = pixel.a as u32;
925 Rgba8Pixel {
926 r: (((pixel.r as u32 * af + 128) * 257) >> 16) as u8,
927 g: (((pixel.g as u32 * af + 128) * 257) >> 16) as u8,
928 b: (((pixel.b as u32 * af + 128) * 257) >> 16) as u8,
929 a: pixel.a,
930 }
931 }
932 }
933
934 #[cfg(feature = "unstable-wgpu-29")]
940 pub fn to_wgpu_29_texture(&self) -> Option<wgpu_29::Texture> {
941 match &self.0 {
942 ImageInner::WGPUTexture(WGPUTexture::WGPU29Texture(texture)) => Some(texture.clone()),
943 _ => None,
944 }
945 }
946
947 #[cfg(feature = "unstable-wgpu-30")]
953 pub fn to_wgpu_30_texture(&self) -> Option<wgpu_30::Texture> {
954 match &self.0 {
955 ImageInner::WGPUTexture(WGPUTexture::WGPU30Texture(texture)) => Some(texture.clone()),
956 _ => None,
957 }
958 }
959
960 #[allow(unsafe_code)]
980 #[cfg(not(target_arch = "wasm32"))]
981 #[deprecated(since = "1.2.0", note = "Use BorrowedOpenGLTextureBuilder")]
982 pub unsafe fn from_borrowed_gl_2d_rgba_texture(
983 texture_id: core::num::NonZeroU32,
984 size: IntSize,
985 ) -> Self {
986 unsafe { BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size).build() }
987 }
988
989 #[cfg(any(feature = "svg", target_arch = "wasm32"))]
993 pub fn load_from_svg_data(buffer: &[u8]) -> Result<Self, LoadImageError> {
994 #[cfg(target_arch = "wasm32")]
996 {
997 htmlimage::HTMLImage::new_from_data(buffer, "image/svg+xml")
998 .map(|html_image| Image(ImageInner::HTMLImage(vtable::VRc::new(html_image))))
999 .ok_or(LoadImageError(()))
1000 }
1001 #[cfg(not(target_arch = "wasm32"))]
1002 {
1003 let cache_key = ImageCacheKey::Invalid;
1004 Ok(Image(ImageInner::Svg(vtable::VRc::new(
1005 svg::load_from_data(buffer, cache_key).map_err(|_| LoadImageError(()))?,
1006 ))))
1007 }
1008 }
1009
1010 #[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
1019 pub fn load_from_data(data: &[u8], format: Option<&str>) -> Result<Self, LoadImageError> {
1020 ImageInner::load_from_data_with_cache_key(
1021 ImageCacheKey::Invalid,
1022 Slice::from_slice(data),
1023 Slice::from_slice(format.unwrap_or_default().as_bytes()),
1024 )
1025 .map(Image)
1026 .ok_or(LoadImageError(()))
1027 }
1028
1029 pub fn set_nine_slice_edges(&mut self, top: u16, right: u16, bottom: u16, left: u16) {
1035 if top == 0 && left == 0 && right == 0 && bottom == 0 {
1036 if let ImageInner::NineSlice(n) = &self.0 {
1037 self.0 = n.0.clone();
1038 }
1039 } else {
1040 let array = [top, right, bottom, left];
1041 let inner = if let ImageInner::NineSlice(n) = &mut self.0 {
1042 n.0.clone()
1043 } else {
1044 self.0.clone()
1045 };
1046 self.0 = ImageInner::NineSlice(vtable::VRc::new(NineSliceImage(inner, array)));
1047 }
1048 }
1049
1050 pub fn size(&self) -> IntSize {
1052 self.0.size()
1053 }
1054
1055 #[cfg(feature = "std")]
1056 pub fn path(&self) -> Option<&std::path::Path> {
1068 match &self.0 {
1069 ImageInner::EmbeddedImage {
1070 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1071 ..
1072 } => Some(std::path::Path::new(path.as_str())),
1073 ImageInner::NineSlice(nine) => match &nine.0 {
1074 ImageInner::EmbeddedImage {
1075 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1076 ..
1077 } => Some(std::path::Path::new(path.as_str())),
1078 _ => None,
1079 },
1080 _ => None,
1081 }
1082 }
1083}
1084
1085pub fn image_to_rgba8_with_target_size(
1089 image: &Image,
1090 target_size: IntSize,
1091) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
1092 image.render_to_rgba8(Some(target_size.cast_unit()))
1093}
1094
1095#[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
1096pub fn load_image_from_data_uri(
1099 uri: &str,
1100 bytes: &[u8],
1101 format: &str,
1102) -> Result<Image, LoadImageError> {
1103 #[cfg(target_arch = "wasm32")]
1106 {
1107 self::cache::IMAGE_CACHE.with(|global_cache| {
1108 global_cache
1109 .borrow_mut()
1110 .load_image_from_data_uri(uri, bytes, format)
1111 .ok_or(LoadImageError(()))
1112 })
1113 }
1114 #[cfg(not(target_arch = "wasm32"))]
1116 {
1117 let _ = uri;
1118 ImageInner::load_from_data_with_cache_key(
1119 ImageCacheKey::Invalid,
1120 bytes.into(),
1121 format.as_bytes().into(),
1122 )
1123 .map(Image)
1124 .ok_or(Default::default())
1125 }
1126}
1127
1128pub fn image_mime_type_from_extension(extension: &str) -> Option<&'static str> {
1131 for (ext, mime) in [
1132 ("png", "image/png"),
1133 ("jpg", "image/jpeg"),
1134 ("jpeg", "image/jpeg"),
1135 ("svg", "image/svg+xml"),
1136 ("svgz", "image/svg+xml"),
1137 ("gif", "image/gif"),
1138 ("webp", "image/webp"),
1139 ("bmp", "image/bmp"),
1140 ("ico", "image/x-icon"),
1141 ("avif", "image/avif"),
1142 ] {
1143 if extension.eq_ignore_ascii_case(ext) {
1144 return Some(mime);
1145 }
1146 }
1147 None
1148}
1149
1150#[derive(Copy, Clone, Debug, PartialEq, Default)]
1153#[repr(u8)]
1154#[non_exhaustive]
1155pub enum BorrowedOpenGLTextureOrigin {
1156 #[default]
1158 TopLeft,
1159 BottomLeft,
1162}
1163
1164#[cfg(not(target_arch = "wasm32"))]
1182pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture);
1183
1184#[cfg(not(target_arch = "wasm32"))]
1185impl BorrowedOpenGLTextureBuilder {
1186 #[allow(unsafe_code)]
1204 pub unsafe fn new_gl_2d_rgba_texture(texture_id: core::num::NonZeroU32, size: IntSize) -> Self {
1205 Self(BorrowedOpenGLTexture { texture_id, size, origin: Default::default() })
1206 }
1207
1208 pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self {
1210 self.0.origin = origin;
1211 self
1212 }
1213
1214 pub fn build(self) -> Image {
1216 Image(ImageInner::BorrowedOpenGLTexture(self.0))
1217 }
1218}
1219
1220#[cfg(all(target_arch = "wasm32", feature = "std"))]
1226pub fn load_as_html_image(url: &str) -> Result<Image, LoadImageError> {
1227 self::cache::IMAGE_CACHE.with(|global_cache| {
1228 global_cache.borrow_mut().load_as_html_image(url).ok_or(LoadImageError(()))
1229 })
1230}
1231
1232#[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
1235pub fn load_image_from_embedded_data(data: Slice<'static, u8>, format: Slice<'_, u8>) -> Image {
1236 self::cache::IMAGE_CACHE.with(|global_cache| {
1237 global_cache.borrow_mut().load_image_from_embedded_data(data, format).unwrap_or_default()
1238 })
1239}
1240
1241#[test]
1242fn test_image_size_from_buffer_without_backend() {
1243 {
1244 assert_eq!(Image::default().size(), Default::default());
1245 assert!(Image::default().to_rgb8().is_none());
1246 assert!(Image::default().to_rgba8().is_none());
1247 assert!(Image::default().to_rgba8_premultiplied().is_none());
1248 }
1249 {
1250 let buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
1251 let image = Image::from_rgb8(buffer.clone());
1252 assert_eq!(image.size(), [320, 200].into());
1253 assert_eq!(image.to_rgb8().as_ref().map(|b| b.as_slice()), Some(buffer.as_slice()));
1254 }
1255}
1256
1257#[cfg(feature = "svg")]
1258#[test]
1259#[cfg_attr(miri, ignore)]
1261fn test_image_size_from_svg() {
1262 let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
1263 let image = Image::load_from_svg_data(simple_svg.as_bytes()).unwrap();
1264 assert_eq!(image.size(), [320, 200].into());
1265 assert_eq!(image.to_rgba8().unwrap().size(), image.size());
1266}
1267
1268#[cfg(feature = "svg")]
1269#[test]
1270#[cfg_attr(miri, ignore)]
1272fn test_image_invalid_svg() {
1273 let invalid_svg = r#"AaBbCcDd"#;
1274 let result = Image::load_from_svg_data(invalid_svg.as_bytes());
1275 assert!(result.is_err());
1276}
1277
1278#[cfg(feature = "svg")]
1279#[test]
1280#[cfg_attr(miri, ignore)]
1282fn test_image_load_from_data_svg() {
1283 let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
1284 let guessed = Image::load_from_data(simple_svg.as_bytes(), None).unwrap();
1286 assert_eq!(guessed.size(), [320, 200].into());
1287 let hinted = Image::load_from_data(simple_svg.as_bytes(), Some("svg")).unwrap();
1289 assert_eq!(hinted.size(), [320, 200].into());
1290}
1291
1292#[cfg(feature = "svg")]
1293#[test]
1294#[cfg_attr(miri, ignore)]
1296fn test_image_load_from_data_svgz() {
1297 const SIMPLE_SVGZ: &[u8] = &[
1300 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0xb3, 0x29, 0x2e, 0x4b, 0x57,
1301 0x28, 0xcf, 0x4c, 0x29, 0xc9, 0xb0, 0x55, 0x32, 0x36, 0x32, 0x50, 0x52, 0xc8, 0x48, 0xcd,
1302 0x4c, 0xcf, 0x28, 0xb1, 0x55, 0x32, 0x32, 0x00, 0x72, 0x2a, 0x72, 0x73, 0xf2, 0x8a, 0x6d,
1303 0x95, 0x32, 0x4a, 0x4a, 0x0a, 0xac, 0xf4, 0xf5, 0xcb, 0xcb, 0xcb, 0xf5, 0xca, 0x8d, 0xf5,
1304 0xf2, 0x8b, 0xd2, 0xf5, 0x81, 0xb2, 0x06, 0xfa, 0x40, 0xad, 0x4a, 0x76, 0x36, 0x20, 0xca,
1305 0x0e, 0x00, 0x37, 0x91, 0x7a, 0xd6, 0x47, 0x00, 0x00, 0x00,
1306 ];
1307 let image = Image::load_from_data(SIMPLE_SVGZ, Some("svgz")).unwrap();
1308 assert_eq!(image.size(), [320, 200].into());
1309}
1310
1311#[cfg(feature = "image-decoders")]
1312#[test]
1313#[cfg_attr(miri, ignore)]
1314fn test_image_load_from_data_png() {
1315 let mut png = std::io::Cursor::new(std::vec::Vec::new());
1316 image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(2, 3, image::Rgb([0, 255, 0])))
1317 .write_to(&mut png, image::ImageFormat::Png)
1318 .unwrap();
1319 let png = png.into_inner();
1320
1321 let guessed = Image::load_from_data(&png, None).unwrap();
1323 assert_eq!(guessed.size(), [2, 3].into());
1324 let hinted = Image::load_from_data(&png, Some("png")).unwrap();
1326 assert_eq!(hinted.size(), [2, 3].into());
1327
1328 assert!(Image::load_from_data(b"not an image", None).is_err());
1329}
1330
1331#[derive(Debug)]
1333pub struct FitResult {
1334 pub clip_rect: IntRect,
1336 pub source_to_target_x: f32,
1338 pub source_to_target_y: f32,
1340 pub size: euclid::Size2D<f32, PhysicalPx>,
1342 pub offset: euclid::Point2D<f32, PhysicalPx>,
1344 pub tiled: Option<euclid::default::Point2D<u32>>,
1348}
1349
1350impl FitResult {
1351 fn adjust_for_tiling(
1352 self,
1353 ratio: f32,
1354 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1355 tiling: (ImageTiling, ImageTiling),
1356 ) -> Self {
1357 let mut r = self;
1358 let mut tiled = euclid::Point2D::default();
1359 let target = r.size;
1360 let o = r.clip_rect.size.cast::<f32>();
1361 match tiling.0 {
1362 ImageTiling::None => {
1363 r.size.width = o.width * r.source_to_target_x;
1364 if (o.width as f32) > target.width / r.source_to_target_x {
1365 let diff = (o.width as f32 - target.width / r.source_to_target_x) as i32;
1366 r.clip_rect.size.width -= diff;
1367 r.clip_rect.origin.x += match alignment.0 {
1368 ImageHorizontalAlignment::Center => diff / 2,
1369 ImageHorizontalAlignment::Left => 0,
1370 ImageHorizontalAlignment::Right => diff,
1371 };
1372 r.size.width = target.width;
1373 } else if (o.width as f32) < target.width / r.source_to_target_x {
1374 r.offset.x += match alignment.0 {
1375 ImageHorizontalAlignment::Center => {
1376 (target.width - o.width as f32 * r.source_to_target_x) / 2.
1377 }
1378 ImageHorizontalAlignment::Left => 0.,
1379 ImageHorizontalAlignment::Right => {
1380 target.width - o.width as f32 * r.source_to_target_x
1381 }
1382 };
1383 }
1384 }
1385 ImageTiling::Repeat => {
1386 tiled.x = match alignment.0 {
1387 ImageHorizontalAlignment::Left => 0,
1388 ImageHorizontalAlignment::Center => {
1389 ((o.width - target.width / ratio) / 2.).rem_euclid(o.width) as u32
1390 }
1391 ImageHorizontalAlignment::Right => {
1392 (-target.width / ratio).rem_euclid(o.width) as u32
1393 }
1394 };
1395 r.source_to_target_x = ratio;
1396 }
1397 ImageTiling::Round => {
1398 if target.width / ratio <= o.width * 1.5 {
1399 r.source_to_target_x = target.width / o.width;
1400 } else {
1401 let mut rem = (target.width / ratio).rem_euclid(o.width);
1402 if rem > o.width / 2. {
1403 rem -= o.width;
1404 }
1405 r.source_to_target_x = ratio * target.width / (target.width - rem * ratio);
1406 }
1407 }
1408 }
1409
1410 match tiling.1 {
1411 ImageTiling::None => {
1412 r.size.height = o.height * r.source_to_target_y;
1413 if (o.height as f32) > target.height / r.source_to_target_y {
1414 let diff = (o.height as f32 - target.height / r.source_to_target_y) as i32;
1415 r.clip_rect.size.height -= diff;
1416 r.clip_rect.origin.y += match alignment.1 {
1417 ImageVerticalAlignment::Center => diff / 2,
1418 ImageVerticalAlignment::Top => 0,
1419 ImageVerticalAlignment::Bottom => diff,
1420 };
1421 r.size.height = target.height;
1422 } else if (o.height as f32) < target.height / r.source_to_target_y {
1423 r.offset.y += match alignment.1 {
1424 ImageVerticalAlignment::Center => {
1425 (target.height - o.height as f32 * r.source_to_target_y) / 2.
1426 }
1427 ImageVerticalAlignment::Top => 0.,
1428 ImageVerticalAlignment::Bottom => {
1429 target.height - o.height as f32 * r.source_to_target_y
1430 }
1431 };
1432 }
1433 }
1434 ImageTiling::Repeat => {
1435 tiled.y = match alignment.1 {
1436 ImageVerticalAlignment::Top => 0,
1437 ImageVerticalAlignment::Center => {
1438 ((o.height - target.height / ratio) / 2.).rem_euclid(o.height) as u32
1439 }
1440 ImageVerticalAlignment::Bottom => {
1441 (-target.height / ratio).rem_euclid(o.height) as u32
1442 }
1443 };
1444 r.source_to_target_y = ratio;
1445 }
1446 ImageTiling::Round => {
1447 if target.height / ratio <= o.height * 1.5 {
1448 r.source_to_target_y = target.height / o.height;
1449 } else {
1450 let mut rem = (target.height / ratio).rem_euclid(o.height);
1451 if rem > o.height / 2. {
1452 rem -= o.height;
1453 }
1454 r.source_to_target_y = ratio * target.height / (target.height - rem * ratio);
1455 }
1456 }
1457 }
1458 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1459 r.tiled = has_tiling.then_some(tiled);
1460 r
1461 }
1462}
1463
1464#[cfg(not(feature = "std"))]
1465trait RemEuclid {
1466 fn rem_euclid(self, b: f32) -> f32;
1467}
1468#[cfg(not(feature = "std"))]
1469impl RemEuclid for f32 {
1470 fn rem_euclid(self, b: f32) -> f32 {
1471 num_traits::Euclid::rem_euclid(&self, &b)
1472 }
1473}
1474
1475pub fn fit(
1477 image_fit: ImageFit,
1478 target: euclid::Size2D<f32, PhysicalPx>,
1479 source_rect: IntRect,
1480 scale_factor: ScaleFactor,
1481 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1482 tiling: (ImageTiling, ImageTiling),
1483) -> FitResult {
1484 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1485 let o = source_rect.size.cast::<f32>();
1486 let ratio = match image_fit {
1487 _ if has_tiling => scale_factor.get(),
1489 ImageFit::Fill => {
1490 return FitResult {
1491 clip_rect: source_rect,
1492 source_to_target_x: target.width / o.width,
1493 source_to_target_y: target.height / o.height,
1494 size: target,
1495 offset: Default::default(),
1496 tiled: None,
1497 };
1498 }
1499 ImageFit::Preserve => scale_factor.get(),
1500 ImageFit::Contain => f32::min(target.width / o.width, target.height / o.height),
1501 ImageFit::Cover => f32::max(target.width / o.width, target.height / o.height),
1502 };
1503
1504 FitResult {
1505 clip_rect: source_rect,
1506 source_to_target_x: ratio,
1507 source_to_target_y: ratio,
1508 size: target,
1509 offset: euclid::Point2D::default(),
1510 tiled: None,
1511 }
1512 .adjust_for_tiling(ratio, alignment, tiling)
1513}
1514
1515pub fn scalable_render_size(
1521 source_size: IntSize,
1522 image_fit: ImageFit,
1523 target: euclid::Size2D<f32, PhysicalPx>,
1524 scale_factor: ScaleFactor,
1525 tiling: (ImageTiling, ImageTiling),
1526) -> Option<euclid::Size2D<u32, PhysicalPx>> {
1527 let source = source_size.cast::<f32>();
1528 if source.is_empty() {
1529 return None;
1530 }
1531 let fit = fit(
1532 image_fit,
1533 target,
1534 IntRect::from_size(source_size.cast()),
1535 scale_factor,
1536 Default::default(),
1538 tiling,
1539 );
1540 let size = euclid::size2(
1541 (source.width * fit.source_to_target_x) as u32,
1542 (source.height * fit.source_to_target_y) as u32,
1543 );
1544 (!size.is_empty()).then_some(size)
1545}
1546
1547pub fn fit9slice(
1549 source_rect: IntSize,
1550 [t, r, b, l]: [u16; 4],
1551 target: euclid::Size2D<f32, PhysicalPx>,
1552 scale_factor: ScaleFactor,
1553 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1554 tiling: (ImageTiling, ImageTiling),
1555) -> impl Iterator<Item = FitResult> {
1556 let fit_to = |clip_rect: euclid::default::Rect<u16>, target: euclid::Rect<f32, PhysicalPx>| {
1557 (!clip_rect.is_empty() && !target.is_empty()).then(|| {
1558 FitResult {
1559 clip_rect: clip_rect.cast(),
1560 source_to_target_x: target.width() / clip_rect.width() as f32,
1561 source_to_target_y: target.height() / clip_rect.height() as f32,
1562 size: target.size,
1563 offset: target.origin,
1564 tiled: None,
1565 }
1566 .adjust_for_tiling(scale_factor.get(), alignment, tiling)
1567 })
1568 };
1569 use euclid::rect;
1570 let sf = |x| scale_factor.get() * x as f32;
1571 let source = source_rect.cast::<u16>();
1572 if t + b > source.height || l + r > source.width {
1573 [None, None, None, None, None, None, None, None, None]
1574 } else {
1575 [
1576 fit_to(rect(0, 0, l, t), rect(0., 0., sf(l), sf(t))),
1577 fit_to(
1578 rect(l, 0, source.width - l - r, t),
1579 rect(sf(l), 0., target.width - sf(l) - sf(r), sf(t)),
1580 ),
1581 fit_to(rect(source.width - r, 0, r, t), rect(target.width - sf(r), 0., sf(r), sf(t))),
1582 fit_to(
1583 rect(0, t, l, source.height - t - b),
1584 rect(0., sf(t), sf(l), target.height - sf(t) - sf(b)),
1585 ),
1586 fit_to(
1587 rect(l, t, source.width - l - r, source.height - t - b),
1588 rect(sf(l), sf(t), target.width - sf(l) - sf(r), target.height - sf(t) - sf(b)),
1589 ),
1590 fit_to(
1591 rect(source.width - r, t, r, source.height - t - b),
1592 rect(target.width - sf(r), sf(t), sf(r), target.height - sf(t) - sf(b)),
1593 ),
1594 fit_to(rect(0, source.height - b, l, b), rect(0., target.height - sf(b), sf(l), sf(b))),
1595 fit_to(
1596 rect(l, source.height - b, source.width - l - r, b),
1597 rect(sf(l), target.height - sf(b), target.width - sf(l) - sf(r), sf(b)),
1598 ),
1599 fit_to(
1600 rect(source.width - r, source.height - b, r, b),
1601 rect(target.width - sf(r), target.height - sf(b), sf(r), sf(b)),
1602 ),
1603 ]
1604 }
1605 .into_iter()
1606 .flatten()
1607}
1608
1609#[cfg(feature = "ffi")]
1610pub(crate) mod ffi {
1611 #![allow(unsafe_code)]
1612
1613 use super::*;
1614
1615 #[cfg(cbindgen)]
1618 #[repr(C)]
1619 struct Rgb8Pixel {
1620 r: u8,
1622 g: u8,
1624 b: u8,
1626 }
1627
1628 #[cfg(cbindgen)]
1631 #[repr(C)]
1632 struct Rgba8Pixel {
1633 r: u8,
1635 g: u8,
1637 b: u8,
1639 a: u8,
1641 }
1642
1643 #[cfg(all(feature = "std", feature = "image-decoders"))]
1646 #[unsafe(no_mangle)]
1647 pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) {
1648 unsafe {
1649 core::ptr::write(
1650 image,
1651 Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or_default(),
1652 )
1653 }
1654 }
1655
1656 #[cfg(all(feature = "std", feature = "image-decoders"))]
1657 #[unsafe(no_mangle)]
1658 pub unsafe extern "C" fn slint_image_load_from_embedded_data(
1659 data: Slice<'static, u8>,
1660 format: Slice<'static, u8>,
1661 image: *mut Image,
1662 ) {
1663 unsafe { core::ptr::write(image, super::load_image_from_embedded_data(data, format)) };
1664 }
1665
1666 #[cfg(all(feature = "std", feature = "image-decoders"))]
1669 #[unsafe(no_mangle)]
1670 pub unsafe extern "C" fn slint_image_load_from_data(
1671 data: Slice<'_, u8>,
1672 format: Slice<'_, u8>,
1673 image: *mut Image,
1674 ) {
1675 let format = core::str::from_utf8(format.as_slice()).ok();
1676 let loaded = super::Image::load_from_data(data.as_slice(), format).unwrap_or_default();
1677 unsafe { core::ptr::write(image, loaded) };
1678 }
1679
1680 #[unsafe(no_mangle)]
1681 pub extern "C" fn slint_image_size(image: &Image) -> IntSize {
1682 image.size()
1683 }
1684
1685 #[unsafe(no_mangle)]
1686 pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
1687 match &image.0 {
1688 #[cfg(feature = "std")]
1689 ImageInner::EmbeddedImage {
1690 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1691 ..
1692 } => Some(path),
1693 ImageInner::NineSlice(nine) => match &nine.0 {
1694 #[cfg(feature = "std")]
1695 ImageInner::EmbeddedImage {
1696 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1697 ..
1698 } => Some(path),
1699 _ => None,
1700 },
1701 _ => None,
1702 }
1703 }
1704
1705 #[unsafe(no_mangle)]
1706 pub unsafe extern "C" fn slint_image_from_embedded_textures(
1707 textures: &'static StaticTextures,
1708 image: *mut Image,
1709 ) {
1710 unsafe { core::ptr::write(image, Image::from(ImageInner::StaticTextures(textures))) };
1711 }
1712
1713 #[unsafe(no_mangle)]
1714 pub extern "C" fn slint_image_compare_equal(image1: &Image, image2: &Image) -> bool {
1715 image1.eq(image2)
1716 }
1717
1718 #[unsafe(no_mangle)]
1720 pub extern "C" fn slint_image_set_nine_slice_edges(
1721 image: &mut Image,
1722 top: u16,
1723 right: u16,
1724 bottom: u16,
1725 left: u16,
1726 ) {
1727 image.set_nine_slice_edges(top, right, bottom, left);
1728 }
1729
1730 #[unsafe(no_mangle)]
1731 pub extern "C" fn slint_image_to_rgb8(
1732 image: &Image,
1733 data: &mut SharedVector<Rgb8Pixel>,
1734 width: &mut u32,
1735 height: &mut u32,
1736 ) -> bool {
1737 image.to_rgb8().is_some_and(|pixel_buffer| {
1738 *data = pixel_buffer.data.clone();
1739 *width = pixel_buffer.width();
1740 *height = pixel_buffer.height();
1741 true
1742 })
1743 }
1744
1745 #[unsafe(no_mangle)]
1746 pub extern "C" fn slint_image_to_rgba8(
1747 image: &Image,
1748 data: &mut SharedVector<Rgba8Pixel>,
1749 width: &mut u32,
1750 height: &mut u32,
1751 ) -> bool {
1752 image.to_rgba8().is_some_and(|pixel_buffer| {
1753 *data = pixel_buffer.data.clone();
1754 *width = pixel_buffer.width();
1755 *height = pixel_buffer.height();
1756 true
1757 })
1758 }
1759
1760 #[unsafe(no_mangle)]
1761 pub extern "C" fn slint_image_to_rgba8_premultiplied(
1762 image: &Image,
1763 data: &mut SharedVector<Rgba8Pixel>,
1764 width: &mut u32,
1765 height: &mut u32,
1766 ) -> bool {
1767 image.to_rgba8_premultiplied().is_some_and(|pixel_buffer| {
1768 *data = pixel_buffer.data.clone();
1769 *width = pixel_buffer.width();
1770 *height = pixel_buffer.height();
1771 true
1772 })
1773 }
1774}
1775
1776#[derive(Clone, Debug, PartialEq)]
1784#[non_exhaustive]
1785#[cfg(not(target_arch = "wasm32"))]
1786#[repr(C)]
1787pub struct BorrowedOpenGLTexture {
1788 pub texture_id: core::num::NonZeroU32,
1790 pub size: IntSize,
1792 pub origin: BorrowedOpenGLTextureOrigin,
1794}
1795
1796#[cfg(test)]
1797mod tests {
1798 use crate::graphics::Rgba8Pixel;
1799
1800 use super::Image;
1801
1802 #[test]
1803 fn test_premultiplied_to_rgb_zero_alpha() {
1804 let pixel = Rgba8Pixel::new(5, 10, 15, 0);
1805 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1806 assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1807 }
1808
1809 #[test]
1810 fn test_premultiplied_to_rgb_full_alpha() {
1811 let pixel = Rgba8Pixel::new(5, 10, 15, 255);
1812 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1813 assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 255));
1814 }
1815
1816 #[test]
1817 fn test_premultiplied_to_rgb() {
1818 let pixel = Rgba8Pixel::new(5, 10, 15, 128);
1819 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1820 assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 128));
1821 }
1822
1823 #[test]
1824 fn test_rgb_to_premultiplied_zero_alpha() {
1825 let pixel = Rgba8Pixel::new(10, 20, 30, 0);
1826 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1827 assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1828 }
1829
1830 #[test]
1831 fn test_rgb_to_premultiplied_full_alpha() {
1832 let pixel = Rgba8Pixel::new(10, 20, 30, 255);
1833 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1834 assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 255));
1835 }
1836
1837 #[test]
1838 fn test_rgb_to_premultiplied() {
1839 let pixel = Rgba8Pixel::new(10, 20, 30, 128);
1840 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1841 assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 128));
1842 }
1843}