use std::borrow::Cow;
#[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)
}
}
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]
);
}
}