hashavatar 1.1.0

Stable deterministic procedural avatars in Rust with configurable identity hashing, WebP, optional PNG/JPEG/GIF, and SVG export
Documentation
use super::*;

pub(crate) fn encode_rgba_image(
    image: &RgbaImage,
    format: AvatarOutputFormat,
) -> ImageResult<Vec<u8>> {
    let mut bytes = SanitizingVec::with_capacity(image.as_raw().len());
    let result = {
        let cursor = Cursor::new(bytes.as_mut_vec());
        encode_into_writer(image, format, cursor)
    };
    match result {
        Ok(()) => Ok(bytes.into_inner()),
        Err(error) => Err(error),
    }
}

pub(crate) fn encode_owned_rgba_image(
    image: RgbaImage,
    format: AvatarOutputFormat,
) -> ImageResult<Vec<u8>> {
    let image = SanitizingRgbaImage::new(image);
    encode_rgba_image(image.as_image(), format)
}

pub(crate) struct SanitizingRgbaImage {
    image: RgbaImage,
}

impl SanitizingRgbaImage {
    fn new(image: RgbaImage) -> Self {
        Self { image }
    }

    fn as_image(&self) -> &RgbaImage {
        &self.image
    }
}

impl Drop for SanitizingRgbaImage {
    fn drop(&mut self) {
        sanitize_rgba_pixels(&mut self.image);
    }
}

pub(crate) fn sanitize_rgba_pixels(image: &mut RgbaImage) {
    let pixels: &mut [u8] = image.as_mut();
    sanitize_bytes(pixels);
}

pub(crate) struct SanitizingVec {
    bytes: Vec<u8>,
}

impl SanitizingVec {
    fn with_capacity(capacity: usize) -> Self {
        Self {
            bytes: Vec::with_capacity(capacity),
        }
    }

    #[cfg(feature = "jpeg")]
    fn from_vec(bytes: Vec<u8>) -> Self {
        Self { bytes }
    }

    fn as_mut_vec(&mut self) -> &mut Vec<u8> {
        &mut self.bytes
    }

    #[cfg(feature = "jpeg")]
    fn as_slice(&self) -> &[u8] {
        &self.bytes
    }

    fn into_inner(mut self) -> Vec<u8> {
        std::mem::take(&mut self.bytes)
    }
}

impl Drop for SanitizingVec {
    fn drop(&mut self) {
        volatile_sanitize_vec(&mut self.bytes);
    }
}

pub(crate) fn encode_into_writer<W: std::io::Write>(
    image: &RgbaImage,
    format: AvatarOutputFormat,
    writer: W,
) -> ImageResult<()> {
    match format {
        AvatarOutputFormat::WebP => WebPEncoder::new_lossless(writer).write_image(
            image.as_raw(),
            image.width(),
            image.height(),
            ExtendedColorType::Rgba8,
        ),
        #[cfg(feature = "png")]
        AvatarOutputFormat::Png => {
            PngEncoder::new_with_quality(writer, CompressionType::Best, FilterType::Adaptive)
                .write_image(
                    image.as_raw(),
                    image.width(),
                    image.height(),
                    ExtendedColorType::Rgba8,
                )
        }
        #[cfg(feature = "jpeg")]
        AvatarOutputFormat::Jpeg => {
            let rgb = SanitizingVec::from_vec(rgba_to_rgb_over_white(image));
            JpegEncoder::new_with_quality(writer, 92).write_image(
                rgb.as_slice(),
                image.width(),
                image.height(),
                ExtendedColorType::Rgb8,
            )
        }
        #[cfg(feature = "gif")]
        AvatarOutputFormat::Gif => GifEncoder::new(writer).write_image(
            image.as_raw(),
            image.width(),
            image.height(),
            ExtendedColorType::Rgba8,
        ),
    }
}

#[cfg(any(feature = "jpeg", test))]
pub(crate) fn rgba_to_rgb_over_white(image: &RgbaImage) -> Vec<u8> {
    let mut rgb = Vec::with_capacity(image.as_raw().len() / 4 * 3);
    for pixel in image.pixels() {
        let [red, green, blue, alpha] = pixel.0;
        let alpha = u32::from(alpha);
        let inverse_alpha = 255 - alpha;
        rgb.push(((u32::from(red) * alpha + 255 * inverse_alpha + 127) / 255) as u8);
        rgb.push(((u32::from(green) * alpha + 255 * inverse_alpha + 127) / 255) as u8);
        rgb.push(((u32::from(blue) * alpha + 255 * inverse_alpha + 127) / 255) as u8);
    }
    rgb
}