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 #[track_caller]
141 pub fn clone_from_slice<SourcePixelType>(
142 pixel_slice: &[SourcePixelType],
143 width: u32,
144 height: u32,
145 ) -> Self
146 where
147 [SourcePixelType]: rgb::AsPixels<Pixel>,
148 {
149 use rgb::AsPixels;
150 let data: SharedVector<Pixel> = pixel_slice.as_pixels().into();
151 assert_eq!(
152 data.len() as u64,
153 width as u64 * height as u64,
154 "SharedPixelBuffer::clone_from_slice: the slice does not cover the requested {width}x{height} pixels",
155 );
156 Self { width, height, data }
157 }
158}
159
160pub type Rgb8Pixel = rgb::RGB8;
163pub type Rgba8Pixel = rgb::RGBA8;
166
167#[derive(Clone, Debug)]
172#[repr(C)]
173pub enum SharedImageBuffer {
175 RGB8(SharedPixelBuffer<Rgb8Pixel>),
178 RGBA8(SharedPixelBuffer<Rgba8Pixel>),
181 RGBA8Premultiplied(SharedPixelBuffer<Rgba8Pixel>),
188}
189
190impl SharedImageBuffer {
191 #[inline]
193 pub fn width(&self) -> u32 {
194 match self {
195 Self::RGB8(buffer) => buffer.width(),
196 Self::RGBA8(buffer) => buffer.width(),
197 Self::RGBA8Premultiplied(buffer) => buffer.width(),
198 }
199 }
200
201 #[inline]
203 pub fn height(&self) -> u32 {
204 match self {
205 Self::RGB8(buffer) => buffer.height(),
206 Self::RGBA8(buffer) => buffer.height(),
207 Self::RGBA8Premultiplied(buffer) => buffer.height(),
208 }
209 }
210
211 #[inline]
213 pub fn size(&self) -> IntSize {
214 match self {
215 Self::RGB8(buffer) => buffer.size(),
216 Self::RGBA8(buffer) => buffer.size(),
217 Self::RGBA8Premultiplied(buffer) => buffer.size(),
218 }
219 }
220}
221
222impl PartialEq for SharedImageBuffer {
223 fn eq(&self, other: &Self) -> bool {
224 match self {
225 Self::RGB8(lhs_buffer) => {
226 matches!(other, Self::RGB8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
227 }
228 Self::RGBA8(lhs_buffer) => {
229 matches!(other, Self::RGBA8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
230 }
231 Self::RGBA8Premultiplied(lhs_buffer) => {
232 matches!(other, Self::RGBA8Premultiplied(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
233 }
234 }
235 }
236}
237
238#[repr(u8)]
239#[derive(Clone, PartialEq, Debug, Copy)]
240pub enum TexturePixelFormat {
242 Rgb,
244 Rgba,
246 RgbaPremultiplied,
248 AlphaMap,
250 SignedDistanceField,
255}
256
257impl TexturePixelFormat {
258 pub fn bpp(self) -> usize {
260 match self {
261 TexturePixelFormat::Rgb => 3,
262 TexturePixelFormat::Rgba => 4,
263 TexturePixelFormat::RgbaPremultiplied => 4,
264 TexturePixelFormat::AlphaMap => 1,
265 TexturePixelFormat::SignedDistanceField => 1,
266 }
267 }
268}
269
270#[repr(C)]
271#[derive(Clone, PartialEq, Debug)]
272pub struct StaticTexture {
274 pub rect: IntRect,
276 pub format: TexturePixelFormat,
278 pub color: crate::Color,
280 pub index: usize,
282}
283
284#[repr(C)]
286#[derive(Clone, PartialEq, Debug)]
287pub struct StaticTextures {
288 pub size: IntSize,
291 pub original_size: IntSize,
293 pub data: Slice<'static, u8>,
295 pub textures: Slice<'static, StaticTexture>,
297}
298
299#[derive(PartialEq, Eq, Debug, Hash, Clone)]
302#[repr(C)]
303#[cfg(any(feature = "std", feature = "ffi"))]
304pub struct CachedPath {
305 path: SharedString,
306 last_modified: u32,
308}
309
310#[cfg(all(feature = "image-decoders", not(target_arch = "wasm32")))]
311impl CachedPath {
312 fn new<P: AsRef<std::path::Path>>(path: P) -> Self {
313 let path_str = path.as_ref().to_string_lossy().as_ref().into();
314 let timestamp = std::fs::metadata(path)
315 .and_then(|md| md.modified())
316 .unwrap_or(std::time::UNIX_EPOCH)
317 .duration_since(std::time::UNIX_EPOCH)
318 .map(|t| t.as_secs() as u32)
319 .unwrap_or_default();
320 Self { path: path_str, last_modified: timestamp }
321 }
322}
323
324#[derive(PartialEq, Eq, Debug, Hash, Clone)]
327#[repr(u8)]
328pub enum ImageCacheKey {
329 Invalid = 0,
332 #[cfg(any(feature = "std", feature = "ffi"))]
333 Path(CachedPath) = 1,
335 #[cfg(target_arch = "wasm32")]
337 URL(SharedString) = 2,
338 EmbeddedData(usize) = 3,
340}
341
342impl ImageCacheKey {
343 pub fn new(resource: &ImageInner) -> Option<Self> {
346 let key = match resource {
347 ImageInner::None => return None,
348 ImageInner::EmbeddedImage { cache_key, .. } => cache_key.clone(),
349 ImageInner::StaticTextures(textures) => {
350 Self::from_embedded_image_data(textures.data.as_slice())
351 }
352 #[cfg(feature = "svg")]
353 ImageInner::Svg(parsed_svg) => parsed_svg.cache_key(),
354 #[cfg(target_arch = "wasm32")]
355 ImageInner::HTMLImage(htmlimage) => Self::URL(htmlimage.source().into()),
356 ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).cache_key(),
357 #[cfg(not(target_arch = "wasm32"))]
358 ImageInner::BorrowedOpenGLTexture(..) => return None,
359 ImageInner::NineSlice(nine) => vtable::VRc::borrow(nine).cache_key(),
360 #[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
361 ImageInner::WGPUTexture(..) => return None,
362 };
363 if matches!(key, ImageCacheKey::Invalid) { None } else { Some(key) }
364 }
365
366 pub fn from_embedded_image_data(data: &'static [u8]) -> Self {
368 Self::EmbeddedData(data.as_ptr() as usize)
369 }
370}
371
372pub struct NineSliceImage(pub ImageInner, pub [u16; 4]);
374
375impl NineSliceImage {
376 pub fn image(&self) -> Image {
378 Image(self.0.clone())
379 }
380}
381
382impl OpaqueImage for NineSliceImage {
383 fn size(&self) -> IntSize {
384 self.0.size()
385 }
386 fn cache_key(&self) -> ImageCacheKey {
387 ImageCacheKey::new(&self.0).unwrap_or(ImageCacheKey::Invalid)
388 }
389}
390
391#[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
393#[derive(Clone, Debug)]
394pub enum WGPUTexture {
395 #[cfg(feature = "unstable-wgpu-29")]
397 WGPU29Texture(wgpu_29::Texture),
398 #[cfg(feature = "unstable-wgpu-30")]
400 WGPU30Texture(wgpu_30::Texture),
401}
402
403#[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
404impl OpaqueImage for WGPUTexture {
405 fn size(&self) -> IntSize {
406 match self {
407 #[cfg(feature = "unstable-wgpu-29")]
408 Self::WGPU29Texture(texture) => {
409 let size = texture.size();
410 (size.width, size.height).into()
411 }
412 #[cfg(feature = "unstable-wgpu-30")]
413 Self::WGPU30Texture(texture) => {
414 let size = texture.size();
415 (size.width, size.height).into()
416 }
417 }
418 }
419 fn cache_key(&self) -> ImageCacheKey {
420 ImageCacheKey::Invalid
421 }
422}
423
424#[derive(Clone, Debug, Default)]
429#[repr(u8)]
430#[allow(missing_docs)]
431pub enum ImageInner {
432 #[default]
434 None = 0,
435 EmbeddedImage {
436 cache_key: ImageCacheKey,
437 buffer: SharedImageBuffer,
438 } = 1,
439 #[cfg(feature = "svg")]
440 Svg(vtable::VRc<OpaqueImageVTable, svg::ParsedSVG>) = 2,
441 StaticTextures(&'static StaticTextures) = 3,
442 #[cfg(target_arch = "wasm32")]
443 HTMLImage(vtable::VRc<OpaqueImageVTable, htmlimage::HTMLImage>) = 4,
444 BackendStorage(vtable::VRc<OpaqueImageVTable>) = 5,
445 #[cfg(not(target_arch = "wasm32"))]
446 BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6,
447 NineSlice(vtable::VRc<OpaqueImageVTable, NineSliceImage>) = 7,
448 #[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
449 WGPUTexture(WGPUTexture) = 8,
450}
451
452impl ImageInner {
453 pub fn render_to_buffer(
460 &self,
461 _target_size_for_scalable_source: Option<euclid::Size2D<u32, PhysicalPx>>,
462 ) -> Option<SharedImageBuffer> {
463 match self {
464 ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()),
465 #[cfg(feature = "svg")]
466 ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) {
467 Ok(b) => Some(b),
468 Err(resvg::usvg::Error::InvalidSize) => None,
470 Err(err) => {
471 std::eprintln!("Error rendering SVG: {err}");
472 None
473 }
474 },
475 ImageInner::StaticTextures(ts) => {
476 let mut buffer =
477 SharedPixelBuffer::<Rgba8Pixel>::new(ts.size.width, ts.size.height);
478 let stride = buffer.width() as usize;
479 let slice = buffer.make_mut_slice();
480 for t in ts.textures.iter() {
481 let rect = t.rect.to_usize();
482 for y in 0..rect.height() {
483 let slice = &mut slice[(rect.min_y() + y) * stride..][rect.x_range()];
484 let source = &ts.data[t.index + y * rect.width() * t.format.bpp()..];
485 match t.format {
486 TexturePixelFormat::Rgb => {
487 let mut iter = source
488 .as_chunks::<3>()
489 .0
490 .iter()
491 .map(|p| Rgba8Pixel { r: p[0], g: p[1], b: p[2], a: 255 });
492 slice.fill_with(|| iter.next().unwrap());
493 }
494 TexturePixelFormat::RgbaPremultiplied => {
495 let mut iter = source
496 .as_chunks::<4>()
497 .0
498 .iter()
499 .map(|p| Rgba8Pixel { r: p[0], g: p[1], b: p[2], a: p[3] });
500 slice.fill_with(|| iter.next().unwrap());
501 }
502 TexturePixelFormat::Rgba => {
503 let mut iter = source.as_chunks::<4>().0.iter().map(|p| {
504 let a = p[3];
505 Rgba8Pixel {
506 r: (p[0] as u16 * a as u16 / 255) as u8,
507 g: (p[1] as u16 * a as u16 / 255) as u8,
508 b: (p[2] as u16 * a as u16 / 255) as u8,
509 a,
510 }
511 });
512 slice.fill_with(|| iter.next().unwrap());
513 }
514 TexturePixelFormat::AlphaMap => {
515 let col = t.color.to_argb_u8();
516 let mut iter = source.iter().map(|p| {
517 let a = *p as u32 * col.alpha as u32;
518 Rgba8Pixel {
519 r: (col.red as u32 * a / (255 * 255)) as u8,
520 g: (col.green as u32 * a / (255 * 255)) as u8,
521 b: (col.blue as u32 * a / (255 * 255)) as u8,
522 a: (a / 255) as u8,
523 }
524 });
525 slice.fill_with(|| iter.next().unwrap());
526 }
527 TexturePixelFormat::SignedDistanceField => {
528 todo!("converting from a signed distance field to an image")
529 }
530 };
531 }
532 }
533 Some(SharedImageBuffer::RGBA8Premultiplied(buffer))
534 }
535 ImageInner::NineSlice(nine) => nine.0.render_to_buffer(None),
536 _ => None,
537 }
538 }
539
540 pub fn is_svg(&self) -> bool {
542 match self {
543 #[cfg(feature = "svg")]
544 Self::Svg(_) => true,
545 #[cfg(target_arch = "wasm32")]
546 Self::HTMLImage(html_image) => html_image.is_svg(),
547 _ => false,
548 }
549 }
550
551 pub fn size(&self) -> IntSize {
553 match self {
554 ImageInner::None => Default::default(),
555 ImageInner::EmbeddedImage { buffer, .. } => buffer.size(),
556 ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
557 #[cfg(feature = "svg")]
558 ImageInner::Svg(svg) => svg.size(),
559 #[cfg(target_arch = "wasm32")]
560 ImageInner::HTMLImage(htmlimage) => htmlimage.size().unwrap_or_default(),
561 ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size(),
562 #[cfg(not(target_arch = "wasm32"))]
563 ImageInner::BorrowedOpenGLTexture(BorrowedOpenGLTexture { size, .. }) => *size,
564 ImageInner::NineSlice(nine) => nine.0.size(),
565 #[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
566 ImageInner::WGPUTexture(texture) => texture.size(),
567 }
568 }
569
570 #[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
577 pub(crate) fn load_from_data_with_cache_key(
578 cache_key: ImageCacheKey,
579 data: Slice<'_, u8>,
580 format: Slice<'_, u8>,
581 ) -> Option<Self> {
582 #[cfg(target_arch = "wasm32")]
584 {
585 let _ = cache_key;
586 let mime_type = core::str::from_utf8(format.as_slice())
587 .ok()
588 .and_then(image_mime_type_from_extension)
589 .unwrap_or_else(|| {
590 if data.starts_with(b"<?xml") || data.starts_with(b"<svg") {
591 "image/svg+xml"
592 } else {
593 ""
595 }
596 });
597 if mime_type == "image/svg+xml" && data.starts_with(&[0x1f, 0x8b]) {
598 crate::debug_log!("Compressed SVG (.svgz) is not supported on the web");
599 return None;
600 }
601 return htmlimage::HTMLImage::new_from_data(data.as_slice(), mime_type)
602 .map(|html_image| ImageInner::HTMLImage(vtable::VRc::new(html_image)));
603 }
604
605 #[cfg(not(target_arch = "wasm32"))]
606 {
607 #[cfg(feature = "svg")]
608 if format.as_slice() == b"svg"
609 || format.as_slice() == b"svgz"
610 || (format.is_empty() && (data.starts_with(b"<?xml") || data.starts_with(b"<svg")))
611 {
612 return Some(ImageInner::Svg(vtable::VRc::new(
613 svg::load_from_data(data.as_slice(), cache_key).map_or_else(
614 |svg_err| {
615 crate::debug_log!("Error loading SVG: {}", svg_err);
616 None
617 },
618 Some,
619 )?,
620 )));
621 }
622
623 let format = std::str::from_utf8(format.as_slice())
624 .ok()
625 .and_then(image::ImageFormat::from_extension);
626 let maybe_image = if let Some(format) = format {
627 image::load_from_memory_with_format(data.as_slice(), format)
628 } else {
629 image::load_from_memory(data.as_slice())
630 };
631
632 match maybe_image {
633 Ok(image) => Some(ImageInner::EmbeddedImage {
634 cache_key,
635 buffer: dynamic_image_to_shared_image_buffer(image),
636 }),
637 Err(decode_err) => {
638 crate::debug_log!("Error decoding embedded image: {}", decode_err);
639 None
640 }
641 }
642 }
643 }
644}
645
646#[cfg(all(feature = "image-decoders", not(target_arch = "wasm32")))]
648fn dynamic_image_to_shared_image_buffer(dynamic_image: image::DynamicImage) -> SharedImageBuffer {
649 use rgb::AsPixels;
650
651 if dynamic_image.color().has_alpha() {
652 let rgba8image = dynamic_image.to_rgba8();
653 SharedImageBuffer::RGBA8Premultiplied(SharedPixelBuffer {
656 width: rgba8image.width(),
657 height: rgba8image.height(),
658 data: rgba8image
659 .as_pixels()
660 .iter()
661 .map(|pixel| Image::rgba_to_premultiplied_rgba(*pixel))
662 .collect(),
663 })
664 } else {
665 let rgb8image = dynamic_image.to_rgb8();
666 SharedImageBuffer::RGB8(SharedPixelBuffer::clone_from_slice(
667 rgb8image.as_raw(),
668 rgb8image.width(),
669 rgb8image.height(),
670 ))
671 }
672}
673
674impl PartialEq for ImageInner {
675 fn eq(&self, other: &Self) -> bool {
676 match (self, other) {
677 (
678 Self::EmbeddedImage { cache_key: l_cache_key, buffer: l_buffer },
679 Self::EmbeddedImage { cache_key: r_cache_key, buffer: r_buffer },
680 ) => l_cache_key == r_cache_key && l_buffer == r_buffer,
681 #[cfg(feature = "svg")]
682 (Self::Svg(l0), Self::Svg(r0)) => vtable::VRc::ptr_eq(l0, r0),
683 (Self::StaticTextures(l0), Self::StaticTextures(r0)) => l0 == r0,
684 #[cfg(target_arch = "wasm32")]
685 (Self::HTMLImage(l0), Self::HTMLImage(r0)) => vtable::VRc::ptr_eq(l0, r0),
686 (Self::BackendStorage(l0), Self::BackendStorage(r0)) => vtable::VRc::ptr_eq(l0, r0),
687 #[cfg(not(target_arch = "wasm32"))]
688 (Self::BorrowedOpenGLTexture(l0), Self::BorrowedOpenGLTexture(r0)) => l0 == r0,
689 (Self::NineSlice(l), Self::NineSlice(r)) => l.0 == r.0 && l.1 == r.1,
690 _ => false,
691 }
692 }
693}
694
695impl<'a> From<&'a Image> for &'a ImageInner {
696 fn from(other: &'a Image) -> Self {
697 &other.0
698 }
699}
700
701#[derive(Default, Debug, PartialEq)]
703pub struct LoadImageError(());
704
705impl core::fmt::Display for LoadImageError {
706 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
707 f.write_str("The image cannot be loaded")
708 }
709}
710
711#[cfg(feature = "std")]
712impl std::error::Error for LoadImageError {}
713
714#[repr(transparent)]
811#[derive(Default, Clone, Debug, PartialEq, derive_more::From)]
812pub struct Image(pub(crate) ImageInner);
813
814impl Image {
815 #[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
816 pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> {
825 self::cache::IMAGE_CACHE.with(|global_cache| {
826 let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into();
827 global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(()))
828 })
829 }
830
831 pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
834 Image(ImageInner::EmbeddedImage {
835 cache_key: ImageCacheKey::Invalid,
836 buffer: SharedImageBuffer::RGB8(buffer),
837 })
838 }
839
840 pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
843 Image(ImageInner::EmbeddedImage {
844 cache_key: ImageCacheKey::Invalid,
845 buffer: SharedImageBuffer::RGBA8(buffer),
846 })
847 }
848
849 pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
855 Image(ImageInner::EmbeddedImage {
856 cache_key: ImageCacheKey::Invalid,
857 buffer: SharedImageBuffer::RGBA8Premultiplied(buffer),
858 })
859 }
860
861 pub fn to_rgb8(&self) -> Option<SharedPixelBuffer<Rgb8Pixel>> {
864 self.0.render_to_buffer(None).and_then(|image| match image {
865 SharedImageBuffer::RGB8(buffer) => Some(buffer),
866 _ => None,
867 })
868 }
869
870 pub fn to_rgba8(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
873 self.render_to_rgba8(None)
874 }
875
876 fn render_to_rgba8(
877 &self,
878 target_size: Option<euclid::Size2D<u32, PhysicalPx>>,
879 ) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
880 self.0.render_to_buffer(target_size).map(|image| match image {
881 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
882 width: buffer.width,
883 height: buffer.height,
884 data: buffer.data.into_iter().map(Into::into).collect(),
885 },
886 SharedImageBuffer::RGBA8(buffer) => buffer,
887 SharedImageBuffer::RGBA8Premultiplied(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
888 width: buffer.width,
889 height: buffer.height,
890 data: buffer.data.into_iter().map(Image::premultiplied_rgba_to_rgba).collect(),
891 },
892 })
893 }
894
895 pub fn to_rgba8_premultiplied(&self) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
899 self.0.render_to_buffer(None).map(|image| match image {
900 SharedImageBuffer::RGB8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
901 width: buffer.width,
902 height: buffer.height,
903 data: buffer.data.into_iter().map(Into::into).collect(),
904 },
905 SharedImageBuffer::RGBA8(buffer) => SharedPixelBuffer::<Rgba8Pixel> {
906 width: buffer.width,
907 height: buffer.height,
908 data: buffer.data.into_iter().map(Image::rgba_to_premultiplied_rgba).collect(),
909 },
910 SharedImageBuffer::RGBA8Premultiplied(buffer) => buffer,
911 })
912 }
913
914 fn premultiplied_rgba_to_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
916 if pixel.a == 0 {
917 Rgba8Pixel::new(0, 0, 0, 0)
918 } else {
919 let af = pixel.a as u32;
920 let round = (af / 2) as u32;
921 Rgba8Pixel {
922 r: ((pixel.r as u32 * 255 + round) / af).min(255) as u8,
923 g: ((pixel.g as u32 * 255 + round) / af).min(255) as u8,
924 b: ((pixel.b as u32 * 255 + round) / af).min(255) as u8,
925 a: pixel.a,
926 }
927 }
928 }
929
930 fn rgba_to_premultiplied_rgba(pixel: Rgba8Pixel) -> Rgba8Pixel {
932 if pixel.a == 255 {
933 pixel
934 } else {
935 let af = pixel.a as u32;
936 Rgba8Pixel {
937 r: (((pixel.r as u32 * af + 128) * 257) >> 16) as u8,
938 g: (((pixel.g as u32 * af + 128) * 257) >> 16) as u8,
939 b: (((pixel.b as u32 * af + 128) * 257) >> 16) as u8,
940 a: pixel.a,
941 }
942 }
943 }
944
945 #[cfg(feature = "unstable-wgpu-29")]
951 pub fn to_wgpu_29_texture(&self) -> Option<wgpu_29::Texture> {
952 match &self.0 {
953 ImageInner::WGPUTexture(WGPUTexture::WGPU29Texture(texture)) => Some(texture.clone()),
954 _ => None,
955 }
956 }
957
958 #[cfg(feature = "unstable-wgpu-30")]
964 pub fn to_wgpu_30_texture(&self) -> Option<wgpu_30::Texture> {
965 match &self.0 {
966 ImageInner::WGPUTexture(WGPUTexture::WGPU30Texture(texture)) => Some(texture.clone()),
967 _ => None,
968 }
969 }
970
971 #[allow(unsafe_code)]
991 #[cfg(not(target_arch = "wasm32"))]
992 #[deprecated(since = "1.2.0", note = "Use BorrowedOpenGLTextureBuilder")]
993 pub unsafe fn from_borrowed_gl_2d_rgba_texture(
994 texture_id: core::num::NonZeroU32,
995 size: IntSize,
996 ) -> Self {
997 unsafe { BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size).build() }
998 }
999
1000 #[cfg(any(feature = "svg", target_arch = "wasm32"))]
1004 pub fn load_from_svg_data(buffer: &[u8]) -> Result<Self, LoadImageError> {
1005 #[cfg(target_arch = "wasm32")]
1007 {
1008 htmlimage::HTMLImage::new_from_data(buffer, "image/svg+xml")
1009 .map(|html_image| Image(ImageInner::HTMLImage(vtable::VRc::new(html_image))))
1010 .ok_or(LoadImageError(()))
1011 }
1012 #[cfg(not(target_arch = "wasm32"))]
1013 {
1014 let cache_key = ImageCacheKey::Invalid;
1015 Ok(Image(ImageInner::Svg(vtable::VRc::new(
1016 svg::load_from_data(buffer, cache_key).map_err(|_| LoadImageError(()))?,
1017 ))))
1018 }
1019 }
1020
1021 #[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
1030 pub fn load_from_data(data: &[u8], format: Option<&str>) -> Result<Self, LoadImageError> {
1031 ImageInner::load_from_data_with_cache_key(
1032 ImageCacheKey::Invalid,
1033 Slice::from_slice(data),
1034 Slice::from_slice(format.unwrap_or_default().as_bytes()),
1035 )
1036 .map(Image)
1037 .ok_or(LoadImageError(()))
1038 }
1039
1040 pub fn set_nine_slice_edges(&mut self, top: u16, right: u16, bottom: u16, left: u16) {
1046 if top == 0 && left == 0 && right == 0 && bottom == 0 {
1047 if let ImageInner::NineSlice(n) = &self.0 {
1048 self.0 = n.0.clone();
1049 }
1050 } else {
1051 let array = [top, right, bottom, left];
1052 let inner = if let ImageInner::NineSlice(n) = &mut self.0 {
1053 n.0.clone()
1054 } else {
1055 self.0.clone()
1056 };
1057 self.0 = ImageInner::NineSlice(vtable::VRc::new(NineSliceImage(inner, array)));
1058 }
1059 }
1060
1061 pub fn size(&self) -> IntSize {
1063 self.0.size()
1064 }
1065
1066 #[cfg(feature = "std")]
1067 pub fn path(&self) -> Option<&std::path::Path> {
1079 match &self.0 {
1080 ImageInner::EmbeddedImage {
1081 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1082 ..
1083 } => Some(std::path::Path::new(path.as_str())),
1084 ImageInner::NineSlice(nine) => match &nine.0 {
1085 ImageInner::EmbeddedImage {
1086 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1087 ..
1088 } => Some(std::path::Path::new(path.as_str())),
1089 _ => None,
1090 },
1091 _ => None,
1092 }
1093 }
1094}
1095
1096pub fn image_to_rgba8_with_target_size(
1100 image: &Image,
1101 target_size: IntSize,
1102) -> Option<SharedPixelBuffer<Rgba8Pixel>> {
1103 image.render_to_rgba8(Some(target_size.cast_unit()))
1104}
1105
1106#[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
1107pub fn load_image_from_data_uri(
1110 uri: &str,
1111 bytes: &[u8],
1112 format: &str,
1113) -> Result<Image, LoadImageError> {
1114 #[cfg(target_arch = "wasm32")]
1117 {
1118 self::cache::IMAGE_CACHE.with(|global_cache| {
1119 global_cache
1120 .borrow_mut()
1121 .load_image_from_data_uri(uri, bytes, format)
1122 .ok_or(LoadImageError(()))
1123 })
1124 }
1125 #[cfg(not(target_arch = "wasm32"))]
1127 {
1128 let _ = uri;
1129 ImageInner::load_from_data_with_cache_key(
1130 ImageCacheKey::Invalid,
1131 bytes.into(),
1132 format.as_bytes().into(),
1133 )
1134 .map(Image)
1135 .ok_or(Default::default())
1136 }
1137}
1138
1139pub fn image_mime_type_from_extension(extension: &str) -> Option<&'static str> {
1142 for (ext, mime) in [
1143 ("png", "image/png"),
1144 ("jpg", "image/jpeg"),
1145 ("jpeg", "image/jpeg"),
1146 ("svg", "image/svg+xml"),
1147 ("svgz", "image/svg+xml"),
1148 ("gif", "image/gif"),
1149 ("webp", "image/webp"),
1150 ("bmp", "image/bmp"),
1151 ("ico", "image/x-icon"),
1152 ("avif", "image/avif"),
1153 ] {
1154 if extension.eq_ignore_ascii_case(ext) {
1155 return Some(mime);
1156 }
1157 }
1158 None
1159}
1160
1161#[derive(Copy, Clone, Debug, PartialEq, Default)]
1164#[repr(u8)]
1165#[non_exhaustive]
1166pub enum BorrowedOpenGLTextureOrigin {
1167 #[default]
1169 TopLeft,
1170 BottomLeft,
1173}
1174
1175#[cfg(not(target_arch = "wasm32"))]
1193pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture);
1194
1195#[cfg(not(target_arch = "wasm32"))]
1196impl BorrowedOpenGLTextureBuilder {
1197 #[allow(unsafe_code)]
1215 pub unsafe fn new_gl_2d_rgba_texture(texture_id: core::num::NonZeroU32, size: IntSize) -> Self {
1216 Self(BorrowedOpenGLTexture { texture_id, size, origin: Default::default() })
1217 }
1218
1219 pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self {
1221 self.0.origin = origin;
1222 self
1223 }
1224
1225 pub fn build(self) -> Image {
1227 Image(ImageInner::BorrowedOpenGLTexture(self.0))
1228 }
1229}
1230
1231#[cfg(all(target_arch = "wasm32", feature = "std"))]
1237pub fn load_as_html_image(url: &str) -> Result<Image, LoadImageError> {
1238 self::cache::IMAGE_CACHE.with(|global_cache| {
1239 global_cache.borrow_mut().load_as_html_image(url).ok_or(LoadImageError(()))
1240 })
1241}
1242
1243#[cfg(any(feature = "image-decoders", all(target_arch = "wasm32", feature = "std")))]
1246pub fn load_image_from_embedded_data(data: Slice<'static, u8>, format: Slice<'_, u8>) -> Image {
1247 self::cache::IMAGE_CACHE.with(|global_cache| {
1248 global_cache.borrow_mut().load_image_from_embedded_data(data, format).unwrap_or_default()
1249 })
1250}
1251
1252#[test]
1253fn test_image_size_from_buffer_without_backend() {
1254 {
1255 assert_eq!(Image::default().size(), Default::default());
1256 assert!(Image::default().to_rgb8().is_none());
1257 assert!(Image::default().to_rgba8().is_none());
1258 assert!(Image::default().to_rgba8_premultiplied().is_none());
1259 }
1260 {
1261 let buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
1262 let image = Image::from_rgb8(buffer.clone());
1263 assert_eq!(image.size(), [320, 200].into());
1264 assert_eq!(image.to_rgb8().as_ref().map(|b| b.as_slice()), Some(buffer.as_slice()));
1265 }
1266}
1267
1268#[cfg(feature = "svg")]
1269#[test]
1270#[cfg_attr(miri, ignore)]
1272fn test_image_size_from_svg() {
1273 let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
1274 let image = Image::load_from_svg_data(simple_svg.as_bytes()).unwrap();
1275 assert_eq!(image.size(), [320, 200].into());
1276 assert_eq!(image.to_rgba8().unwrap().size(), image.size());
1277}
1278
1279#[cfg(feature = "svg")]
1280#[test]
1281#[cfg_attr(miri, ignore)]
1283fn test_image_invalid_svg() {
1284 let invalid_svg = r#"AaBbCcDd"#;
1285 let result = Image::load_from_svg_data(invalid_svg.as_bytes());
1286 assert!(result.is_err());
1287}
1288
1289#[cfg(feature = "svg")]
1290#[test]
1291#[cfg_attr(miri, ignore)]
1293fn test_image_load_from_data_svg() {
1294 let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
1295 let guessed = Image::load_from_data(simple_svg.as_bytes(), None).unwrap();
1297 assert_eq!(guessed.size(), [320, 200].into());
1298 let hinted = Image::load_from_data(simple_svg.as_bytes(), Some("svg")).unwrap();
1300 assert_eq!(hinted.size(), [320, 200].into());
1301}
1302
1303#[cfg(feature = "svg")]
1304#[test]
1305#[cfg_attr(miri, ignore)]
1307fn test_image_load_from_data_svgz() {
1308 const SIMPLE_SVGZ: &[u8] = &[
1311 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0xb3, 0x29, 0x2e, 0x4b, 0x57,
1312 0x28, 0xcf, 0x4c, 0x29, 0xc9, 0xb0, 0x55, 0x32, 0x36, 0x32, 0x50, 0x52, 0xc8, 0x48, 0xcd,
1313 0x4c, 0xcf, 0x28, 0xb1, 0x55, 0x32, 0x32, 0x00, 0x72, 0x2a, 0x72, 0x73, 0xf2, 0x8a, 0x6d,
1314 0x95, 0x32, 0x4a, 0x4a, 0x0a, 0xac, 0xf4, 0xf5, 0xcb, 0xcb, 0xcb, 0xf5, 0xca, 0x8d, 0xf5,
1315 0xf2, 0x8b, 0xd2, 0xf5, 0x81, 0xb2, 0x06, 0xfa, 0x40, 0xad, 0x4a, 0x76, 0x36, 0x20, 0xca,
1316 0x0e, 0x00, 0x37, 0x91, 0x7a, 0xd6, 0x47, 0x00, 0x00, 0x00,
1317 ];
1318 let image = Image::load_from_data(SIMPLE_SVGZ, Some("svgz")).unwrap();
1319 assert_eq!(image.size(), [320, 200].into());
1320}
1321
1322#[cfg(feature = "image-decoders")]
1323#[test]
1324#[cfg_attr(miri, ignore)]
1325fn test_image_load_from_data_png() {
1326 let mut png = std::io::Cursor::new(std::vec::Vec::new());
1327 image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(2, 3, image::Rgb([0, 255, 0])))
1328 .write_to(&mut png, image::ImageFormat::Png)
1329 .unwrap();
1330 let png = png.into_inner();
1331
1332 let guessed = Image::load_from_data(&png, None).unwrap();
1334 assert_eq!(guessed.size(), [2, 3].into());
1335 let hinted = Image::load_from_data(&png, Some("png")).unwrap();
1337 assert_eq!(hinted.size(), [2, 3].into());
1338
1339 assert!(Image::load_from_data(b"not an image", None).is_err());
1340}
1341
1342#[derive(Debug)]
1344pub struct FitResult {
1345 pub clip_rect: IntRect,
1347 pub source_to_target_x: f32,
1349 pub source_to_target_y: f32,
1351 pub size: euclid::Size2D<f32, PhysicalPx>,
1353 pub offset: euclid::Point2D<f32, PhysicalPx>,
1355 pub tiled: Option<euclid::default::Point2D<u32>>,
1359}
1360
1361impl FitResult {
1362 fn adjust_for_tiling(
1363 self,
1364 ratio: f32,
1365 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1366 tiling: (ImageTiling, ImageTiling),
1367 ) -> Self {
1368 let mut r = self;
1369 let mut tiled = euclid::Point2D::default();
1370 let target = r.size;
1371 let o = r.clip_rect.size.cast::<f32>();
1372 match tiling.0 {
1373 ImageTiling::None => {
1374 r.size.width = o.width * r.source_to_target_x;
1375 if (o.width as f32) > target.width / r.source_to_target_x {
1376 let diff = (o.width as f32 - target.width / r.source_to_target_x) as i32;
1377 r.clip_rect.size.width -= diff;
1378 r.clip_rect.origin.x += match alignment.0 {
1379 ImageHorizontalAlignment::Center => diff / 2,
1380 ImageHorizontalAlignment::Left => 0,
1381 ImageHorizontalAlignment::Right => diff,
1382 };
1383 r.size.width = target.width;
1384 } else if (o.width as f32) < target.width / r.source_to_target_x {
1385 r.offset.x += match alignment.0 {
1386 ImageHorizontalAlignment::Center => {
1387 (target.width - o.width as f32 * r.source_to_target_x) / 2.
1388 }
1389 ImageHorizontalAlignment::Left => 0.,
1390 ImageHorizontalAlignment::Right => {
1391 target.width - o.width as f32 * r.source_to_target_x
1392 }
1393 };
1394 }
1395 }
1396 ImageTiling::Repeat => {
1397 tiled.x = match alignment.0 {
1398 ImageHorizontalAlignment::Left => 0,
1399 ImageHorizontalAlignment::Center => {
1400 ((o.width - target.width / ratio) / 2.).rem_euclid(o.width) as u32
1401 }
1402 ImageHorizontalAlignment::Right => {
1403 (-target.width / ratio).rem_euclid(o.width) as u32
1404 }
1405 };
1406 r.source_to_target_x = ratio;
1407 }
1408 ImageTiling::Round => {
1409 if target.width / ratio <= o.width * 1.5 {
1410 r.source_to_target_x = target.width / o.width;
1411 } else {
1412 let mut rem = (target.width / ratio).rem_euclid(o.width);
1413 if rem > o.width / 2. {
1414 rem -= o.width;
1415 }
1416 r.source_to_target_x = ratio * target.width / (target.width - rem * ratio);
1417 }
1418 }
1419 }
1420
1421 match tiling.1 {
1422 ImageTiling::None => {
1423 r.size.height = o.height * r.source_to_target_y;
1424 if (o.height as f32) > target.height / r.source_to_target_y {
1425 let diff = (o.height as f32 - target.height / r.source_to_target_y) as i32;
1426 r.clip_rect.size.height -= diff;
1427 r.clip_rect.origin.y += match alignment.1 {
1428 ImageVerticalAlignment::Center => diff / 2,
1429 ImageVerticalAlignment::Top => 0,
1430 ImageVerticalAlignment::Bottom => diff,
1431 };
1432 r.size.height = target.height;
1433 } else if (o.height as f32) < target.height / r.source_to_target_y {
1434 r.offset.y += match alignment.1 {
1435 ImageVerticalAlignment::Center => {
1436 (target.height - o.height as f32 * r.source_to_target_y) / 2.
1437 }
1438 ImageVerticalAlignment::Top => 0.,
1439 ImageVerticalAlignment::Bottom => {
1440 target.height - o.height as f32 * r.source_to_target_y
1441 }
1442 };
1443 }
1444 }
1445 ImageTiling::Repeat => {
1446 tiled.y = match alignment.1 {
1447 ImageVerticalAlignment::Top => 0,
1448 ImageVerticalAlignment::Center => {
1449 ((o.height - target.height / ratio) / 2.).rem_euclid(o.height) as u32
1450 }
1451 ImageVerticalAlignment::Bottom => {
1452 (-target.height / ratio).rem_euclid(o.height) as u32
1453 }
1454 };
1455 r.source_to_target_y = ratio;
1456 }
1457 ImageTiling::Round => {
1458 if target.height / ratio <= o.height * 1.5 {
1459 r.source_to_target_y = target.height / o.height;
1460 } else {
1461 let mut rem = (target.height / ratio).rem_euclid(o.height);
1462 if rem > o.height / 2. {
1463 rem -= o.height;
1464 }
1465 r.source_to_target_y = ratio * target.height / (target.height - rem * ratio);
1466 }
1467 }
1468 }
1469 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1470 r.tiled = has_tiling.then_some(tiled);
1471 r
1472 }
1473}
1474
1475#[cfg(not(feature = "std"))]
1476trait RemEuclid {
1477 fn rem_euclid(self, b: f32) -> f32;
1478}
1479#[cfg(not(feature = "std"))]
1480impl RemEuclid for f32 {
1481 fn rem_euclid(self, b: f32) -> f32 {
1482 num_traits::Euclid::rem_euclid(&self, &b)
1483 }
1484}
1485
1486pub fn fit(
1488 image_fit: ImageFit,
1489 target: euclid::Size2D<f32, PhysicalPx>,
1490 source_rect: IntRect,
1491 scale_factor: ScaleFactor,
1492 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1493 tiling: (ImageTiling, ImageTiling),
1494) -> FitResult {
1495 let has_tiling = tiling != (ImageTiling::None, ImageTiling::None);
1496 let o = source_rect.size.cast::<f32>();
1497 let ratio = match image_fit {
1498 _ if has_tiling => scale_factor.get(),
1500 ImageFit::Fill => {
1501 return FitResult {
1502 clip_rect: source_rect,
1503 source_to_target_x: target.width / o.width,
1504 source_to_target_y: target.height / o.height,
1505 size: target,
1506 offset: Default::default(),
1507 tiled: None,
1508 };
1509 }
1510 ImageFit::Preserve => scale_factor.get(),
1511 ImageFit::Contain => f32::min(target.width / o.width, target.height / o.height),
1512 ImageFit::Cover => f32::max(target.width / o.width, target.height / o.height),
1513 };
1514
1515 FitResult {
1516 clip_rect: source_rect,
1517 source_to_target_x: ratio,
1518 source_to_target_y: ratio,
1519 size: target,
1520 offset: euclid::Point2D::default(),
1521 tiled: None,
1522 }
1523 .adjust_for_tiling(ratio, alignment, tiling)
1524}
1525
1526pub fn scalable_render_size(
1532 source_size: IntSize,
1533 image_fit: ImageFit,
1534 target: euclid::Size2D<f32, PhysicalPx>,
1535 scale_factor: ScaleFactor,
1536 tiling: (ImageTiling, ImageTiling),
1537) -> Option<euclid::Size2D<u32, PhysicalPx>> {
1538 let source = source_size.cast::<f32>();
1539 if source.is_empty() {
1540 return None;
1541 }
1542 let fit = fit(
1543 image_fit,
1544 target,
1545 IntRect::from_size(source_size.cast()),
1546 scale_factor,
1547 Default::default(),
1549 tiling,
1550 );
1551 let size = euclid::size2(
1552 (source.width * fit.source_to_target_x) as u32,
1553 (source.height * fit.source_to_target_y) as u32,
1554 );
1555 (!size.is_empty()).then_some(size)
1556}
1557
1558pub fn fit9slice(
1560 source_rect: IntSize,
1561 [t, r, b, l]: [u16; 4],
1562 target: euclid::Size2D<f32, PhysicalPx>,
1563 scale_factor: ScaleFactor,
1564 alignment: (ImageHorizontalAlignment, ImageVerticalAlignment),
1565 tiling: (ImageTiling, ImageTiling),
1566) -> impl Iterator<Item = FitResult> {
1567 let fit_to = |clip_rect: euclid::default::Rect<u16>, target: euclid::Rect<f32, PhysicalPx>| {
1568 (!clip_rect.is_empty() && !target.is_empty()).then(|| {
1569 FitResult {
1570 clip_rect: clip_rect.cast(),
1571 source_to_target_x: target.width() / clip_rect.width() as f32,
1572 source_to_target_y: target.height() / clip_rect.height() as f32,
1573 size: target.size,
1574 offset: target.origin,
1575 tiled: None,
1576 }
1577 .adjust_for_tiling(scale_factor.get(), alignment, tiling)
1578 })
1579 };
1580 use euclid::rect;
1581 let sf = |x| scale_factor.get() * x as f32;
1582 let source = source_rect.cast::<u16>();
1583 if t + b > source.height || l + r > source.width {
1584 [None, None, None, None, None, None, None, None, None]
1585 } else {
1586 [
1587 fit_to(rect(0, 0, l, t), rect(0., 0., sf(l), sf(t))),
1588 fit_to(
1589 rect(l, 0, source.width - l - r, t),
1590 rect(sf(l), 0., target.width - sf(l) - sf(r), sf(t)),
1591 ),
1592 fit_to(rect(source.width - r, 0, r, t), rect(target.width - sf(r), 0., sf(r), sf(t))),
1593 fit_to(
1594 rect(0, t, l, source.height - t - b),
1595 rect(0., sf(t), sf(l), target.height - sf(t) - sf(b)),
1596 ),
1597 fit_to(
1598 rect(l, t, source.width - l - r, source.height - t - b),
1599 rect(sf(l), sf(t), target.width - sf(l) - sf(r), target.height - sf(t) - sf(b)),
1600 ),
1601 fit_to(
1602 rect(source.width - r, t, r, source.height - t - b),
1603 rect(target.width - sf(r), sf(t), sf(r), target.height - sf(t) - sf(b)),
1604 ),
1605 fit_to(rect(0, source.height - b, l, b), rect(0., target.height - sf(b), sf(l), sf(b))),
1606 fit_to(
1607 rect(l, source.height - b, source.width - l - r, b),
1608 rect(sf(l), target.height - sf(b), target.width - sf(l) - sf(r), sf(b)),
1609 ),
1610 fit_to(
1611 rect(source.width - r, source.height - b, r, b),
1612 rect(target.width - sf(r), target.height - sf(b), sf(r), sf(b)),
1613 ),
1614 ]
1615 }
1616 .into_iter()
1617 .flatten()
1618}
1619
1620#[cfg(feature = "ffi")]
1621pub(crate) mod ffi {
1622 #![allow(unsafe_code)]
1623
1624 use super::*;
1625
1626 #[cfg(cbindgen)]
1629 #[repr(C)]
1630 struct Rgb8Pixel {
1631 r: u8,
1633 g: u8,
1635 b: u8,
1637 }
1638
1639 #[cfg(cbindgen)]
1642 #[repr(C)]
1643 struct Rgba8Pixel {
1644 r: u8,
1646 g: u8,
1648 b: u8,
1650 a: u8,
1652 }
1653
1654 #[cfg(all(feature = "std", feature = "image-decoders"))]
1657 #[unsafe(no_mangle)]
1658 pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) {
1659 unsafe {
1660 core::ptr::write(
1661 image,
1662 Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or_default(),
1663 )
1664 }
1665 }
1666
1667 #[cfg(all(feature = "std", feature = "image-decoders"))]
1668 #[unsafe(no_mangle)]
1669 pub unsafe extern "C" fn slint_image_load_from_embedded_data(
1670 data: Slice<'static, u8>,
1671 format: Slice<'static, u8>,
1672 image: *mut Image,
1673 ) {
1674 unsafe { core::ptr::write(image, super::load_image_from_embedded_data(data, format)) };
1675 }
1676
1677 #[cfg(all(feature = "std", feature = "image-decoders"))]
1680 #[unsafe(no_mangle)]
1681 pub unsafe extern "C" fn slint_image_load_from_data(
1682 data: Slice<'_, u8>,
1683 format: Slice<'_, u8>,
1684 image: *mut Image,
1685 ) {
1686 let format = core::str::from_utf8(format.as_slice()).ok();
1687 let loaded = super::Image::load_from_data(data.as_slice(), format).unwrap_or_default();
1688 unsafe { core::ptr::write(image, loaded) };
1689 }
1690
1691 #[unsafe(no_mangle)]
1692 pub extern "C" fn slint_image_size(image: &Image) -> IntSize {
1693 image.size()
1694 }
1695
1696 #[unsafe(no_mangle)]
1697 pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
1698 match &image.0 {
1699 #[cfg(feature = "std")]
1700 ImageInner::EmbeddedImage {
1701 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1702 ..
1703 } => Some(path),
1704 ImageInner::NineSlice(nine) => match &nine.0 {
1705 #[cfg(feature = "std")]
1706 ImageInner::EmbeddedImage {
1707 cache_key: ImageCacheKey::Path(CachedPath { path, .. }),
1708 ..
1709 } => Some(path),
1710 _ => None,
1711 },
1712 _ => None,
1713 }
1714 }
1715
1716 #[unsafe(no_mangle)]
1717 pub unsafe extern "C" fn slint_image_from_embedded_textures(
1718 textures: &'static StaticTextures,
1719 image: *mut Image,
1720 ) {
1721 unsafe { core::ptr::write(image, Image::from(ImageInner::StaticTextures(textures))) };
1722 }
1723
1724 #[unsafe(no_mangle)]
1725 pub extern "C" fn slint_image_compare_equal(image1: &Image, image2: &Image) -> bool {
1726 image1.eq(image2)
1727 }
1728
1729 #[unsafe(no_mangle)]
1731 pub extern "C" fn slint_image_set_nine_slice_edges(
1732 image: &mut Image,
1733 top: u16,
1734 right: u16,
1735 bottom: u16,
1736 left: u16,
1737 ) {
1738 image.set_nine_slice_edges(top, right, bottom, left);
1739 }
1740
1741 #[unsafe(no_mangle)]
1742 pub extern "C" fn slint_image_to_rgb8(
1743 image: &Image,
1744 data: &mut SharedVector<Rgb8Pixel>,
1745 width: &mut u32,
1746 height: &mut u32,
1747 ) -> bool {
1748 image.to_rgb8().is_some_and(|pixel_buffer| {
1749 *data = pixel_buffer.data.clone();
1750 *width = pixel_buffer.width();
1751 *height = pixel_buffer.height();
1752 true
1753 })
1754 }
1755
1756 #[unsafe(no_mangle)]
1757 pub extern "C" fn slint_image_to_rgba8(
1758 image: &Image,
1759 data: &mut SharedVector<Rgba8Pixel>,
1760 width: &mut u32,
1761 height: &mut u32,
1762 ) -> bool {
1763 image.to_rgba8().is_some_and(|pixel_buffer| {
1764 *data = pixel_buffer.data.clone();
1765 *width = pixel_buffer.width();
1766 *height = pixel_buffer.height();
1767 true
1768 })
1769 }
1770
1771 #[unsafe(no_mangle)]
1772 pub extern "C" fn slint_image_to_rgba8_premultiplied(
1773 image: &Image,
1774 data: &mut SharedVector<Rgba8Pixel>,
1775 width: &mut u32,
1776 height: &mut u32,
1777 ) -> bool {
1778 image.to_rgba8_premultiplied().is_some_and(|pixel_buffer| {
1779 *data = pixel_buffer.data.clone();
1780 *width = pixel_buffer.width();
1781 *height = pixel_buffer.height();
1782 true
1783 })
1784 }
1785}
1786
1787#[derive(Clone, Debug, PartialEq)]
1795#[non_exhaustive]
1796#[cfg(not(target_arch = "wasm32"))]
1797#[repr(C)]
1798pub struct BorrowedOpenGLTexture {
1799 pub texture_id: core::num::NonZeroU32,
1801 pub size: IntSize,
1803 pub origin: BorrowedOpenGLTextureOrigin,
1805}
1806
1807#[cfg(test)]
1808mod tests {
1809 use crate::graphics::{Rgba8Pixel, SharedPixelBuffer};
1810
1811 use super::Image;
1812
1813 #[test]
1814 #[should_panic(expected = "the requested 8x8 pixels")]
1815 fn clone_from_slice_rejects_a_short_slice() {
1816 let gray = [0u8; 8 * 8];
1819 SharedPixelBuffer::<Rgba8Pixel>::clone_from_slice(&gray, 8, 8);
1820 }
1821
1822 #[test]
1823 #[should_panic(expected = "the requested 8x8 pixels")]
1824 fn clone_from_slice_rejects_a_long_slice() {
1825 let rgba = [0u8; 8 * 8 * 4];
1827 SharedPixelBuffer::<crate::graphics::Rgb8Pixel>::clone_from_slice(&rgba, 8, 8);
1828 }
1829
1830 #[test]
1831 fn test_premultiplied_to_rgb_zero_alpha() {
1832 let pixel = Rgba8Pixel::new(5, 10, 15, 0);
1833 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1834 assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1835 }
1836
1837 #[test]
1838 fn test_premultiplied_to_rgb_full_alpha() {
1839 let pixel = Rgba8Pixel::new(5, 10, 15, 255);
1840 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1841 assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 255));
1842 }
1843
1844 #[test]
1845 fn test_premultiplied_to_rgb() {
1846 let pixel = Rgba8Pixel::new(5, 10, 15, 128);
1847 let converted = Image::premultiplied_rgba_to_rgba(pixel);
1848 assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 128));
1849 }
1850
1851 #[test]
1852 fn test_rgb_to_premultiplied_zero_alpha() {
1853 let pixel = Rgba8Pixel::new(10, 20, 30, 0);
1854 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1855 assert_eq!(converted, Rgba8Pixel::new(0, 0, 0, 0));
1856 }
1857
1858 #[test]
1859 fn test_rgb_to_premultiplied_full_alpha() {
1860 let pixel = Rgba8Pixel::new(10, 20, 30, 255);
1861 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1862 assert_eq!(converted, Rgba8Pixel::new(10, 20, 30, 255));
1863 }
1864
1865 #[test]
1866 fn test_rgb_to_premultiplied() {
1867 let pixel = Rgba8Pixel::new(10, 20, 30, 128);
1868 let converted = Image::rgba_to_premultiplied_rgba(pixel);
1869 assert_eq!(converted, Rgba8Pixel::new(5, 10, 15, 128));
1870 }
1871}