use std::borrow::Cow;
use std::sync::{Arc, OnceLock};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AlphaMode {
#[default]
Straight,
Premultiplied,
}
#[derive(Debug, Clone)]
pub struct Image {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
}
impl Image {
pub fn new(width: u32, height: u32, pixels: Vec<u8>) -> Self {
Self::from_straight_rgba(width, height, pixels)
}
pub fn from_straight_rgba(width: u32, height: u32, pixels: Vec<u8>) -> Self {
Self {
width,
height,
pixels,
}
}
pub fn from_premultiplied_rgba(width: u32, height: u32, pixels: Vec<u8>) -> Self {
Self::with_alpha_mode(width, height, pixels, AlphaMode::Premultiplied)
}
pub fn with_alpha_mode(
width: u32,
height: u32,
mut pixels: Vec<u8>,
alpha_mode: AlphaMode,
) -> Self {
convert_rgba_alpha_mode(&mut pixels, alpha_mode, AlphaMode::Straight);
Self {
width,
height,
pixels,
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn alpha_mode(&self) -> AlphaMode {
AlphaMode::Straight
}
pub fn pixels_in_alpha_mode(&self, alpha_mode: AlphaMode) -> Cow<'_, [u8]> {
if alpha_mode == AlphaMode::Straight {
Cow::Borrowed(&self.pixels)
} else {
let mut pixels = self.pixels.clone();
convert_rgba_alpha_mode(&mut pixels, AlphaMode::Straight, alpha_mode);
Cow::Owned(pixels)
}
}
pub fn encode_png(&self) -> crate::core::Result<Vec<u8>> {
crate::export::encode_rgba_png(self)
}
}
#[derive(Clone, Debug)]
pub struct RenderedLayer {
kind: LayerKind,
}
#[derive(Clone, Debug)]
enum LayerKind {
Straight(Arc<Image>),
Premultiplied {
width: u32,
height: u32,
pixels: Arc<Vec<u8>>,
straight: Arc<OnceLock<Arc<Image>>>,
},
}
impl RenderedLayer {
pub fn from_straight_image(image: Arc<Image>) -> Self {
Self {
kind: LayerKind::Straight(image),
}
}
pub fn from_premultiplied_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Self {
Self {
kind: LayerKind::Premultiplied {
width,
height,
pixels: Arc::new(pixels),
straight: Arc::new(OnceLock::new()),
},
}
}
pub fn width(&self) -> u32 {
match &self.kind {
LayerKind::Straight(image) => image.width,
LayerKind::Premultiplied { width, .. } => *width,
}
}
pub fn height(&self) -> u32 {
match &self.kind {
LayerKind::Straight(image) => image.height,
LayerKind::Premultiplied { height, .. } => *height,
}
}
pub fn alpha_mode(&self) -> AlphaMode {
match &self.kind {
LayerKind::Straight(_) => AlphaMode::Straight,
LayerKind::Premultiplied { .. } => AlphaMode::Premultiplied,
}
}
pub fn pixels(&self) -> &[u8] {
match &self.kind {
LayerKind::Straight(image) => &image.pixels,
LayerKind::Premultiplied { pixels, .. } => pixels,
}
}
pub fn image(&self) -> &Arc<Image> {
match &self.kind {
LayerKind::Straight(image) => image,
LayerKind::Premultiplied {
width,
height,
pixels,
straight,
} => straight.get_or_init(|| {
Arc::new(Image::from_premultiplied_rgba(
*width,
*height,
pixels.as_ref().clone(),
))
}),
}
}
pub fn same_buffer_as(&self, other: &Self) -> bool {
match (&self.kind, &other.kind) {
(LayerKind::Straight(this), LayerKind::Straight(that)) => Arc::ptr_eq(this, that),
(
LayerKind::Premultiplied { pixels: this, .. },
LayerKind::Premultiplied { pixels: that, .. },
) => Arc::ptr_eq(this, that),
_ => false,
}
}
pub fn has_straight_view(&self) -> bool {
match &self.kind {
LayerKind::Straight(_) => true,
LayerKind::Premultiplied { straight, .. } => straight.get().is_some(),
}
}
}
pub fn source_over_straight_rgba(destination: [u8; 4], source: [u8; 4]) -> [u8; 4] {
let source_alpha = u64::from(source[3]);
if source_alpha == 0 {
return destination;
}
if source_alpha == 255 {
return source;
}
let destination_alpha = u64::from(destination[3]);
let inverse_source_alpha = 255 - source_alpha;
let output_alpha_numerator = source_alpha * 255 + destination_alpha * inverse_source_alpha;
if output_alpha_numerator == 0 {
return [0; 4];
}
let mut output = [0; 4];
for channel in 0..3 {
let color_numerator = u64::from(source[channel]) * source_alpha * 255
+ u64::from(destination[channel]) * destination_alpha * inverse_source_alpha;
output[channel] = ((color_numerator + output_alpha_numerator / 2) / output_alpha_numerator)
.min(255) as u8;
}
output[3] = ((output_alpha_numerator + 127) / 255).min(255) as u8;
output
}
pub(crate) fn convert_rgba_alpha_mode(pixels: &mut [u8], from: AlphaMode, to: AlphaMode) {
match (from, to) {
(AlphaMode::Straight, AlphaMode::Premultiplied) => {
for pixel in pixels.chunks_exact_mut(4) {
let alpha = u32::from(pixel[3]);
for channel in &mut pixel[..3] {
*channel = ((u32::from(*channel) * alpha + 127) / 255) as u8;
}
}
}
(AlphaMode::Premultiplied, AlphaMode::Straight) => {
for pixel in pixels.chunks_exact_mut(4) {
let alpha = u32::from(pixel[3]);
if alpha == 0 {
pixel[..3].fill(0);
continue;
}
for channel in &mut pixel[..3] {
*channel = ((u32::from(*channel) * 255 + alpha / 2) / alpha).min(255) as u8;
}
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::{AlphaMode, Image, source_over_straight_rgba};
use std::borrow::Cow;
#[test]
fn new_preserves_pixels_and_defaults_to_straight_alpha() {
let pixels = vec![20, 40, 60, 128];
let image = Image::new(1, 1, pixels.clone());
assert_eq!(image.pixels, pixels);
assert_eq!(image.alpha_mode(), AlphaMode::Straight);
}
#[test]
fn premultiplied_constructor_normalizes_to_canonical_straight_alpha() {
let image = Image::from_premultiplied_rgba(1, 1, vec![64, 32, 16, 128]);
assert_eq!(image.pixels, vec![128, 64, 32, 128]);
assert_eq!(image.alpha_mode(), AlphaMode::Straight);
}
#[test]
fn requested_premultiplied_pixels_convert_without_mutating_image() {
let image = Image::from_straight_rgba(2, 1, vec![128, 64, 32, 128, 7, 9, 11, 255]);
let premultiplied = image.pixels_in_alpha_mode(AlphaMode::Premultiplied);
assert_eq!(premultiplied.as_ref(), &[64, 32, 16, 128, 7, 9, 11, 255]);
assert_eq!(image.pixels, vec![128, 64, 32, 128, 7, 9, 11, 255]);
}
#[test]
fn normalizing_zero_alpha_discards_unrecoverable_rgb() {
let image = Image::from_premultiplied_rgba(1, 1, vec![99, 88, 77, 0]);
assert_eq!(image.pixels, vec![0, 0, 0, 0]);
}
#[test]
fn matching_alpha_mode_returns_borrowed_pixels() {
let image = Image::new(1, 1, vec![1, 2, 3, 4]);
assert!(matches!(
image.pixels_in_alpha_mode(AlphaMode::Straight),
Cow::Borrowed(_)
));
assert!(matches!(
image.pixels_in_alpha_mode(AlphaMode::Premultiplied),
Cow::Owned(_)
));
}
#[test]
fn png_encoding_uses_canonical_straight_pixels() {
let image = Image::from_premultiplied_rgba(1, 1, vec![64, 32, 16, 128]);
let encoded = image.encode_png().expect("PNG should encode");
let decoded = ::image::load_from_memory(&encoded)
.expect("PNG should decode")
.to_rgba8();
assert_eq!(decoded.as_raw(), &[128, 64, 32, 128]);
assert_eq!(image.pixels, vec![128, 64, 32, 128]);
assert_eq!(image.alpha_mode(), AlphaMode::Straight);
}
#[test]
fn public_struct_literal_remains_source_compatible_and_straight() {
let image = Image {
width: 1,
height: 1,
pixels: vec![10, 20, 30, 128],
};
assert_eq!(image.alpha_mode(), AlphaMode::Straight);
}
#[test]
fn straight_source_over_preserves_transparent_destination_color_math() {
assert_eq!(
source_over_straight_rgba([0, 0, 255, 0], [255, 0, 0, 128]),
[255, 0, 0, 128]
);
}
#[test]
fn straight_source_over_includes_translucent_destination_alpha() {
assert_eq!(
source_over_straight_rgba([0, 0, 255, 128], [255, 0, 0, 128]),
[170, 0, 85, 192]
);
}
}