diff options
Diffstat (limited to 'rsvg/src/surface_utils/mod.rs')
-rw-r--r-- | rsvg/src/surface_utils/mod.rs | 338 |
1 files changed, 338 insertions, 0 deletions
diff --git a/rsvg/src/surface_utils/mod.rs b/rsvg/src/surface_utils/mod.rs new file mode 100644 index 00000000..8529bf68 --- /dev/null +++ b/rsvg/src/surface_utils/mod.rs @@ -0,0 +1,338 @@ +//! Various utilities for working with Cairo image surfaces. + +use std::alloc; +use std::slice; + +pub mod iterators; +pub mod shared_surface; +pub mod srgb; + +// These two are for Cairo's platform-endian 0xaarrggbb pixels + +#[cfg(target_endian = "little")] +use rgb::alt::BGRA8; +#[cfg(target_endian = "little")] +#[allow(clippy::upper_case_acronyms)] +pub type CairoARGB = BGRA8; + +#[cfg(target_endian = "big")] +use rgb::alt::ARGB8; +#[cfg(target_endian = "big")] +#[allow(clippy::upper_case_acronyms)] +pub type CairoARGB = ARGB8; + +/// GdkPixbuf's endian-independent RGBA8 pixel layout. +pub type GdkPixbufRGBA = rgb::RGBA8; + +/// GdkPixbuf's packed RGB pixel layout. +pub type GdkPixbufRGB = rgb::RGB8; + +/// Analogous to `rgb::FromSlice`, to convert from `[T]` to `[CairoARGB]` +#[allow(clippy::upper_case_acronyms)] +pub trait AsCairoARGB { + /// Reinterpret slice as `CairoARGB` pixels. + fn as_cairo_argb(&self) -> &[CairoARGB]; + + /// Reinterpret mutable slice as `CairoARGB` pixels. + fn as_cairo_argb_mut(&mut self) -> &mut [CairoARGB]; +} + +// SAFETY: transmuting from u32 to CairoRGB is based on the following assumptions: +// * there are no invalid bit representations for ARGB +// * u32 and ARGB are the same size +// * u32 is sufficiently aligned +impl AsCairoARGB for [u32] { + fn as_cairo_argb(&self) -> &[CairoARGB] { + const LAYOUT_U32: alloc::Layout = alloc::Layout::new::<u32>(); + const LAYOUT_ARGB: alloc::Layout = alloc::Layout::new::<CairoARGB>(); + let _: [(); LAYOUT_U32.size()] = [(); LAYOUT_ARGB.size()]; + let _: [(); 0] = [(); LAYOUT_U32.align() % LAYOUT_ARGB.align()]; + unsafe { slice::from_raw_parts(self.as_ptr() as *const _, self.len()) } + } + + fn as_cairo_argb_mut(&mut self) -> &mut [CairoARGB] { + unsafe { slice::from_raw_parts_mut(self.as_mut_ptr() as *mut _, self.len()) } + } +} + +/// Modes which specify how the values of out of bounds pixels are computed. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum EdgeMode { + /// The nearest inbounds pixel value is returned. + Duplicate, + /// The image is extended by taking the color values from the opposite of the image. + /// + /// Imagine the image being tiled infinitely, with the original image at the origin. + Wrap, + /// Zero RGBA values are returned. + None, +} + +/// Trait to convert pixels in various formats to RGBA, for GdkPixbuf. +/// +/// GdkPixbuf unconditionally uses RGBA ordering regardless of endianness, +/// but we need to convert to it from Cairo's endian-dependent 0xaarrggbb. +pub trait ToGdkPixbufRGBA { + fn to_pixbuf_rgba(&self) -> GdkPixbufRGBA; +} + +/// Trait to convert pixels in various formats to our own Pixel layout. +pub trait ToPixel { + fn to_pixel(&self) -> Pixel; +} + +/// Trait to convert pixels in various formats to Cairo's endian-dependent 0xaarrggbb. +pub trait ToCairoARGB { + fn to_cairo_argb(&self) -> CairoARGB; +} + +impl ToGdkPixbufRGBA for Pixel { + #[inline] + fn to_pixbuf_rgba(&self) -> GdkPixbufRGBA { + GdkPixbufRGBA { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } +} + +impl ToPixel for CairoARGB { + #[inline] + fn to_pixel(&self) -> Pixel { + Pixel { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } +} + +impl ToPixel for GdkPixbufRGBA { + #[inline] + fn to_pixel(&self) -> Pixel { + Pixel { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } +} + +impl ToPixel for GdkPixbufRGB { + #[inline] + fn to_pixel(&self) -> Pixel { + Pixel { + r: self.r, + g: self.g, + b: self.b, + a: 255, + } + } +} + +impl ToCairoARGB for Pixel { + #[inline] + fn to_cairo_argb(&self) -> CairoARGB { + CairoARGB { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } +} + +/// Extension methods for `cairo::ImageSurfaceData`. +pub trait ImageSurfaceDataExt { + /// Sets the pixel at the given coordinates. Assumes the `ARgb32` format. + fn set_pixel(&mut self, stride: usize, pixel: Pixel, x: u32, y: u32); +} + +/// A pixel consisting of R, G, B and A values. +pub type Pixel = rgb::RGBA8; + +pub trait PixelOps { + fn premultiply(self) -> Self; + fn unpremultiply(self) -> Self; + fn diff(&self, other: &Self) -> Self; + fn to_luminance_mask(&self) -> Self; + fn to_u32(&self) -> u32; + fn from_u32(x: u32) -> Self; +} + +impl PixelOps for Pixel { + /// Returns an unpremultiplied value of this pixel. + /// + /// For a fully transparent pixel, a transparent black pixel will be returned. + #[inline] + fn unpremultiply(self) -> Self { + if self.a == 0 { + Self { + r: 0, + g: 0, + b: 0, + a: 0, + } + } else { + let alpha = f32::from(self.a) / 255.0; + self.map_rgb(|x| ((f32::from(x) / alpha) + 0.5) as u8) + } + } + + /// Returns a premultiplied value of this pixel. + #[inline] + fn premultiply(self) -> Self { + let a = self.a as u32; + self.map_rgb(|x| (((x as u32) * a + 127) / 255) as u8) + } + + #[inline] + fn diff(&self, other: &Pixel) -> Pixel { + self.iter() + .zip(other.iter()) + .map(|(l, r)| (l as i32 - r as i32).unsigned_abs() as u8) + .collect() + } + + /// Returns a 'mask' pixel with only the alpha channel + /// + /// Assuming, the pixel is linear RGB (not sRGB) + /// y = luminance + /// Y = 0.2126 R + 0.7152 G + 0.0722 B + /// 1.0 opacity = 255 + /// + /// When Y = 1.0, pixel for mask should be 0xFFFFFFFF + /// (you get 1.0 luminance from 255 from R, G and B) + /// + /// r_mult = 0xFFFFFFFF / (255.0 * 255.0) * .2126 = 14042.45 ~= 14042 + /// g_mult = 0xFFFFFFFF / (255.0 * 255.0) * .7152 = 47239.69 ~= 47240 + /// b_mult = 0xFFFFFFFF / (255.0 * 255.0) * .0722 = 4768.88 ~= 4769 + /// + /// This allows for the following expected behaviour: + /// (we only care about the most significant byte) + /// if pixel = 0x00FFFFFF, pixel' = 0xFF...... + /// if pixel = 0x00020202, pixel' = 0x02...... + + /// if pixel = 0x00000000, pixel' = 0x00...... + #[inline] + fn to_luminance_mask(&self) -> Self { + let r = u32::from(self.r); + let g = u32::from(self.g); + let b = u32::from(self.b); + + Self { + r: 0, + g: 0, + b: 0, + a: (((r * 14042 + g * 47240 + b * 4769) * 255) >> 24) as u8, + } + } + + /// Returns the pixel value as a `u32`, in the same format as `cairo::Format::ARgb32`. + #[inline] + fn to_u32(&self) -> u32 { + (u32::from(self.a) << 24) + | (u32::from(self.r) << 16) + | (u32::from(self.g) << 8) + | u32::from(self.b) + } + + /// Converts a `u32` in the same format as `cairo::Format::ARgb32` into a `Pixel`. + #[inline] + fn from_u32(x: u32) -> Self { + Self { + r: ((x >> 16) & 0xFF) as u8, + g: ((x >> 8) & 0xFF) as u8, + b: (x & 0xFF) as u8, + a: ((x >> 24) & 0xFF) as u8, + } + } +} + +impl<'a> ImageSurfaceDataExt for cairo::ImageSurfaceData<'a> { + #[inline] + fn set_pixel(&mut self, stride: usize, pixel: Pixel, x: u32, y: u32) { + let this: &mut [u8] = &mut *self; + // SAFETY: this code assumes that cairo image surface data is correctly + // aligned for u32. This assumption is justified by the Cairo docs, + // which say this: + // + // https://cairographics.org/manual/cairo-Image-Surfaces.html#cairo-image-surface-create-for-data + // + // > This pointer must be suitably aligned for any kind of variable, + // > (for example, a pointer returned by malloc). + #[allow(clippy::cast_ptr_alignment)] + let this: &mut [u32] = + unsafe { slice::from_raw_parts_mut(this.as_mut_ptr() as *mut u32, this.len() / 4) }; + this.set_pixel(stride, pixel, x, y); + } +} +impl ImageSurfaceDataExt for [u8] { + #[inline] + fn set_pixel(&mut self, stride: usize, pixel: Pixel, x: u32, y: u32) { + use byteorder::{NativeEndian, WriteBytesExt}; + let mut this = &mut self[y as usize * stride + x as usize * 4..]; + this.write_u32::<NativeEndian>(pixel.to_u32()) + .expect("out of bounds pixel access on [u8]"); + } +} +impl ImageSurfaceDataExt for [u32] { + #[inline] + fn set_pixel(&mut self, stride: usize, pixel: Pixel, x: u32, y: u32) { + self[(y as usize * stride + x as usize * 4) / 4] = pixel.to_u32(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + #[test] + fn pixel_diff() { + let a = Pixel::new(0x10, 0x20, 0xf0, 0x40); + assert_eq!(a, a.diff(&Pixel::default())); + let b = Pixel::new(0x50, 0xff, 0x20, 0x10); + assert_eq!(a.diff(&b), Pixel::new(0x40, 0xdf, 0xd0, 0x30)); + } + + // Floating-point reference implementation + fn premultiply_float(pixel: Pixel) -> Pixel { + let alpha = f64::from(pixel.a) / 255.0; + pixel.map_rgb(|x| ((f64::from(x) * alpha) + 0.5) as u8) + } + + prop_compose! { + fn arbitrary_pixel()(a: u8, r: u8, g: u8, b: u8) -> Pixel { + Pixel { r, g, b, a } + } + } + + proptest! { + #[test] + fn pixel_premultiply(pixel in arbitrary_pixel()) { + prop_assert_eq!(pixel.premultiply(), premultiply_float(pixel)); + } + + #[test] + fn pixel_unpremultiply(pixel in arbitrary_pixel()) { + let roundtrip = pixel.premultiply().unpremultiply(); + if pixel.a == 0 { + prop_assert_eq!(roundtrip, Pixel::default()); + } else { + // roundtrip can't be perfect, the accepted error depends on alpha + let tolerance = 0xff / pixel.a; + let diff = roundtrip.diff(&pixel); + prop_assert!(diff.r <= tolerance, "red component value differs by more than {}: {:?}", tolerance, roundtrip); + prop_assert!(diff.g <= tolerance, "green component value differs by more than {}: {:?}", tolerance, roundtrip); + prop_assert!(diff.b <= tolerance, "blue component value differs by more than {}: {:?}", tolerance, roundtrip); + + prop_assert_eq!(pixel.a, roundtrip.a); + } + } + } +} |