use crate::lengths::PhysicalPx;
use crate::slice::Slice;
use crate::{SharedString, SharedVector};
use super::{IntRect, IntSize};
use crate::items::ImageFit;
#[cfg(feature = "image-decoders")]
pub mod cache;
#[cfg(target_arch = "wasm32")]
mod htmlimage;
#[cfg(feature = "svg")]
mod svg;
#[allow(missing_docs)]
#[vtable::vtable]
#[repr(C)]
pub struct OpaqueImageVTable {
drop_in_place: fn(VRefMut<OpaqueImageVTable>) -> Layout,
dealloc: fn(&OpaqueImageVTable, ptr: *mut u8, layout: Layout),
size: fn(VRef<OpaqueImageVTable>) -> IntSize,
cache_key: fn(VRef<OpaqueImageVTable>) -> ImageCacheKey,
}
#[cfg(feature = "svg")]
OpaqueImageVTable_static! {
pub static PARSED_SVG_VT for svg::ParsedSVG
}
#[cfg(target_arch = "wasm32")]
OpaqueImageVTable_static! {
pub static HTML_IMAGE_VT for htmlimage::HTMLImage
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct SharedPixelBuffer<Pixel> {
width: u32,
height: u32,
data: SharedVector<Pixel>,
}
impl<Pixel> SharedPixelBuffer<Pixel> {
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn size(&self) -> IntSize {
[self.width, self.height].into()
}
}
impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
pub fn make_mut_slice(&mut self) -> &mut [Pixel] {
self.data.make_mut_slice()
}
}
impl<Pixel: Clone + rgb::Pod> SharedPixelBuffer<Pixel>
where
[Pixel]: rgb::ComponentBytes<u8>,
{
pub fn as_bytes(&self) -> &[u8] {
use rgb::ComponentBytes;
self.data.as_slice().as_bytes()
}
pub fn make_mut_bytes(&mut self) -> &mut [u8] {
use rgb::ComponentBytes;
self.data.make_mut_slice().as_bytes_mut()
}
}
impl<Pixel> SharedPixelBuffer<Pixel> {
pub fn as_slice(&self) -> &[Pixel] {
self.data.as_slice()
}
}
impl<Pixel: Clone + Default> SharedPixelBuffer<Pixel> {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
data: core::iter::repeat(Pixel::default())
.take(width as usize * height as usize)
.collect(),
}
}
}
impl<Pixel: Clone> SharedPixelBuffer<Pixel> {
pub fn clone_from_slice<SourcePixelType>(
pixel_slice: &[SourcePixelType],
width: u32,
height: u32,
) -> Self
where
[SourcePixelType]: rgb::AsPixels<Pixel>,
{
use rgb::AsPixels;
Self { width, height, data: pixel_slice.as_pixels().into() }
}
}
pub type Rgb8Pixel = rgb::RGB8;
pub type Rgba8Pixel = rgb::RGBA8;
#[derive(Clone, Debug)]
#[repr(C)]
pub enum SharedImageBuffer {
RGB8(SharedPixelBuffer<Rgb8Pixel>),
RGBA8(SharedPixelBuffer<Rgba8Pixel>),
RGBA8Premultiplied(SharedPixelBuffer<Rgba8Pixel>),
}
impl SharedImageBuffer {
#[inline]
pub fn width(&self) -> u32 {
match self {
Self::RGB8(buffer) => buffer.width(),
Self::RGBA8(buffer) => buffer.width(),
Self::RGBA8Premultiplied(buffer) => buffer.width(),
}
}
#[inline]
pub fn height(&self) -> u32 {
match self {
Self::RGB8(buffer) => buffer.height(),
Self::RGBA8(buffer) => buffer.height(),
Self::RGBA8Premultiplied(buffer) => buffer.height(),
}
}
#[inline]
pub fn size(&self) -> IntSize {
match self {
Self::RGB8(buffer) => buffer.size(),
Self::RGBA8(buffer) => buffer.size(),
Self::RGBA8Premultiplied(buffer) => buffer.size(),
}
}
}
impl PartialEq for SharedImageBuffer {
fn eq(&self, other: &Self) -> bool {
match self {
Self::RGB8(lhs_buffer) => {
matches!(other, Self::RGB8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
}
Self::RGBA8(lhs_buffer) => {
matches!(other, Self::RGBA8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
}
Self::RGBA8Premultiplied(lhs_buffer) => {
matches!(other, Self::RGBA8Premultiplied(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr()))
}
}
}
}
#[repr(u8)]
#[derive(Clone, PartialEq, Debug, Copy)]
pub enum PixelFormat {
Rgb,
Rgba,
RgbaPremultiplied,
AlphaMap,
}
impl PixelFormat {
pub fn bpp(self) -> usize {
match self {
PixelFormat::Rgb => 3,
PixelFormat::Rgba => 4,
PixelFormat::RgbaPremultiplied => 4,
PixelFormat::AlphaMap => 1,
}
}
}
#[repr(C)]
#[derive(Clone, PartialEq, Debug)]
pub struct StaticTexture {
pub rect: IntRect,
pub format: PixelFormat,
pub color: crate::Color,
pub index: usize,
}
#[repr(C)]
#[derive(Clone, PartialEq, Debug)]
pub struct StaticTextures {
pub size: IntSize,
pub original_size: IntSize,
pub data: Slice<'static, u8>,
pub textures: Slice<'static, StaticTexture>,
}
#[derive(PartialEq, Eq, Debug, Hash, Clone)]
#[repr(u8)]
pub enum ImageCacheKey {
Invalid = 0,
Path(SharedString) = 1,
#[cfg(target_arch = "wasm32")]
URL(SharedString) = 2,
EmbeddedData(usize) = 3,
}
impl ImageCacheKey {
pub fn new(resource: &ImageInner) -> Option<Self> {
let key = match resource {
ImageInner::None => return None,
ImageInner::EmbeddedImage { cache_key, .. } => cache_key.clone(),
ImageInner::StaticTextures(textures) => {
Self::from_embedded_image_data(textures.data.as_slice())
}
#[cfg(feature = "svg")]
ImageInner::Svg(parsed_svg) => parsed_svg.cache_key(),
#[cfg(target_arch = "wasm32")]
ImageInner::HTMLImage(htmlimage) => Self::URL(htmlimage.source().into()),
ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).cache_key(),
#[cfg(not(target_arch = "wasm32"))]
ImageInner::BorrowedOpenGLTexture(..) => return None,
};
if matches!(key, ImageCacheKey::Invalid) {
None
} else {
Some(key)
}
}
pub fn from_embedded_image_data(data: &'static [u8]) -> Self {
Self::EmbeddedData(data.as_ptr() as usize)
}
}
#[derive(Clone, Debug, Default)]
#[repr(u8)]
#[allow(missing_docs)]
pub enum ImageInner {
#[default]
None = 0,
EmbeddedImage {
cache_key: ImageCacheKey,
buffer: SharedImageBuffer,
} = 1,
#[cfg(feature = "svg")]
Svg(vtable::VRc<OpaqueImageVTable, svg::ParsedSVG>) = 2,
StaticTextures(&'static StaticTextures) = 3,
#[cfg(target_arch = "wasm32")]
HTMLImage(vtable::VRc<OpaqueImageVTable, htmlimage::HTMLImage>) = 4,
BackendStorage(vtable::VRc<OpaqueImageVTable>) = 5,
#[cfg(not(target_arch = "wasm32"))]
BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6,
}
impl ImageInner {
pub fn render_to_buffer(
&self,
_target_size_for_scalable_source: Option<euclid::Size2D<u32, PhysicalPx>>,
) -> Option<SharedImageBuffer> {
match self {
ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()),
#[cfg(feature = "svg")]
ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) {
Ok(b) => Some(b),
Err(resvg::usvg::Error::InvalidSize) => None,
Err(err) => {
eprintln!("Error rendering SVG: {err}");
None
}
},
ImageInner::StaticTextures(ts) => {
let mut buffer =
SharedPixelBuffer::<Rgba8Pixel>::new(ts.size.width, ts.size.height);
let stride = buffer.width() as usize;
let slice = buffer.make_mut_slice();
for t in ts.textures.iter() {
let rect = t.rect.to_usize();
for y in 0..rect.height() {
let slice = &mut slice[(rect.min_y() + y) * stride..][rect.x_range()];
let source = &ts.data[t.index + y * rect.width() * t.format.bpp()..];
match t.format {
PixelFormat::Rgb => {
let mut iter = source.chunks_exact(3).map(|p| Rgba8Pixel {
r: p[0],
g: p[1],
b: p[2],
a: 255,
});
slice.fill_with(|| iter.next().unwrap());
}
PixelFormat::RgbaPremultiplied => {
let mut iter = source.chunks_exact(4).map(|p| Rgba8Pixel {
r: p[0],
g: p[1],
b: p[2],
a: p[3],
});
slice.fill_with(|| iter.next().unwrap());
}
PixelFormat::Rgba => {
let mut iter = source.chunks_exact(4).map(|p| {
let a = p[3];
Rgba8Pixel {
r: (p[0] as u16 * a as u16 / 255) as u8,
g: (p[1] as u16 * a as u16 / 255) as u8,
b: (p[2] as u16 * a as u16 / 255) as u8,
a,
}
});
slice.fill_with(|| iter.next().unwrap());
}
PixelFormat::AlphaMap => {
let col = t.color.to_argb_u8();
let mut iter = source.iter().map(|p| {
let a = *p as u32 * col.alpha as u32;
Rgba8Pixel {
r: (col.red as u32 * a / (255 * 255)) as u8,
g: (col.green as u32 * a / (255 * 255)) as u8,
b: (col.blue as u32 * a / (255 * 255)) as u8,
a: (a / 255) as u8,
}
});
slice.fill_with(|| iter.next().unwrap());
}
};
}
}
Some(SharedImageBuffer::RGBA8Premultiplied(buffer))
}
_ => None,
}
}
pub fn is_svg(&self) -> bool {
match self {
#[cfg(feature = "svg")]
Self::Svg(_) => true,
#[cfg(target_arch = "wasm32")]
Self::HTMLImage(html_image) => html_image.is_svg(),
_ => false,
}
}
}
impl PartialEq for ImageInner {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Self::EmbeddedImage { cache_key: l_cache_key, buffer: l_buffer },
Self::EmbeddedImage { cache_key: r_cache_key, buffer: r_buffer },
) => l_cache_key == r_cache_key && l_buffer == r_buffer,
#[cfg(feature = "svg")]
(Self::Svg(l0), Self::Svg(r0)) => vtable::VRc::ptr_eq(l0, r0),
(Self::StaticTextures(l0), Self::StaticTextures(r0)) => l0 == r0,
#[cfg(target_arch = "wasm32")]
(Self::HTMLImage(l0), Self::HTMLImage(r0)) => vtable::VRc::ptr_eq(l0, r0),
(Self::BackendStorage(l0), Self::BackendStorage(r0)) => vtable::VRc::ptr_eq(l0, r0),
#[cfg(not(target_arch = "wasm32"))]
(Self::BorrowedOpenGLTexture(l0), Self::BorrowedOpenGLTexture(r0)) => l0 == r0,
_ => false,
}
}
}
impl<'a> From<&'a Image> for &'a ImageInner {
fn from(other: &'a Image) -> Self {
&other.0
}
}
#[derive(Default, Debug, PartialEq)]
pub struct LoadImageError(());
impl core::fmt::Display for LoadImageError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("The image cannot be loaded")
}
}
#[cfg(feature = "std")]
impl std::error::Error for LoadImageError {}
#[repr(transparent)]
#[derive(Default, Clone, Debug, PartialEq, derive_more::From)]
pub struct Image(ImageInner);
impl Image {
#[cfg(feature = "image-decoders")]
pub fn load_from_path(path: &std::path::Path) -> Result<Self, LoadImageError> {
self::cache::IMAGE_CACHE.with(|global_cache| {
let path: SharedString = path.to_str().ok_or(LoadImageError(()))?.into();
global_cache.borrow_mut().load_image_from_path(&path).ok_or(LoadImageError(()))
})
}
pub fn from_rgb8(buffer: SharedPixelBuffer<Rgb8Pixel>) -> Self {
Image(ImageInner::EmbeddedImage {
cache_key: ImageCacheKey::Invalid,
buffer: SharedImageBuffer::RGB8(buffer),
})
}
pub fn from_rgba8(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
Image(ImageInner::EmbeddedImage {
cache_key: ImageCacheKey::Invalid,
buffer: SharedImageBuffer::RGBA8(buffer),
})
}
pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer<Rgba8Pixel>) -> Self {
Image(ImageInner::EmbeddedImage {
cache_key: ImageCacheKey::Invalid,
buffer: SharedImageBuffer::RGBA8Premultiplied(buffer),
})
}
#[allow(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
#[deprecated(since = "1.2.0", note = "Use BorrowedOpenGLTextureBuilder")]
pub unsafe fn from_borrowed_gl_2d_rgba_texture(
texture_id: core::num::NonZeroU32,
size: IntSize,
) -> Self {
BorrowedOpenGLTextureBuilder::new_gl_2d_rgba_texture(texture_id, size).build()
}
#[cfg(feature = "svg")]
pub fn load_from_svg_data(buffer: &[u8]) -> Result<Self, LoadImageError> {
let cache_key = ImageCacheKey::Invalid;
Ok(Image(ImageInner::Svg(vtable::VRc::new(
svg::load_from_data(buffer, cache_key).map_err(|_| LoadImageError(()))?,
))))
}
pub fn size(&self) -> IntSize {
match &self.0 {
ImageInner::None => Default::default(),
ImageInner::EmbeddedImage { buffer, .. } => buffer.size(),
ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size,
#[cfg(feature = "svg")]
ImageInner::Svg(svg) => svg.size(),
#[cfg(target_arch = "wasm32")]
ImageInner::HTMLImage(htmlimage) => htmlimage.size().unwrap_or_default(),
ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size(),
#[cfg(not(target_arch = "wasm32"))]
ImageInner::BorrowedOpenGLTexture(BorrowedOpenGLTexture { size, .. }) => *size,
}
}
#[cfg(feature = "std")]
pub fn path(&self) -> Option<&std::path::Path> {
match &self.0 {
ImageInner::EmbeddedImage { cache_key: ImageCacheKey::Path(path), .. } => {
Some(std::path::Path::new(path.as_str()))
}
_ => None,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Default)]
#[repr(u8)]
#[non_exhaustive]
pub enum BorrowedOpenGLTextureOrigin {
#[default]
TopLeft,
BottomLeft,
}
#[cfg(not(target_arch = "wasm32"))]
pub struct BorrowedOpenGLTextureBuilder(BorrowedOpenGLTexture);
#[cfg(not(target_arch = "wasm32"))]
impl BorrowedOpenGLTextureBuilder {
#[allow(unsafe_code)]
pub unsafe fn new_gl_2d_rgba_texture(texture_id: core::num::NonZeroU32, size: IntSize) -> Self {
Self(BorrowedOpenGLTexture { texture_id, size, origin: Default::default() })
}
pub fn origin(mut self, origin: BorrowedOpenGLTextureOrigin) -> Self {
self.0.origin = origin;
self
}
pub fn build(self) -> Image {
Image(ImageInner::BorrowedOpenGLTexture(self.0))
}
}
#[cfg(feature = "image-decoders")]
pub fn load_image_from_embedded_data(data: Slice<'static, u8>, format: Slice<'_, u8>) -> Image {
self::cache::IMAGE_CACHE.with(|global_cache| {
global_cache.borrow_mut().load_image_from_embedded_data(data, format).unwrap_or_else(|| {
panic!("internal error: embedded image data is not supported by run-time library",)
})
})
}
#[test]
fn test_image_size_from_buffer_without_backend() {
{
assert_eq!(Image::default().size(), Default::default());
}
{
let buffer = SharedPixelBuffer::<Rgb8Pixel>::new(320, 200);
let image = Image::from_rgb8(buffer);
assert_eq!(image.size(), [320, 200].into())
}
}
#[cfg(feature = "svg")]
#[test]
fn test_image_size_from_svg() {
let simple_svg = r#"<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"></svg>"#;
let image = Image::load_from_svg_data(simple_svg.as_bytes()).unwrap();
assert_eq!(image.size(), [320, 200].into());
}
#[cfg(feature = "svg")]
#[test]
fn test_image_invalid_svg() {
let invalid_svg = r#"AaBbCcDd"#;
let result = Image::load_from_svg_data(invalid_svg.as_bytes());
assert!(result.is_err());
}
pub fn fit_size(
image_fit: ImageFit,
target: euclid::Size2D<f32, PhysicalPx>,
origin: IntSize,
) -> euclid::Size2D<f32, PhysicalPx> {
let o = origin.cast::<f32>();
let ratio = match image_fit {
ImageFit::Fill => return target,
ImageFit::Contain => f32::min(target.width / o.width, target.height / o.height),
ImageFit::Cover => f32::max(target.width / o.width, target.height / o.height),
};
euclid::Size2D::from_untyped(o * ratio)
}
#[cfg(feature = "ffi")]
pub(crate) mod ffi {
#![allow(unsafe_code)]
use super::super::IntSize;
use super::*;
#[cfg(cbindgen)]
#[repr(C)]
struct Rgb8Pixel {
r: u8,
g: u8,
b: u8,
}
#[cfg(cbindgen)]
#[repr(C)]
struct Rgba8Pixel {
r: u8,
g: u8,
b: u8,
a: u8,
}
#[cfg(feature = "image-decoders")]
#[no_mangle]
pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) {
core::ptr::write(
image,
Image::load_from_path(std::path::Path::new(path.as_str())).unwrap_or(Image::default()),
)
}
#[cfg(feature = "std")]
#[no_mangle]
pub unsafe extern "C" fn slint_image_load_from_embedded_data(
data: Slice<'static, u8>,
format: Slice<'static, u8>,
image: *mut Image,
) {
core::ptr::write(image, super::load_image_from_embedded_data(data, format));
}
#[no_mangle]
pub unsafe extern "C" fn slint_image_size(image: &Image) -> IntSize {
image.size()
}
#[no_mangle]
pub unsafe extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> {
match &image.0 {
ImageInner::EmbeddedImage { cache_key, .. } => match cache_key {
ImageCacheKey::Path(path) => Some(path),
_ => None,
},
_ => None,
}
}
#[no_mangle]
pub unsafe extern "C" fn slint_image_from_embedded_textures(
textures: &'static StaticTextures,
image: *mut Image,
) {
core::ptr::write(image, Image::from(ImageInner::StaticTextures(textures)));
}
#[no_mangle]
pub unsafe extern "C" fn slint_image_compare_equal(image1: &Image, image2: &Image) -> bool {
return image1.eq(image2);
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
#[cfg(not(target_arch = "wasm32"))]
#[repr(C)]
pub struct BorrowedOpenGLTexture {
pub texture_id: core::num::NonZeroU32,
pub size: IntSize,
pub origin: BorrowedOpenGLTextureOrigin,
}