use crabshell_ui::Fit;
use tiny_skia::Pixmap;
use super::fixture;
use crate::images::{rasterize, scale};
const RED: [u8; 4] = [255, 0, 0, 255];
const GREEN: [u8; 4] = [0, 255, 0, 255];
const BLUE: [u8; 4] = [0, 0, 255, 255];
const CLEAR: [u8; 4] = [0, 0, 0, 0];
fn load(name: &str, width: u32, height: u32, fit: Fit) -> Pixmap {
rasterize(&fixture(name), width, height, fit).unwrap_or_else(|error| panic!("{name}: {error}"))
}
fn assert_pixel(pixmap: &Pixmap, (x, y): (u32, u32), expected: [u8; 4], what: &str) {
let pixel = pixmap.pixel(x, y).expect("the pixel is inside");
let got = [pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()];
assert!(
got.iter().zip(expected).all(|(a, b)| a.abs_diff(b) <= 8),
"{what}: pixel {x},{y} is {got:?}, not {expected:?}"
);
}
#[test]
fn every_format_decodes_to_the_same_picture() {
for name in ["stripes.png", "stripes.jpg", "stripes.gif", "stripes.webp"] {
let image = load(name, 40, 20, Fit::Fill);
assert_pixel(&image, (4, 10), RED, name);
assert_pixel(&image, (20, 10), GREEN, name);
assert_pixel(&image, (35, 10), BLUE, name);
}
}
#[test]
fn fill_stretches_contain_letterboxes_and_cover_crops() {
let fill = load("stripes.png", 20, 20, Fit::Fill);
assert_pixel(&fill, (1, 10), RED, "fill");
assert_pixel(&fill, (10, 0), GREEN, "fill");
assert_pixel(&fill, (18, 19), BLUE, "fill");
let contain = load("stripes.png", 20, 20, Fit::Contain);
assert_pixel(&contain, (10, 2), CLEAR, "contain");
assert_pixel(&contain, (10, 17), CLEAR, "contain");
assert_pixel(&contain, (1, 10), RED, "contain");
assert_pixel(&contain, (10, 10), GREEN, "contain");
let cover = load("stripes.png", 20, 20, Fit::Cover);
for x in [0, 10, 19] {
assert_pixel(&cover, (x, 10), GREEN, "cover");
}
}
#[test]
fn a_large_picture_shrinks_smoothly() {
let small = load("stripes.png", 4, 2, Fit::Fill);
let channels = |x, y| {
let pixel = small.pixel(x, y).expect("the pixel is inside");
[pixel.red(), pixel.green(), pixel.blue()]
};
let [red, _, blue] = channels(0, 1);
assert!(red > 200 && blue < 50, "left {:?}", channels(0, 1));
let [red, _, blue] = channels(3, 0);
assert!(blue > 200 && red < 50, "right {:?}", channels(3, 0));
let [_, green, _] = channels(1, 1);
assert!(green > 200, "middle {:?}", channels(1, 1));
}
#[test]
fn a_gif_frame_covering_part_of_its_canvas_lands_where_it_says() {
let image = load("partial.gif", 40, 20, Fit::Fill);
assert_pixel(&image, (10, 10), RED, "partial");
assert_pixel(&image, (2, 2), CLEAR, "partial");
assert_pixel(&image, (30, 10), CLEAR, "partial");
}
#[test]
fn an_svg_is_drawn_at_the_size_asked() {
let image = load("shapes.svg", 40, 20, Fit::Contain);
assert_eq!((image.width(), image.height()), (40, 20));
assert!(image.pixels().iter().any(|pixel| pixel.alpha() > 0));
}
#[test]
fn oversized_missing_and_unknown_files_are_refused() {
for name in [
"too-wide.png",
"too-wide.jpg",
"too-wide.gif",
"too-wide.webp",
] {
let error = rasterize(&fixture(name), 10, 10, Fit::Fill).expect_err(name);
assert!(error.contains("9000"), "{name}: {error}");
}
for name in [
"too-many.png",
"too-many.jpg",
"too-many.gif",
"too-many.webp",
] {
let error = rasterize(&fixture(name), 10, 10, Fit::Fill).expect_err(name);
assert!(error.contains("5000x5000"), "{name}: {error}");
}
assert!(rasterize(&fixture("missing.png"), 10, 10, Fit::Fill).is_err());
let error = rasterize(&fixture("shapes.bmp"), 10, 10, Fit::Fill).expect_err("bmp");
assert!(error.contains(".png, .jpg"), "{error}");
assert!(rasterize(&fixture("stripes.png"), 0, 10, Fit::Fill).is_err());
}
#[test]
fn fine_detail_averages_out_instead_of_aliasing() {
let mut columns = Pixmap::new(64, 4).expect("a pixmap");
for (index, pixel) in columns
.data_mut()
.as_chunks_mut::<4>()
.0
.iter_mut()
.enumerate()
{
let value = if index % 2 == 0 { 0 } else { 255 };
*pixel = [value, value, value, 255];
}
let small = scale(columns.as_ref(), 3, 1, Fit::Fill).expect("scaled");
for x in 0..3 {
let gray = small.pixel(x, 0).expect("inside").red();
assert!((100..=156).contains(&gray), "pixel {x} is {gray}, not gray");
}
}
#[test]
fn a_gif_frame_with_no_pixels_is_refused() {
let mut gif = b"GIF89a\x04\x00\x04\x00\x80\x00\x00".to_vec();
gif.extend_from_slice(&[0, 0, 0, 255, 255, 255]);
gif.extend_from_slice(&[0x2c, 0, 0, 0, 0, 0, 0, 0, 0, 0x40]);
gif.extend_from_slice(&[2, 2, 0x44, 0x01, 0, 0x3b]);
assert!(crate::images::gif(&gif).is_err());
}
#[test]
fn an_image_past_the_window_edge_is_cut_not_squashed() {
let mut scene = super::Scene::new();
let panel = scene.panel().child(
crabshell_ui::Row::new()
.align(crabshell_ui::Align::Center)
.child(
crabshell_ui::Image::new(fixture("bands.png"))
.width(20)
.height(20),
),
);
let window = scene.window(panel, 40.0, 10.0);
let buffer = scene.full(&window, 1.0);
let pixel = |x: u32, y: u32| {
let at = ((y * buffer.width + x) * 4) as usize;
[buffer.pixels[at], buffer.pixels[at + 2]]
};
assert_eq!(pixel(5, 4), [255, 0], "row 4 is red");
assert_eq!(pixel(5, 5), [0, 255], "row 5 is blue");
}
fn icons() -> crate::icons::Icons {
crate::icons::Icons::with_themes(crate::theme::Themes::new(Vec::new()))
}
fn scratch(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("crabshell-images-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("a scratch directory");
dir
}
#[test]
fn a_file_rewritten_or_appearing_later_is_read_again() {
let dir = scratch("rewritten");
let path = dir.join("cover.png");
let mut icons = icons();
let top_left = |icons: &mut crate::icons::Icons| {
icons
.image(&path, 20, 20, Fit::Fill)
.and_then(|pixmap| pixmap.pixel(0, 0))
.map(|pixel| [pixel.red(), pixel.blue()])
};
assert_eq!(top_left(&mut icons), None);
std::fs::copy(fixture("stripes.png"), &path).expect("copy");
assert_eq!(top_left(&mut icons), Some([255, 0]));
std::fs::copy(fixture("bands.png"), &path).expect("copy");
let later = std::time::SystemTime::now() + std::time::Duration::from_secs(5);
std::fs::File::options()
.write(true)
.open(&path)
.and_then(|file| file.set_modified(later))
.expect("set the time");
let pixel = top_left(&mut icons).expect("drawn");
assert_eq!(pixel, [255, 0]);
let bottom = icons
.image(&path, 20, 20, Fit::Fill)
.and_then(|pixmap| pixmap.pixel(0, 19))
.map(|pixel| pixel.blue());
assert_eq!(bottom, Some(255), "the new picture, blue below");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn cached_pictures_past_the_byte_budget_are_dropped_once_unused() {
let mut icons = icons();
let path = fixture("stripes.png");
for side in [2000, 2001, 2002] {
assert!(icons.image(&path, side, 2000, Fit::Fill).is_some());
}
assert_eq!(icons.cached(), 3);
for _ in 0..20 {
icons.image(&path, 2002, 2000, Fit::Fill);
icons.end_frame();
}
assert_eq!(icons.cached(), 1);
}