#![cfg(not(miri))]
use std::{fs, io::Cursor, path::PathBuf};
use image::{AnimationDecoder, codecs::gif::GifDecoder};
use maple_render_core::{
GifAnim,
error::Result,
input::{Input, Inputs},
render::RenderQuality,
renders::Renders,
repository::Repository,
};
fn here() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn template(name: &str) -> PathBuf {
here().join("..").join("..").join("templates").join(format!("{name}.zip"))
}
fn example(name: &str) -> PathBuf {
here().join("..").join("..").join("examples").join(name)
}
fn load_repository(template_name: &str) -> Result<Repository> {
let bytes = fs::read(template(template_name))?;
Repository::load_from_bytes(bytes)
}
fn build_renders(template_name: &str, input: &str) -> Result<Renders> {
let repo = load_repository(template_name)?;
let mut inputs = Inputs::new();
inputs.push(Input::load(example(input))?);
Ok(Renders::new(repo, inputs, RenderQuality::Sampled))
}
fn decode_gif(data: &[u8]) -> Vec<image::Frame> {
GifDecoder::new(Cursor::new(data))
.expect("decode GIF")
.into_frames()
.collect_frames()
.expect("decode frames")
}
#[test]
fn gif_defaults_use_template_palette_and_timing() {
if !template("billboard-cityscape").is_file() || !example("frog.jpg").is_file() {
return;
}
let renders = build_renders("billboard-cityscape", "frog.jpg").expect("renders");
let mut gif = GifAnim::new(renders);
gif.apply().expect("gif apply");
let default_bytes = gif.encode().expect("gif encode");
let renders = build_renders("billboard-cityscape", "frog.jpg").expect("renders");
let mut gif = GifAnim::new(renders);
let repo = load_repository("billboard-cityscape").expect("repo");
gif.set_palette_frames(repo.get_palette());
gif.set_timing(repo.get_period(), repo.get_hold());
gif.apply().expect("gif apply");
let explicit_bytes = gif.encode().expect("gif encode");
let default_frames = decode_gif(&default_bytes);
let explicit_frames = decode_gif(&explicit_bytes);
assert!(
default_frames.len() > 20,
"default GIF must be animated, got {} frames",
default_frames.len()
);
assert_eq!(
default_frames.len(),
explicit_frames.len(),
"default and explicit GIFs must have the same frame count"
);
assert_eq!(
default_bytes, explicit_bytes,
"default GIF must be identical to explicitly-configured GIF"
);
let mut distinct = std::collections::HashSet::new();
for frame in default_frames.iter().skip(4).take(8) {
for p in frame.buffer().pixels() {
distinct.insert((p[0], p[1], p[2], p[3]));
if distinct.len() > 64 {
break;
}
}
if distinct.len() > 64 {
break;
}
}
assert!(distinct.len() > 8, "GIF frames must contain actual content");
}
#[test]
fn gif_explicit_overrides_take_precedence() {
if !template("billboard-cityscape").is_file() || !example("frog.jpg").is_file() {
return;
}
let renders = build_renders("billboard-cityscape", "frog.jpg").expect("renders");
let mut gif = GifAnim::new(renders);
gif.set_palette_frames(vec![0]);
gif.set_timing(0.1, 5.0);
gif.apply().expect("gif apply");
let frames = decode_gif(&gif.encode().expect("gif encode"));
assert_eq!(frames.len(), 1, "frame-0 palette override must be honored");
assert_eq!(frames[0].delay().numer_denom_ms(), (9_100, 1), "timing override must be honored");
}
#[test]
fn gif_defaults_still_encode_single_palette_templates() {
if !template("toaster").is_file() || !example("frog.jpg").is_file() {
return;
}
let renders = build_renders("toaster", "frog.jpg").expect("renders");
let mut gif = GifAnim::new(renders);
gif.apply().expect("gif apply");
let bytes = gif.encode().expect("gif encode");
let frames = decode_gif(&bytes);
assert!(frames.len() > 5, "toaster GIF must be animated, got {} frames", frames.len());
assert!(bytes.len() > 100_000, "toaster GIF must carry content, got {} bytes", bytes.len());
}