diff --git a/api/node/rust/types/image_data.rs b/api/node/rust/types/image_data.rs index 6cf15f66c1c..88d3d6381d5 100644 --- a/api/node/rust/types/image_data.rs +++ b/api/node/rust/types/image_data.rs @@ -68,6 +68,14 @@ impl SlintImageData { (self.width() * self.height()) as usize, )); } + SharedImageBuffer::Gray8(buffer) => { + let rgba = buffer + .as_bytes() + .iter() + .flat_map(|g| [*g, *g, *g, 255]) + .collect::>(); + return Buffer::from(rgba); + } } } diff --git a/internal/backends/qt/qt_window.rs b/internal/backends/qt/qt_window.rs index 4c7403becfa..0e41148389b 100644 --- a/internal/backends/qt/qt_window.rs +++ b/internal/backends/qt/qt_window.rs @@ -1434,6 +1434,9 @@ fn shared_image_buffer_to_pixmap(buffer: &SharedImageBuffer) -> Option { (qttypes::ImageFormat::RGB888, img.width() * 3, img.as_bytes().as_ptr()) } + SharedImageBuffer::Gray8(img) => { + (qttypes::ImageFormat::Grayscale8, img.width(), img.as_bytes().as_ptr()) + } }; let width: i32 = buffer.width() as _; let height: i32 = buffer.height() as _; diff --git a/internal/backends/winit/winitwindowadapter.rs b/internal/backends/winit/winitwindowadapter.rs index 3c673625c8a..74424400352 100644 --- a/internal/backends/winit/winitwindowadapter.rs +++ b/internal/backends/winit/winitwindowadapter.rs @@ -140,6 +140,9 @@ fn icon_to_winit( .chain(std::iter::once(alpha as u8)) }) .collect(), + SharedImageBuffer::Gray8(pixels) => { + pixels.as_bytes().iter().flat_map(|g| [*g, *g, *g, 255]).collect() + } }; winit::window::Icon::from_rgba(rgba_pixels, pixel_buffer.width(), pixel_buffer.height()).ok() diff --git a/internal/compiler/embedded_resources.rs b/internal/compiler/embedded_resources.rs index d3826a19f5a..5ca94909028 100644 --- a/internal/compiler/embedded_resources.rs +++ b/internal/compiler/embedded_resources.rs @@ -20,6 +20,8 @@ pub enum PixelFormat { RgbaPremultiplied, // 8bit alpha map with a given color AlphaMap([u8; 3]), + // 8bit grayscale. Each pixel is a luminance value. + Gray8, } #[cfg(feature = "software-renderer")] diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index 49279da7f05..1628fdae573 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -69,6 +69,7 @@ impl quote::ToTokens for crate::embedded_resources::PixelFormat { quote!(sp::TexturePixelFormat::RgbaPremultiplied) } AlphaMap(_) => quote!(sp::TexturePixelFormat::AlphaMap), + Gray8 => quote!(sp::TexturePixelFormat::Gray8), }; tokens.extend(tks); } diff --git a/internal/compiler/passes/embed_images.rs b/internal/compiler/passes/embed_images.rs index b30df1d08cf..dded9402f76 100644 --- a/internal/compiler/passes/embed_images.rs +++ b/internal/compiler/passes/embed_images.rs @@ -268,6 +268,7 @@ fn generate_texture( assert!(right > left); // otherwise we would have a transparent image } let mut is_opaque = true; + let mut is_grayscale = true; enum ColorState { Unset, Different, @@ -284,6 +285,9 @@ fn generate_texture( if alpha == 0 { continue; } + if p[0] != p[1] || p[1] != p[2] { + is_grayscale = false; + } let get_pixel = || match source_format { SourceFormat::RgbaPremultiplied => <[u8; 3]>::try_from(&p.0[0..3]) .unwrap() @@ -311,6 +315,8 @@ fn generate_texture( let format = if let ColorState::Rgb(c) = color { PixelFormat::AlphaMap(c) + } else if is_opaque && is_grayscale { + PixelFormat::Gray8 } else if is_opaque { PixelFormat::Rgb } else { @@ -364,6 +370,7 @@ fn convert_image( }) .collect(), (_, PixelFormat::AlphaMap(_)) => i.pixels().map(|(_, _, p)| p[3]).collect(), + (_, PixelFormat::Gray8) => i.pixels().map(|(_, _, p)| p[0]).collect(), } } diff --git a/internal/core/api.rs b/internal/core/api.rs index abff9bef783..b6c9f869d71 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -15,7 +15,7 @@ use alloc::string::String; #[cfg(target_has_atomic = "ptr")] pub use crate::future::*; pub use crate::graphics::{ - Brush, Color, Image, LoadImageError, OklchColor, Rgb8Pixel, Rgba8Pixel, RgbaColor, + Brush, Color, Gray8Pixel, Image, LoadImageError, OklchColor, Rgb8Pixel, Rgba8Pixel, RgbaColor, SharedPixelBuffer, }; pub use crate::input::Keys; diff --git a/internal/core/graphics/image.rs b/internal/core/graphics/image.rs index 6927b86e966..bd6d6f3c437 100644 --- a/internal/core/graphics/image.rs +++ b/internal/core/graphics/image.rs @@ -151,6 +151,8 @@ pub type Rgb8Pixel = rgb::RGB8; /// Convenience alias for a pixel with four color channels (red, green, blue and alpha), each /// encoded as u8. pub type Rgba8Pixel = rgb::RGBA8; +/// Convenience alias for a single-channel grayscale pixel encoded as u8. +pub type Gray8Pixel = rgb::Gray; /// SharedImageBuffer is a container for images that are stored in CPU accessible memory. /// @@ -173,6 +175,9 @@ pub enum SharedImageBuffer { /// Only construct this format if you know that your pixels are encoded this way. It is more efficient /// for rendering. RGBA8Premultiplied(SharedPixelBuffer), + /// This variant holds the data for a grayscale image where each pixel is a single luminance + /// channel encoded as unsigned byte. + Gray8(SharedPixelBuffer), } impl SharedImageBuffer { @@ -183,6 +188,7 @@ impl SharedImageBuffer { Self::RGB8(buffer) => buffer.width(), Self::RGBA8(buffer) => buffer.width(), Self::RGBA8Premultiplied(buffer) => buffer.width(), + Self::Gray8(buffer) => buffer.width(), } } @@ -193,6 +199,7 @@ impl SharedImageBuffer { Self::RGB8(buffer) => buffer.height(), Self::RGBA8(buffer) => buffer.height(), Self::RGBA8Premultiplied(buffer) => buffer.height(), + Self::Gray8(buffer) => buffer.height(), } } @@ -203,6 +210,7 @@ impl SharedImageBuffer { Self::RGB8(buffer) => buffer.size(), Self::RGBA8(buffer) => buffer.size(), Self::RGBA8Premultiplied(buffer) => buffer.size(), + Self::Gray8(buffer) => buffer.size(), } } } @@ -219,6 +227,9 @@ impl PartialEq for SharedImageBuffer { Self::RGBA8Premultiplied(lhs_buffer) => { matches!(other, Self::RGBA8Premultiplied(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr())) } + Self::Gray8(lhs_buffer) => { + matches!(other, Self::Gray8(rhs_buffer) if lhs_buffer.data.as_ptr().eq(&rhs_buffer.data.as_ptr())) + } } } } @@ -240,6 +251,8 @@ pub enum TexturePixelFormat { /// and i8::MAX corresponds to 3 pixels inside the shape. /// The array must be width * height +1 bytes long. (the extra bit is read but never used) SignedDistanceField, + /// Grayscale. 8bits. Each pixel is a luminance value rendered as (v, v, v, 255). + Gray8, } impl TexturePixelFormat { @@ -251,6 +264,7 @@ impl TexturePixelFormat { TexturePixelFormat::RgbaPremultiplied => 4, TexturePixelFormat::AlphaMap => 1, TexturePixelFormat::SignedDistanceField => 1, + TexturePixelFormat::Gray8 => 1, } } } @@ -514,6 +528,15 @@ impl ImageInner { }); slice.fill_with(|| iter.next().unwrap()); } + TexturePixelFormat::Gray8 => { + let mut iter = source.iter().map(|v| Rgba8Pixel { + r: *v, + g: *v, + b: *v, + a: 255, + }); + slice.fill_with(|| iter.next().unwrap()); + } TexturePixelFormat::SignedDistanceField => { todo!("converting from a signed distance field to an image") } @@ -743,6 +766,15 @@ impl Image { }) } + /// Creates a new Image from the specified shared pixel buffer, where each pixel is a single + /// grayscale luminance value encoded as u8. + pub fn from_gray8(buffer: SharedPixelBuffer) -> Self { + Image(ImageInner::EmbeddedImage { + cache_key: ImageCacheKey::Invalid, + buffer: SharedImageBuffer::Gray8(buffer), + }) + } + /// Returns the pixel buffer for the Image if available in RGB format without alpha. /// Returns None if the pixels cannot be obtained, for example when the image was created from borrowed OpenGL textures. pub fn to_rgb8(&self) -> Option> { @@ -767,6 +799,18 @@ impl Image { height: buffer.height, data: buffer.data.into_iter().map(Image::premultiplied_rgba_to_rgba).collect(), }, + SharedImageBuffer::Gray8(buffer) => SharedPixelBuffer:: { + width: buffer.width, + height: buffer.height, + data: buffer + .data + .into_iter() + .map(|g| { + let v = g.value(); + Rgba8Pixel::new(v, v, v, 255) + }) + .collect(), + }, }) } @@ -786,6 +830,18 @@ impl Image { data: buffer.data.into_iter().map(Image::rgba_to_premultiplied_rgba).collect(), }, SharedImageBuffer::RGBA8Premultiplied(buffer) => buffer, + SharedImageBuffer::Gray8(buffer) => SharedPixelBuffer:: { + width: buffer.width, + height: buffer.height, + data: buffer + .data + .into_iter() + .map(|g| { + let v = g.value(); + Rgba8Pixel::new(v, v, v, 255) + }) + .collect(), + }, }) } @@ -1332,6 +1388,15 @@ pub(crate) mod ffi { a: u8, } + // Expand Gray8Pixel so that cbindgen can see it. (is in fact rgb::Gray) + /// Represents a grayscale pixel. + #[cfg(cbindgen)] + #[repr(C)] + struct Gray8Pixel { + /// luminance value (between 0 and 255) + v: u8, + } + #[cfg(feature = "image-decoders")] #[unsafe(no_mangle)] pub unsafe extern "C" fn slint_image_load_from_path(path: &SharedString, image: *mut Image) { diff --git a/internal/core/graphics/image/cache.rs b/internal/core/graphics/image/cache.rs index 5490787ebc7..95426920bc6 100644 --- a/internal/core/graphics/image/cache.rs +++ b/internal/core/graphics/image/cache.rs @@ -18,6 +18,7 @@ impl clru::WeightScale for ImageWeightInBytes { SharedImageBuffer::RGB8(pixels) => pixels.as_bytes().len(), SharedImageBuffer::RGBA8(pixels) => pixels.as_bytes().len(), SharedImageBuffer::RGBA8Premultiplied(pixels) => pixels.as_bytes().len(), + SharedImageBuffer::Gray8(pixels) => pixels.as_bytes().len(), }, #[cfg(feature = "svg")] ImageInner::Svg(_) => 512, // Don't know how to measure the size of the parsed SVG tree... diff --git a/internal/renderers/femtovg/images.rs b/internal/renderers/femtovg/images.rs index fd1f52c90be..e52376dbfa7 100644 --- a/internal/renderers/femtovg/images.rs +++ b/internal/renderers/femtovg/images.rs @@ -324,6 +324,13 @@ fn image_buffer_to_image_source( }, femtovg::ImageFlags::PREMULTIPLIED, ), + SharedImageBuffer::Gray8(buffer) => ( + { + imgref::ImgRef::new(buffer.as_slice(), buffer.width() as _, buffer.height() as _) + .into() + }, + femtovg::ImageFlags::empty(), + ), } } diff --git a/internal/renderers/skia/cached_image.rs b/internal/renderers/skia/cached_image.rs index 918e59f3755..bbf57ec0d45 100644 --- a/internal/renderers/skia/cached_image.rs +++ b/internal/renderers/skia/cached_image.rs @@ -70,6 +70,7 @@ pub(crate) fn as_skia_image( SharedImageBuffer::RGB8(_) => unreachable!(), SharedImageBuffer::RGBA8(_) => unreachable!(), SharedImageBuffer::RGBA8Premultiplied(pixels) => pixels, + SharedImageBuffer::Gray8(_) => unreachable!(), }; let image_info = skia_safe::ImageInfo::new( @@ -140,6 +141,13 @@ fn image_buffer_to_skia_image(buffer: &SharedImageBuffer) -> Option ( + skia_safe::Data::new_copy(pixels.as_bytes()), + pixels.width() as usize, + pixels.size(), + skia_safe::ColorType::Gray8, + skia_safe::AlphaType::Opaque, + ), }; let image_info = skia_safe::ImageInfo::new( skia_safe::ISize::new(size.width as i32, size.height as i32), diff --git a/internal/renderers/software/draw_functions.rs b/internal/renderers/software/draw_functions.rs index b6a290d681c..714f9d6a9a0 100644 --- a/internal/renderers/software/draw_functions.rs +++ b/internal/renderers/software/draw_functions.rs @@ -280,6 +280,19 @@ pub(super) fn draw_texture_line( pix.blend(c); } } + TexturePixelFormat::Gray8 => { + for pix in line_buffer { + let pos = pos(1).0; + let v = data[pos]; + if alpha == 0xff { + *pix = TargetPixel::from_rgb(v, v, v); + } else { + pix.blend(PremultipliedRgbaColor::premultiply(Color::from_argb_u8( + alpha, v, v, v, + ))); + } + } + } TexturePixelFormat::SignedDistanceField => { const RANGE: i32 = 6; let factor = (362 * 256 / delta.0) * RANGE; // 362 ≃ 255 * sqrt(2) diff --git a/internal/renderers/software/scene.rs b/internal/renderers/software/scene.rs index 4d29a71a1f6..e65a7e0ae5f 100644 --- a/internal/renderers/software/scene.rs +++ b/internal/renderers/software/scene.rs @@ -470,6 +470,12 @@ impl SharedBufferCommand { extra: self.extra, } } + SharedBufferData::SharedImage(SharedImageBuffer::Gray8(b)) => SceneTexture { + data: &b.as_bytes()[start..end], + pixel_stride: stride as u16, + format: TexturePixelFormat::Gray8, + extra: self.extra, + }, SharedBufferData::AlphaMap { data, width } => SceneTexture { data: &data[start..end], pixel_stride: *width, diff --git a/internal/renderers/software/target_pixel_buffer.rs b/internal/renderers/software/target_pixel_buffer.rs index ada768f0b7a..b6413c7f271 100644 --- a/internal/renderers/software/target_pixel_buffer.rs +++ b/internal/renderers/software/target_pixel_buffer.rs @@ -119,6 +119,12 @@ impl DrawTextureArgs { size, ) } + SharedBufferData::SharedImage(SharedImageBuffer::Gray8(b)) => TextureData::new( + &b.as_bytes()[start..end], + TexturePixelFormat::Gray8, + stride, + size, + ), SharedBufferData::AlphaMap { data, .. } => TextureData::new( &data[start..end], TexturePixelFormat::AlphaMap,