img2avif 0.1.4

Convert images to AVIF format.
Documentation
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom};

/// Converts the input file to AVIF format using default quality (85) and speed (6).
///
/// # Arguments
/// * `file` - An input stream implementing `Read` and `Seek`.
///
/// # Returns
/// * `Ok(Vec<u8>)` - The converted AVIF file as a byte stream.
/// * `Err(image::ImageError)` - Error occurred during conversion.
pub fn convert<F>(file: &mut F) -> Result<Vec<u8>, image::ImageError>
where
    F: Seek + Read,
{
    let quality = 85;
    let speed = 6;

    convert_with_quality_speed(file, quality, speed)
}

/// Converts the input file to AVIF format with custom quality and speed parameters.
///
/// # Arguments
/// * `file` - An input stream implementing `Read` and `Seek`. Must be mutable.
/// * `quality` - AVIF encoding quality (0-100).
/// * `speed` - AVIF encoding speed (0-10).
///
/// # Returns
/// * `Ok(Vec<u8>)` - The converted AVIF file as a byte stream.
/// * `Err(image::ImageError)` - Error occurred during conversion.
pub fn convert_with_quality_speed<F>(
    file: &mut F,
    quality: u8,
    speed: u8,
) -> Result<Vec<u8>, image::ImageError>
where
    F: Seek + Read,
{
    let reader = BufReader::new(file);

    let img = image::ImageReader::new(reader)
        .with_guessed_format()?
        .decode()?;

    let mut buffer = Cursor::new(Vec::new());

    let avif_encoder =
        image::codecs::avif::AvifEncoder::new_with_speed_quality(&mut buffer, speed, quality);

    img.write_with_encoder(avif_encoder)?;

    let output = buffer.into_inner();

    Ok(output)
}

/// Converts the input file to AVIF format, attempting to stay below a maximum file size.
/// It iteratively reduces quality based on the stride until the size constraint is met or quality reaches 0.
///
/// # Arguments
/// * `file` - An input stream implementing `Read` and `Seek`. Must be mutable.
/// * `stride` - Optional step size (as u8) to decrease quality in each iteration if the output size exceeds `max_size`. Defaults to 10.
/// * `speed` - AVIF encoding speed (0-10).
/// * `max_size` - The target maximum size in bytes for the output AVIF file. The unit is in bytes.
///
/// # Returns
/// * `Ok(Vec<u8>)` - The converted AVIF file as a byte stream, ideally under `max_size`.
/// * `Err(image::ImageError)` - Error occurred during conversion or if quality reduction fails to meet the size constraint.
pub fn convert_with_max_size<F>(
    file: &mut F,
    stride: Option<u8>,
    speed: Option<u8>,
    max_size: usize,
) -> Result<Vec<u8>, image::ImageError>
where
    F: Seek + Read,
{
    let mut quality = 100u8;
    let stride = stride.unwrap_or(10);
    let min_quality = 1u8;

    let speed = speed.unwrap_or(6);

    loop {
        file.seek(SeekFrom::Start(0))?;

        let output = convert_with_quality_speed(file, quality, speed)?;

        if output.len() <= max_size {
            if output.is_empty() {
                return Err(image::ImageError::Encoding(
                    image::error::EncodingError::new(
                        image::error::ImageFormatHint::Name("AVIF".to_string()),
                        "Encoding produced zero bytes, possibly due to extremely low quality.",
                    ),
                ));
            }
            return Ok(output);
        }

        if quality <= min_quality || quality < stride {
            return Err(image::ImageError::Encoding(
                image::error::EncodingError::new(
                    image::error::ImageFormatHint::Name("AVIF".to_string()),
                    format!(
                        "Could not meet max_size constraint ({}) even at lowest practical quality ({})",
                        max_size, quality
                    ),
                ),
            ));
        }

        quality = quality.saturating_sub(stride);
        quality = quality.max(min_quality);
    }
}

#[cfg(test)]
mod tests {
    use std::fs::File;

    use super::*;

    // Helper to open file mutably for tests, ensuring it's readable
    fn open_test_file(path: &str) -> File {
        File::options()
            .read(true)
            .open(path)
            .unwrap_or_else(|_| panic!("Failed to open test image: {}", path))
    }

    #[test]
    fn test_png2avif() {
        let mut file = open_test_file("./test/test.png");
        let result = convert(&mut file);
        assert!(result.is_ok(), "PNG Conversion failed: {:?}", result.err());
        let output_data = result.unwrap();
        assert!(!output_data.is_empty(), "PNG Output AVIF data is empty");
    }

    #[test]
    fn test_jpeg2avif() {
        let mut file = open_test_file("./test/test.jpg");
        let result = convert(&mut file);
        assert!(result.is_ok(), "JPEG Conversion failed: {:?}", result.err());
        let output_data = result.unwrap();
        assert!(!output_data.is_empty(), "JPEG Output AVIF data is empty");
    }

    #[test]
    fn test_webp2avif() {
        let mut file = open_test_file("./test/test.webp");
        let result = convert(&mut file);
        assert!(result.is_ok(), "WEBP Conversion failed: {:?}", result.err());
        let output_data = result.unwrap();
        assert!(!output_data.is_empty(), "WEBP Output AVIF data is empty");
    }

    #[test]
    fn test_error_file() {
        let mut file = open_test_file("./test/error.jpg");
        let result = convert(&mut file);
        assert!(
            result.is_err(),
            "Expected an error for unsupported/corrupt file type"
        );
    }

    #[test]
    fn test_max_size_success() {
        let mut file = open_test_file("./test/test.png");
        let max_size = 200 * 1024;
        let speed = Some(5u8);
        let stride = Some(10u8);

        let result = convert_with_max_size(&mut file, stride, speed, max_size);

        assert!(
            result.is_ok(),
            "convert_with_max_size failed when it should succeed: {:?}",
            result.err()
        );
        let output_data = result.unwrap();
        assert!(!output_data.is_empty(), "Max size output data is empty");
        assert!(
            output_data.len() <= max_size,
            "Output size {} exceeds max_size {}",
            output_data.len(),
            max_size
        );
        println!(
            "Max size success: Achieved size {} bytes (limit {})",
            output_data.len(),
            max_size
        );
    }

    #[test]
    fn test_max_size_fail() {
        let mut file = open_test_file("./test/test.png");
        let very_small_max_size = 100;
        let speed = Some(5u8);
        let stride = Some(20u8);

        let result = convert_with_max_size(&mut file, stride, speed, very_small_max_size);

        assert!(
            result.is_err(),
            "convert_with_max_size succeeded when it should fail (max_size too small)"
        );
        assert!(
            matches!(result.err().unwrap(), image::ImageError::Encoding(_)),
            "Expected an Encoding error due to size constraint"
        );
        println!(
            "Max size fail: Correctly failed for max_size {}",
            very_small_max_size
        );
    }
}