valo-codec 0.4.3

Image decoding for Valo: a decoder interface, ordered fallback, and inline or worker-thread execution
Documentation
mod support;

use pollster::block_on;
use std::time::Duration;
use support::*;
use valo_codec::{DecodeError, DecodeLimits, DecodeOptions, ImageLoader, Repetition};

#[test]
fn the_first_decoder_that_accepts_owns_the_image_and_later_ones_are_not_asked() {
    let log = Log::default();
    let loader = ImageLoader::new(
        images(),
        decoders([
            FakeDecoder::new("native").declining("animation"),
            FakeDecoder::new("software").sharing_log(&log),
            FakeDecoder::new("never").sharing_log(&log),
        ]),
    );
    let image = block_on(loader.decode(bytes(), DecodeOptions::default())).unwrap();
    assert_eq!(image.size(), [1, 1]);
    assert_eq!(
        log.events(),
        ["open:software", "frame:software:0", "drop:software"]
    );
}

#[test]
fn every_decoder_declining_reports_each_reason_and_a_failure_stops_the_search() {
    let loader = ImageLoader::new(
        images(),
        decoders([
            FakeDecoder::new("native").declining("no such container"),
            FakeDecoder::new("software").declining("unknown format"),
        ]),
    );
    match block_on(loader.decode(bytes(), DecodeOptions::default())) {
        Err(DecodeError::Unsupported(declined)) => {
            let names: Vec<_> = declined.iter().map(|d| d.decoder).collect();
            assert_eq!(names, ["native", "software"]);
            assert_eq!(declined[1].reason, "unknown format");
        }
        other => panic!("{other:?}"),
    }

    let log = Log::default();
    let loader = ImageLoader::new(
        images(),
        decoders([
            FakeDecoder::new("native").failing(DecodeError::InvalidData("truncated".into())),
            FakeDecoder::new("software").sharing_log(&log),
        ]),
    );
    assert!(matches!(
        block_on(loader.decode(bytes(), DecodeOptions::default())),
        Err(DecodeError::InvalidData(_))
    ));
    assert!(log.events().is_empty(), "a failure must not fall through");
}

#[test]
fn no_decoders_and_empty_or_oversized_input_are_rejected_before_any_decoder_runs() {
    assert!(matches!(
        block_on(ImageLoader::new(images(), Vec::new()).decode(bytes(), DecodeOptions::default())),
        Err(DecodeError::NoDecoder)
    ));
    let log = Log::default();
    let loader = ImageLoader::new(
        images(),
        decoders([FakeDecoder::new("software").sharing_log(&log)]),
    );
    assert!(matches!(
        block_on(loader.decode(std::sync::Arc::from([]), DecodeOptions::default())),
        Err(DecodeError::InvalidData(_))
    ));
    let tiny = DecodeOptions {
        limits: DecodeLimits {
            max_encoded_bytes: 2,
            ..Default::default()
        },
        ..Default::default()
    };
    assert!(matches!(
        block_on(loader.decode(bytes(), tiny)),
        Err(DecodeError::LimitExceeded("encoded bytes"))
    ));
    assert!(log.events().is_empty());
}

#[test]
fn inline_work_happens_at_the_first_poll_and_never_if_dropped_first() {
    let log = Log::default();
    let loader = ImageLoader::new(
        images(),
        decoders([FakeDecoder::new("software").sharing_log(&log)]),
    );
    drop(loader.decode(bytes(), DecodeOptions::default()));
    assert!(log.events().is_empty());
    let mut pending = loader.decode(bytes(), DecodeOptions::default());
    assert!(log.events().is_empty());
    assert!(pending.try_take().unwrap().is_ok());
    assert_eq!(log.events().len(), 3);
}

#[test]
fn a_codec_hands_out_frames_in_order_and_wraps_after_the_last() {
    let loader = ImageLoader::new(
        images(),
        decoders([FakeDecoder::new("software").frames(3, Repetition::Times(2))]),
    );
    let mut codec = block_on(loader.open(bytes(), DecodeOptions::default())).unwrap();
    assert_eq!(codec.info().frame_count, 3);
    assert_eq!(codec.info().repetition, Repetition::Times(2));
    let durations: Vec<_> = (0..4)
        .map(|_| block_on(codec.next_frame()).unwrap().duration)
        .collect();
    assert_eq!(durations, [10, 11, 12, 10].map(Duration::from_millis));
}

#[test]
fn a_frame_failure_is_reported_and_the_codec_stays_usable() {
    let mut decoder = FakeDecoder::new("software").frames(2, Repetition::Forever);
    decoder.fail_frame = Some((1, DecodeError::InvalidData("bad chunk".into())));
    let loader = ImageLoader::new(images(), decoders([decoder]));
    let mut codec = block_on(loader.open(bytes(), DecodeOptions::default())).unwrap();
    assert!(block_on(codec.next_frame()).is_ok());
    assert!(matches!(
        block_on(codec.next_frame()),
        Err(DecodeError::InvalidData(_))
    ));
    assert!(block_on(codec.next_frame()).is_ok());
}

#[test]
fn max_size_is_passed_to_the_decoder_and_frames_come_back_at_that_size() {
    let mut decoder = FakeDecoder::new("software");
    decoder.size = [400, 200];
    let loader = ImageLoader::new(images(), decoders([decoder]));
    let options = DecodeOptions {
        max_size: Some([100, 100]),
        mipmaps: true,
        ..Default::default()
    };
    let mut codec = block_on(loader.open(bytes(), options)).unwrap();
    assert_eq!(codec.info().size, [100, 50]);
    let frame = block_on(codec.next_frame()).unwrap();
    assert_eq!(frame.image.size(), [100, 50]);
    assert_eq!(frame.image.mip_levels(), 7);
}

#[test]
fn limits_on_frames_and_pixels_apply_to_what_the_decoder_reports() {
    let loader = ImageLoader::new(
        images(),
        decoders([FakeDecoder::new("software").frames(5, Repetition::Forever)]),
    );
    let few_frames = DecodeOptions {
        limits: DecodeLimits {
            max_frames: 4,
            ..Default::default()
        },
        ..Default::default()
    };
    assert!(matches!(
        block_on(loader.open(bytes(), few_frames)),
        Err(DecodeError::LimitExceeded("animation frames"))
    ));
    let mut large = FakeDecoder::new("software");
    large.size = [1000, 1000];
    let loader = ImageLoader::new(images(), decoders([large]));
    let few_pixels = DecodeOptions {
        limits: DecodeLimits {
            max_pixels: 10,
            ..Default::default()
        },
        ..Default::default()
    };
    assert!(matches!(
        block_on(loader.decode(bytes(), few_pixels)),
        Err(DecodeError::LimitExceeded("decoded pixels"))
    ));
}

#[test]
fn a_decoder_that_answers_later_is_waited_for_rather_than_assumed_ready() {
    let (decoder, release) = DelayedDecoder::new("browser");
    let loader = ImageLoader::new(images(), vec![Box::new(decoder)]);
    let mut decoding = loader.decode(bytes(), DecodeOptions::default());
    assert!(
        decoding.try_take().is_none(),
        "nothing to take while the decoder is still working"
    );
    release.release();
    let image = decoding.try_take().expect("the answer arrived").unwrap();
    assert_eq!(image.size(), [1, 1]);
}