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 wml2::draw::{
    AnimationLayer, EncodeOptions, ImageBuffer, ImageRect, NextBlend, NextDispose, NextOption,
    NextOptions, image_encoder, image_load,
};
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(
    x: i32,
    y: i32,
    width: usize,
    height: usize,
    delay_ms: u64,
    dispose: NextDispose,
    blend: NextBlend,
) -> NextOptions {
    NextOptions {
        flag: NextOption::Continue,
        await_time: delay_ms,
        image_rect: Some(ImageRect {
            start_x: x,
            start_y: y,
            width,
            height,
        }),
        dispose_option: Some(dispose),
        blend: Some(blend),
    }
}

fn apply_source_over(dst: &mut [u8], src: &[u8]) {
    let src_alpha = src[3] as u32;
    if src_alpha == 0 {
        return;
    }
    if src_alpha == 255 {
        dst.copy_from_slice(src);
        return;
    }

    let dst_alpha = dst[3] as u32;
    let out_alpha = src_alpha + ((dst_alpha * (255 - src_alpha) + 127) / 255);
    if out_alpha == 0 {
        dst.copy_from_slice(&[0, 0, 0, 0]);
        return;
    }

    for channel in 0..3 {
        let src_premul = src[channel] as u32 * src_alpha;
        let dst_premul = dst[channel] as u32 * dst_alpha;
        let out_premul = src_premul + ((dst_premul * (255 - src_alpha) + 127) / 255);
        dst[channel] = ((out_premul * 255 + (out_alpha / 2)) / out_alpha) as u8;
    }
    dst[3] = out_alpha as u8;
}

fn apply_frame(
    canvas: &mut [u8],
    canvas_width: usize,
    x: usize,
    y: usize,
    width: usize,
    height: usize,
    buffer: &[u8],
    blend: bool,
) {
    for row in 0..height {
        let src_row = row * width * 4;
        let dst_row = (y + row) * canvas_width * 4;
        for col in 0..width {
            let src_offset = src_row + col * 4;
            let dst_offset = dst_row + (x + col) * 4;
            if blend {
                apply_source_over(
                    &mut canvas[dst_offset..dst_offset + 4],
                    &buffer[src_offset..src_offset + 4],
                );
            } else {
                canvas[dst_offset..dst_offset + 4]
                    .copy_from_slice(&buffer[src_offset..src_offset + 4]);
            }
        }
    }
}

fn clear_rect(
    canvas: &mut [u8],
    canvas_width: usize,
    x: usize,
    y: usize,
    width: usize,
    height: usize,
) {
    for row in 0..height {
        let dst_row = (y + row) * canvas_width * 4;
        for col in 0..width {
            let dst_offset = dst_row + (x + col) * 4;
            canvas[dst_offset..dst_offset + 4].copy_from_slice(&[0, 0, 0, 0]);
        }
    }
}

fn animated_image() -> ImageBuffer {
    let frame0 = solid_rgba(2, 2, [255, 0, 0, 255]);
    let frame1 = solid_rgba(2, 2, [0, 0, 255, 128]);
    let frame2 = solid_rgba(2, 2, [0, 255, 0, 255]);

    let mut image = ImageBuffer::from_buffer(4, 4, vec![0; 4 * 4 * 4]);
    image.loop_count = Some(3);
    image.animation = Some(vec![
        AnimationLayer {
            width: 2,
            height: 2,
            start_x: 0,
            start_y: 0,
            buffer: frame0,
            control: frame_control(0, 0, 2, 2, 80, NextDispose::None, NextBlend::Override),
        },
        AnimationLayer {
            width: 2,
            height: 2,
            start_x: 1,
            start_y: 1,
            buffer: frame1,
            control: frame_control(1, 1, 2, 2, 120, NextDispose::Background, NextBlend::Source),
        },
        AnimationLayer {
            width: 2,
            height: 2,
            start_x: 2,
            start_y: 0,
            buffer: frame2,
            control: frame_control(2, 0, 2, 2, 40, NextDispose::None, NextBlend::Override),
        },
    ]);
    image
}

fn expected_animation_frames() -> Vec<Vec<u8>> {
    let frame0 = solid_rgba(2, 2, [255, 0, 0, 255]);
    let frame1 = solid_rgba(2, 2, [0, 0, 255, 128]);
    let frame2 = solid_rgba(2, 2, [0, 255, 0, 255]);
    let mut canvas = vec![0; 4 * 4 * 4];

    apply_frame(&mut canvas, 4, 0, 0, 2, 2, &frame0, false);
    let expected0 = canvas.clone();

    apply_frame(&mut canvas, 4, 1, 1, 2, 2, &frame1, true);
    let expected1 = canvas.clone();
    clear_rect(&mut canvas, 4, 1, 1, 2, 2);

    apply_frame(&mut canvas, 4, 2, 0, 2, 2, &frame2, false);
    let expected2 = canvas;

    vec![expected0, expected1, expected2]
}

fn flatten_partial_alpha_for_gif(rgba: &[u8]) -> Vec<u8> {
    let mut flattened = Vec::with_capacity(rgba.len());
    for pixel in rgba.chunks_exact(4) {
        match pixel[3] {
            0 => flattened.extend_from_slice(pixel),
            255 => flattened.extend_from_slice(pixel),
            alpha => {
                flattened.push(((pixel[0] as u32 * alpha as u32 + 127) / 255) as u8);
                flattened.push(((pixel[1] as u32 * alpha as u32 + 127) / 255) as u8);
                flattened.push(((pixel[2] as u32 * alpha as u32 + 127) / 255) as u8);
                flattened.push(255);
            }
        }
    }
    flattened
}

#[test]
fn encode_gif_preserves_exact_palette_without_dithering() {
    let palette = [
        [0, 0, 0, 255],
        [255, 0, 0, 255],
        [0, 255, 0, 255],
        [0, 0, 255, 255],
        [255, 255, 0, 255],
        [255, 0, 255, 255],
        [0, 255, 255, 255],
        [255, 255, 255, 255],
    ];
    let mut rgba = Vec::with_capacity(16 * 16 * 4);
    for y in 0..16 {
        for x in 0..16 {
            rgba.extend_from_slice(&palette[(x + y) % palette.len()]);
        }
    }

    let mut image = ImageBuffer::from_buffer(16, 16, rgba.clone());
    let mut encode = EncodeOptions {
        debug_flag: 0,
        drawer: &mut image,
        options: None,
    };
    let data = image_encoder(&mut encode, ImageFormat::Gif).unwrap();

    assert!(data.starts_with(b"GIF89a"));
    let decoded = image_load(&data).unwrap();
    assert_eq!(decoded.width, 16);
    assert_eq!(decoded.height, 16);
    assert_eq!(decoded.buffer.as_ref().unwrap(), &rgba);
}

#[test]
fn encode_gif_reserves_transparent_index() {
    let mut rgba = vec![0; 4 * 4 * 4];
    for y in 0..4usize {
        for x in 0..4usize {
            let offset = (y * 4 + x) * 4;
            if (x + y) % 2 == 0 {
                rgba[offset..offset + 4].copy_from_slice(&[255, 0, 0, 255]);
            }
        }
    }

    let mut image = ImageBuffer::from_buffer(4, 4, rgba.clone());
    let mut encode = EncodeOptions {
        debug_flag: 0,
        drawer: &mut image,
        options: None,
    };
    let data = image_encoder(&mut encode, ImageFormat::Gif).unwrap();

    assert!(data.starts_with(b"GIF89a"));
    assert!(data.windows(2).any(|window| window == [0x21, 0xf9]));

    let decoded = image_load(&data).unwrap();
    assert_eq!(decoded.buffer.as_ref().unwrap(), &rgba);
}

#[test]
fn encode_animated_gif_via_public_api() {
    let expected = expected_animation_frames();
    let expected_gif_frame1 = flatten_partial_alpha_for_gif(&expected[1]);
    let mut image = animated_image();
    let mut encode = EncodeOptions {
        debug_flag: 0,
        drawer: &mut image,
        options: None,
    };

    let data = image_encoder(&mut encode, ImageFormat::Gif).unwrap();
    assert!(data.starts_with(b"GIF89a"));

    let decoded = image_load(&data).unwrap();
    assert_eq!(decoded.width, 4);
    assert_eq!(decoded.height, 4);
    assert_eq!(decoded.loop_count, Some(3));
    assert_eq!(decoded.first_wait_time, Some(80));
    assert_eq!(decoded.buffer.as_ref().unwrap(), &expected[0]);
    assert_eq!(
        decoded.animation.as_ref().map(|frames| frames.len()),
        Some(3)
    );
    let frames = decoded.animation.as_ref().unwrap();
    assert_eq!(frames[0].buffer, expected[0]);
    assert_eq!(frames[1].buffer, expected_gif_frame1);
    assert_eq!(frames[2].buffer, expected[2]);
}

#[test]
fn encode_gif_semitransparent_canvas_via_public_api() {
    let expected = expected_animation_frames();
    let mut image = ImageBuffer::from_buffer(4, 4, expected[1].clone());
    let mut encode = EncodeOptions {
        debug_flag: 0,
        drawer: &mut image,
        options: None,
    };

    let data = image_encoder(&mut encode, ImageFormat::Gif).unwrap();
    let decoded = image_load(&data).unwrap();
    assert_eq!(decoded.width, 4);
    assert_eq!(decoded.height, 4);
}

#[test]
fn encode_gif_third_animation_canvas_via_public_api() {
    let expected = expected_animation_frames();
    let mut image = ImageBuffer::from_buffer(4, 4, expected[2].clone());
    let mut encode = EncodeOptions {
        debug_flag: 0,
        drawer: &mut image,
        options: None,
    };

    let data = image_encoder(&mut encode, ImageFormat::Gif).unwrap();
    let decoded = image_load(&data).unwrap();
    assert_eq!(decoded.width, 4);
    assert_eq!(decoded.height, 4);
}