blob: e0d1dbb17faf1acb7abebc57c600082e61e0bb8c [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! A simple pixmap type.
use alloc::vec;
use alloc::vec::Vec;
use peniko::color::Rgba8;
use crate::peniko::color::PremulRgba8;
#[cfg(feature = "png")]
extern crate std;
/// A pixmap of premultiplied RGBA8 values backed by [`u8`][core::u8].
#[derive(Debug, Clone)]
pub struct Pixmap {
/// Width of the pixmap in pixels.
width: u16,
/// Height of the pixmap in pixels.
height: u16,
/// Buffer of the pixmap in RGBA8 format.
buf: Vec<PremulRgba8>,
}
impl Pixmap {
/// Create a new pixmap with the given width and height in pixels.
pub fn new(width: u16, height: u16) -> Self {
let buf = vec![PremulRgba8::from_u32(0); width as usize * height as usize];
Self { width, height, buf }
}
/// Create a new pixmap with the given premultiplied RGBA8 data.
///
/// The `data` vector must be of length `width * height` exactly.
///
/// The pixels are in row-major order.
///
/// # Panics
///
/// Panics if the `data` vector is not of length `width * height`.
pub fn from_parts(data: Vec<PremulRgba8>, width: u16, height: u16) -> Self {
assert_eq!(
data.len(),
usize::from(width) * usize::from(height),
"Expected `data` to have length of exactly `width * height`"
);
Self {
width,
height,
buf: data,
}
}
/// Resizes the pixmap container to the given width and height; this does not resize the
/// contained image.
///
/// If the pixmap buffer has to grow to fit the new size, those pixels are set to transparent
/// black. If the pixmap buffer is larger than required, the buffer is truncated and its
/// reserved capacity is unchanged.
pub fn resize(&mut self, width: u16, height: u16) {
self.width = width;
self.height = height;
self.buf.resize(
usize::from(width) * usize::from(height),
PremulRgba8::from_u32(0),
);
}
/// Shrink the capacity of the pixmap buffer to fit the pixmap's current size.
pub fn shrink_to_fit(&mut self) {
self.buf.shrink_to_fit();
}
/// The reserved capacity (in pixels) of this pixmap.
///
/// When calling [`Pixmap::resize`] with a `width * height` smaller than this value, the pixmap
/// does not need to reallocate.
pub fn capacity(&self) -> usize {
self.buf.capacity()
}
/// Return the width of the pixmap.
pub fn width(&self) -> u16 {
self.width
}
/// Return the height of the pixmap.
pub fn height(&self) -> u16 {
self.height
}
/// Apply an alpha value to the whole pixmap.
pub fn multiply_alpha(&mut self, alpha: u8) {
#[expect(
clippy::cast_possible_truncation,
reason = "cannot overflow in this case"
)]
let multiply = |component| ((u16::from(alpha) * u16::from(component)) / 255) as u8;
for pixel in self.data_mut() {
*pixel = PremulRgba8 {
r: multiply(pixel.r),
g: multiply(pixel.g),
b: multiply(pixel.b),
a: multiply(pixel.a),
};
}
}
/// Create a pixmap from a PNG file.
#[cfg(feature = "png")]
pub fn from_png(data: impl std::io::Read) -> Result<Self, png::DecodingError> {
let mut decoder = png::Decoder::new(data);
decoder.set_transformations(
png::Transformations::normalize_to_color8() | png::Transformations::ALPHA,
);
let mut reader = decoder.read_info()?;
let mut pixmap = {
let info = reader.info();
let width: u16 = info
.width
.try_into()
.map_err(|_| png::DecodingError::LimitsExceeded)?;
let height: u16 = info
.height
.try_into()
.map_err(|_| png::DecodingError::LimitsExceeded)?;
Self::new(width, height)
};
// Note `reader.info()` returns the pre-transformation color type output, whereas
// `reader.output_color_type()` takes the transformation into account.
let (color_type, bit_depth) = reader.output_color_type();
debug_assert_eq!(
bit_depth,
png::BitDepth::Eight,
"normalize_to_color8 means the bit depth is always 8."
);
match color_type {
png::ColorType::Rgb | png::ColorType::Grayscale => {
unreachable!("We set a transformation to always convert to alpha")
}
png::ColorType::Indexed => {
unreachable!("Transformation should have expanded indexed images")
}
png::ColorType::Rgba => {
debug_assert_eq!(
pixmap.data_as_u8_slice().len(),
reader.output_buffer_size(),
"The pixmap buffer should have the same number of bytes as the image."
);
reader.next_frame(pixmap.data_as_u8_slice_mut())?;
}
png::ColorType::GrayscaleAlpha => {
debug_assert_eq!(
pixmap.data().len() * 2,
reader.output_buffer_size(),
"The pixmap buffer should have twice the number of bytes of the grayscale image."
);
let mut grayscale_data = vec![0; reader.output_buffer_size()];
reader.next_frame(&mut grayscale_data)?;
for (grayscale_pixel, pixmap_pixel) in
grayscale_data.chunks_exact(2).zip(pixmap.data_mut())
{
let [gray, alpha] = grayscale_pixel.try_into().unwrap();
*pixmap_pixel = PremulRgba8 {
r: gray,
g: gray,
b: gray,
a: alpha,
};
}
}
};
for pixel in pixmap.data_mut() {
let alpha = u16::from(pixel.a);
#[expect(
clippy::cast_possible_truncation,
reason = "Overflow should be impossible."
)]
let premultiply = |e: u8| ((u16::from(e) * alpha) / 255) as u8;
pixel.r = premultiply(pixel.r);
pixel.g = premultiply(pixel.g);
pixel.b = premultiply(pixel.b);
}
Ok(pixmap)
}
/// Returns a reference to the underlying data as premultiplied RGBA8.
///
/// The pixels are in row-major order.
pub fn data(&self) -> &[PremulRgba8] {
&self.buf
}
/// Returns a mutable reference to the underlying data as premultiplied RGBA8.
///
/// The pixels are in row-major order.
pub fn data_mut(&mut self) -> &mut [PremulRgba8] {
&mut self.buf
}
/// Returns a reference to the underlying data as premultiplied RGBA8.
///
/// The pixels are in row-major order. Each pixel consists of four bytes in the order
/// `[r, g, b, a]`.
pub fn data_as_u8_slice(&self) -> &[u8] {
bytemuck::cast_slice(&self.buf)
}
/// Returns a mutable reference to the underlying data as premultiplied RGBA8.
///
/// The pixels are in row-major order. Each pixel consists of four bytes in the order
/// `[r, g, b, a]`.
pub fn data_as_u8_slice_mut(&mut self) -> &mut [u8] {
bytemuck::cast_slice_mut(&mut self.buf)
}
/// Sample a pixel from the pixmap.
///
/// The pixel data is [premultiplied RGBA8][PremulRgba8].
#[inline(always)]
pub fn sample(&self, x: u16, y: u16) -> PremulRgba8 {
let idx = self.width as usize * y as usize + x as usize;
self.buf[idx]
}
/// Sample a pixel from a custom-calculated index. This index should be calculated assuming that
/// the data is stored in row-major order.
#[inline(always)]
pub fn sample_idx(&self, idx: u32) -> PremulRgba8 {
self.buf[idx as usize]
}
/// Consume the pixmap, returning the data as the underlying [`Vec`] of premultiplied RGBA8.
///
/// The pixels are in row-major order.
pub fn take(self) -> Vec<PremulRgba8> {
self.buf
}
/// Consume the pixmap, returning the data as (unpremultiplied) RGBA8.
///
/// Not fast, but useful for saving to PNG etc.
///
/// The pixels are in row-major order.
pub fn take_unpremultiplied(self) -> Vec<Rgba8> {
self.buf
.into_iter()
.map(|PremulRgba8 { r, g, b, a }| {
let alpha = 255.0 / f32::from(a);
if a != 0 {
#[expect(clippy::cast_possible_truncation, reason = "deliberate quantization")]
let unpremultiply = |component| (f32::from(component) * alpha + 0.5) as u8;
Rgba8 {
r: unpremultiply(r),
g: unpremultiply(g),
b: unpremultiply(b),
a,
}
} else {
Rgba8 { r, g, b, a }
}
})
.collect()
}
}