valo_codec_software/
encode.rs1use std::borrow::Cow;
5use std::fmt;
6
7use image::{ExtendedColorType, ImageEncoder};
8#[cfg(feature = "jpeg")]
9use valo::Color;
10use valo::{AlphaType, PixelBuffer, PixelFormat};
11
12#[derive(Clone, Copy, Debug, PartialEq)]
14pub enum Encoding {
15 #[cfg(feature = "png")]
17 Png,
18 #[cfg(feature = "jpeg")]
21 Jpeg {
22 quality: u8,
24 background: Color,
26 },
27 #[cfg(feature = "webp")]
29 WebpLossless,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
34pub enum EncodeError {
35 Quality(u8),
37 Failed(String),
39}
40
41impl fmt::Display for EncodeError {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 EncodeError::Quality(quality) => {
45 write!(f, "a JPEG's quality is 1 to 100, not {quality}")
46 }
47 EncodeError::Failed(why) => write!(f, "encoding failed: {why}"),
48 }
49 }
50}
51
52impl std::error::Error for EncodeError {}
53
54pub fn encode(pixels: &PixelBuffer, encoding: Encoding) -> Result<Vec<u8>, EncodeError> {
57 let rgba = straight_rgba(pixels);
58 let [width, height] = pixels.layout().size;
59 let mut bytes = Vec::new();
60 let written = match encoding {
61 #[cfg(feature = "png")]
62 Encoding::Png => image::codecs::png::PngEncoder::new(&mut bytes).write_image(
63 &rgba,
64 width,
65 height,
66 ExtendedColorType::Rgba8,
67 ),
68 #[cfg(feature = "jpeg")]
69 Encoding::Jpeg {
70 quality,
71 background,
72 } => {
73 if !(1..=100).contains(&quality) {
74 return Err(EncodeError::Quality(quality));
75 }
76 image::codecs::jpeg::JpegEncoder::new_with_quality(&mut bytes, quality).write_image(
77 &over(&rgba, background),
78 width,
79 height,
80 ExtendedColorType::Rgb8,
81 )
82 }
83 #[cfg(feature = "webp")]
84 Encoding::WebpLossless => image::codecs::webp::WebPEncoder::new_lossless(&mut bytes)
85 .write_image(&rgba, width, height, ExtendedColorType::Rgba8),
86 };
87 written.map_err(|error| EncodeError::Failed(error.to_string()))?;
88 Ok(bytes)
89}
90
91fn straight_rgba(pixels: &PixelBuffer) -> Cow<'_, [u8]> {
93 let layout = pixels.layout();
94 let row = layout.size[0] as usize * 4;
95 let packed = layout.row_bytes as usize == row;
96 if packed && layout.format == PixelFormat::Rgba8 && layout.alpha == AlphaType::Straight {
97 return Cow::Borrowed(pixels.bytes());
98 }
99 let mut rgba: Vec<u8> = pixels
100 .bytes()
101 .chunks(layout.row_bytes as usize)
102 .take(layout.size[1] as usize)
103 .flat_map(|samples| &samples[..row])
104 .copied()
105 .collect();
106 if layout.format == PixelFormat::Bgra8 {
107 for pixel in rgba.as_chunks_mut::<4>().0 {
108 pixel.swap(0, 2);
109 }
110 }
111 if layout.alpha == AlphaType::Premultiplied {
112 valo::unpremultiply(&mut rgba);
113 }
114 Cow::Owned(rgba)
115}
116
117#[cfg(feature = "jpeg")]
119fn over(rgba: &[u8], background: Color) -> Vec<u8> {
120 let below = [background.r, background.g, background.b]
121 .map(|channel| (channel.clamp(0.0, 1.0) * 255.0).round() as u16);
122 rgba.as_chunks::<4>()
123 .0
124 .iter()
125 .flat_map(|&[red, green, blue, alpha]| {
126 let alpha = u16::from(alpha);
127 let mut rgb = [red, green, blue];
128 for (channel, below) in rgb.iter_mut().zip(below) {
129 *channel =
130 ((u16::from(*channel) * alpha + below * (255 - alpha) + 127) / 255) as u8;
131 }
132 rgb
133 })
134 .collect()
135}