use std::borrow::Cow;
use std::fmt;
use image::{ExtendedColorType, ImageEncoder};
#[cfg(feature = "jpeg")]
use valo::Color;
use valo::{AlphaType, PixelBuffer, PixelFormat};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Encoding {
#[cfg(feature = "png")]
Png,
#[cfg(feature = "jpeg")]
Jpeg {
quality: u8,
background: Color,
},
#[cfg(feature = "webp")]
WebpLossless,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EncodeError {
Quality(u8),
Failed(String),
}
impl fmt::Display for EncodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EncodeError::Quality(quality) => {
write!(f, "a JPEG's quality is 1 to 100, not {quality}")
}
EncodeError::Failed(why) => write!(f, "encoding failed: {why}"),
}
}
}
impl std::error::Error for EncodeError {}
pub fn encode(pixels: &PixelBuffer, encoding: Encoding) -> Result<Vec<u8>, EncodeError> {
let rgba = straight_rgba(pixels);
let [width, height] = pixels.layout().size;
let mut bytes = Vec::new();
let written = match encoding {
#[cfg(feature = "png")]
Encoding::Png => image::codecs::png::PngEncoder::new(&mut bytes).write_image(
&rgba,
width,
height,
ExtendedColorType::Rgba8,
),
#[cfg(feature = "jpeg")]
Encoding::Jpeg {
quality,
background,
} => {
if !(1..=100).contains(&quality) {
return Err(EncodeError::Quality(quality));
}
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut bytes, quality).write_image(
&over(&rgba, background),
width,
height,
ExtendedColorType::Rgb8,
)
}
#[cfg(feature = "webp")]
Encoding::WebpLossless => image::codecs::webp::WebPEncoder::new_lossless(&mut bytes)
.write_image(&rgba, width, height, ExtendedColorType::Rgba8),
};
written.map_err(|error| EncodeError::Failed(error.to_string()))?;
Ok(bytes)
}
fn straight_rgba(pixels: &PixelBuffer) -> Cow<'_, [u8]> {
let layout = pixels.layout();
let row = layout.size[0] as usize * 4;
let packed = layout.row_bytes as usize == row;
if packed && layout.format == PixelFormat::Rgba8 && layout.alpha == AlphaType::Straight {
return Cow::Borrowed(pixels.bytes());
}
let mut rgba: Vec<u8> = pixels
.bytes()
.chunks(layout.row_bytes as usize)
.take(layout.size[1] as usize)
.flat_map(|samples| &samples[..row])
.copied()
.collect();
if layout.format == PixelFormat::Bgra8 {
for pixel in rgba.as_chunks_mut::<4>().0 {
pixel.swap(0, 2);
}
}
if layout.alpha == AlphaType::Premultiplied {
valo::unpremultiply(&mut rgba);
}
Cow::Owned(rgba)
}
#[cfg(feature = "jpeg")]
fn over(rgba: &[u8], background: Color) -> Vec<u8> {
let below = [background.r, background.g, background.b]
.map(|channel| (channel.clamp(0.0, 1.0) * 255.0).round() as u16);
rgba.as_chunks::<4>()
.0
.iter()
.flat_map(|&[red, green, blue, alpha]| {
let alpha = u16::from(alpha);
let mut rgb = [red, green, blue];
for (channel, below) in rgb.iter_mut().zip(below) {
*channel =
((u16::from(*channel) * alpha + below * (255 - alpha) + 127) / 255) as u8;
}
rgb
})
.collect()
}