wml2 0.0.21

Pure Rust multi-format image decoding and encoding library supporting JPEG, PNG, GIF, WebP, TIFF and PC-98 legacy formats (MAG, MAKI, PI, PIC)
Documentation
use std::collections::HashMap;

use bin_rs::Endian;
use wml2::draw::{
    AnimationLayer, ImageBuffer, ImageRect, NextBlend, NextDispose, NextOption, NextOptions,
    image_load, image_to,
};
use wml2::metadata::DataMap;
use wml2::tiff::header::{DataPack, TiffHeader, TiffHeaders, exif_to_bytes};
use wml2::util::ImageFormat;

fn solid_rgba(width: usize, height: usize, rgba: [u8; 4]) -> Vec<u8> {
    let mut buffer = Vec::with_capacity(width * height * 4);
    for _ in 0..(width * height) {
        buffer.extend_from_slice(&rgba);
    }
    buffer
}

fn frame_control(width: usize, height: usize, delay_ms: u64) -> NextOptions {
    NextOptions {
        flag: NextOption::Continue,
        await_time: delay_ms,
        image_rect: Some(ImageRect {
            start_x: 0,
            start_y: 0,
            width,
            height,
        }),
        dispose_option: Some(NextDispose::None),
        blend: Some(NextBlend::Override),
    }
}

fn exif_bytes() -> Vec<u8> {
    let mut headers = TiffHeaders::empty(Endian::LittleEndian);
    headers.headers.push(TiffHeader {
        tagid: 0x010f,
        data: DataPack::Ascii("wml2".to_string()),
        length: 4,
    });
    exif_to_bytes(&headers).unwrap()
}

#[test]
fn image_to_encodes_png_from_imagebuffer() {
    let rgba = solid_rgba(4, 3, [32, 64, 96, 255]);
    let mut image = ImageBuffer::from_buffer(4, 3, rgba);

    let png = image_to(&mut image, ImageFormat::Png, None).unwrap();

    assert!(png.starts_with(&[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
    let decoded = image_load(&png).unwrap();
    assert_eq!(decoded.width, 4);
    assert_eq!(decoded.height, 3);
}

#[test]
fn image_to_encodes_bmp_from_imagebuffer() {
    let rgba = solid_rgba(3, 2, [12, 34, 56, 255]);
    let mut image = ImageBuffer::from_buffer(3, 2, rgba.clone());

    let bmp = image_to(&mut image, ImageFormat::Bmp, None).unwrap();

    assert!(bmp.starts_with(b"BM"));
    let decoded = image_load(&bmp).unwrap();
    assert_eq!(decoded.width, 3);
    assert_eq!(decoded.height, 2);
    assert_eq!(decoded.buffer.unwrap(), rgba);
}

#[test]
fn image_to_encodes_animated_webp_from_imagebuffer() {
    let first = solid_rgba(2, 2, [255, 0, 0, 255]);
    let second = solid_rgba(2, 2, [0, 255, 0, 255]);

    let mut image = ImageBuffer::from_buffer(2, 2, first.clone());
    image.loop_count = Some(2);
    image.animation = Some(vec![
        AnimationLayer {
            width: 2,
            height: 2,
            start_x: 0,
            start_y: 0,
            buffer: first,
            control: frame_control(2, 2, 120),
        },
        AnimationLayer {
            width: 2,
            height: 2,
            start_x: 0,
            start_y: 0,
            buffer: second,
            control: frame_control(2, 2, 240),
        },
    ]);

    let webp = image_to(&mut image, ImageFormat::Webp, None).unwrap();

    assert!(webp.starts_with(b"RIFF"));
    assert!(webp.windows(4).any(|window| window == b"ANIM"));
    let decoded = image_load(&webp).unwrap();
    assert!(
        decoded
            .animation
            .as_ref()
            .map(|frames| frames.len())
            .unwrap_or(0)
            > 1
    );
}

#[test]
fn image_to_forwards_exif_options() {
    let rgba = solid_rgba(4, 4, [10, 20, 30, 255]);
    let mut image = ImageBuffer::from_buffer(4, 4, rgba);
    let mut options = HashMap::new();
    options.insert("exif".to_string(), DataMap::Raw(exif_bytes()));

    let png = image_to(&mut image, ImageFormat::Png, Some(options)).unwrap();

    let decoded = image_load(&png).unwrap();
    let metadata = decoded.metadata.as_ref().unwrap();
    assert!(matches!(metadata.get("EXIF"), Some(DataMap::Exif(_))));
    assert!(matches!(metadata.get("EXIF Raw"), Some(DataMap::Raw(_))));
}