zenbitmaps 0.1.5

PNM/PAM/PFM, BMP, farbfeld, QOI, TGA, and Radiance HDR image codec
Documentation
//! QOI encoder.
//!
//! Uses [rapid-qoi](https://github.com/zakarumych/rapid-qoi) for encoding.

use alloc::vec::Vec;
use enough::Stop;

use crate::error::BitmapError;
use crate::pixel::PixelLayout;

/// Encode pixels as QOI format.
///
/// Accepts `Rgb8`, `Rgba8`, `Bgr8` (swizzled to RGB), `Bgra8` (swizzled to RGBA).
pub(crate) fn encode_qoi(
    pixels: &[u8],
    width: u32,
    height: u32,
    layout: PixelLayout,
    stop: &dyn Stop,
) -> Result<Vec<u8>, BitmapError> {
    let w = width as usize;
    let h = height as usize;
    let bpp = layout.bytes_per_pixel();
    let expected = w
        .checked_mul(h)
        .and_then(|wh| wh.checked_mul(bpp))
        .ok_or(BitmapError::DimensionsTooLarge { width, height })?;
    if pixels.len() < expected {
        return Err(BitmapError::BufferTooSmall {
            needed: expected,
            actual: pixels.len(),
        });
    }

    stop.check()?;

    // Determine QOI color space and prepare pixel data
    let (qoi_pixels, colors) = match layout {
        PixelLayout::Rgb8 => (None, rapid_qoi::Colors::Srgb),
        PixelLayout::Rgba8 => (None, rapid_qoi::Colors::SrgbLinA),
        PixelLayout::Bgr8 => {
            // Swizzle BGR → RGB
            stop.check()?;
            let mut rgb = pixels[..expected].to_vec();
            #[cfg(feature = "simd")]
            {
                let _ = garb::bytes::rgb_to_bgr_inplace(&mut rgb);
            }
            #[cfg(not(feature = "simd"))]
            for pixel in rgb.chunks_exact_mut(3) {
                pixel.swap(0, 2);
            }
            (Some(rgb), rapid_qoi::Colors::Srgb)
        }
        PixelLayout::Bgra8 => {
            // Swizzle BGRA → RGBA
            stop.check()?;
            let mut rgba = pixels[..expected].to_vec();
            #[cfg(feature = "simd")]
            {
                let _ = garb::bytes::rgba_to_bgra_inplace(&mut rgba);
            }
            #[cfg(not(feature = "simd"))]
            for pixel in rgba.chunks_exact_mut(4) {
                pixel.swap(0, 2);
            }
            (Some(rgba), rapid_qoi::Colors::SrgbLinA)
        }
        PixelLayout::Bgrx8 => {
            // Swizzle BGRX → RGBA (set alpha=255)
            stop.check()?;
            let mut rgba = pixels[..expected].to_vec();
            #[cfg(feature = "simd")]
            {
                let _ = garb::bytes::rgba_to_bgra_inplace(&mut rgba);
                let _ = garb::bytes::fill_alpha_rgba(&mut rgba);
            }
            #[cfg(not(feature = "simd"))]
            for pixel in rgba.chunks_exact_mut(4) {
                pixel.swap(0, 2);
                pixel[3] = 255;
            }
            (Some(rgba), rapid_qoi::Colors::SrgbLinA)
        }
        _ => {
            return Err(BitmapError::UnsupportedVariant(alloc::format!(
                "cannot encode {layout:?} as QOI (supported: Rgb8, Rgba8, Bgr8, Bgra8)"
            )));
        }
    };

    let qoi = rapid_qoi::Qoi {
        width,
        height,
        colors,
    };

    let encode_data = qoi_pixels.as_deref().unwrap_or(&pixels[..expected]);
    let encoded = qoi
        .encode_alloc(encode_data)
        .map_err(|e| BitmapError::InvalidData(alloc::format!("{e:?}")))?;

    Ok(encoded)
}