Skip to main content

valo_codec_software/
encode.rs

1//! Encoding pixels as image files on the CPU, the way back from decoding: the same codec
2//! libraries, compiled in by the same Cargo features.
3
4use 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/// `Encoding` is the file `encode` writes, with its format's settings.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub enum Encoding {
15    /// `Png` is an RGBA PNG, transparency kept.
16    #[cfg(feature = "png")]
17    Png,
18    /// `Jpeg` is a JPEG at `quality`, from 1 to 100, laid over `background`, since a JPEG holds
19    /// no transparency; the background's own alpha is ignored.
20    #[cfg(feature = "jpeg")]
21    Jpeg {
22        /// `quality` trades size for detail, from 1 to 100.
23        quality: u8,
24        /// `background` shows through where the pixels are transparent.
25        background: Color,
26    },
27    /// `WebpLossless` is a lossless WebP, transparency kept.
28    #[cfg(feature = "webp")]
29    WebpLossless,
30}
31
32/// `EncodeError` is why pixels could not be encoded.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub enum EncodeError {
35    /// `Quality` is a JPEG quality outside 1 to 100.
36    Quality(u8),
37    /// `Failed` is the encoder failing, with its reason.
38    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
54/// `encode` writes `pixels` as a file in `encoding`. Pixels of any layout are taken, turned into
55/// packed rows of straight RGBA first when they aren't already.
56pub 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
91/// `straight_rgba` is `pixels` as packed rows of straight RGBA, borrowed when they already are.
92fn 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/// `over` is straight RGBA laid over an opaque `background`, as RGB.
118#[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}