use super::quantize::{Dither, Indexed, PaletteOptions, Reduction, quantize, quantize_locked}; use super::web::{ColorSort, gif_comment, jpeg_comment, lossy, snap, sort, to_transparent}; use super::*; /// A `w`×`h` opaque image of `f(x, y)` colours. fn image(w: u32, h: u32, f: impl Fn(u32, u32) -> [u8; 3]) -> Vec { (0..h).flat_map(|y| (0..w).map(move |x| (x, y))).flat_map(|(x, y)| [f(x, y)[0], f(x, y)[1], f(x, y)[2], 255]).collect() } fn colours(ix: &Indexed) -> Vec<[u8; 3]> { ix.indices.iter().map(|&i| ix.palette[i as usize]).collect() } #[test] fn locked_colours_survive_a_reduction() { let px = image(64, 16, |x, y| [(x * 4) as u8, (y * 16) as u8, 90]); let rare = [7, 200, 13]; let mut px = px; px[..4].copy_from_slice(&[rare[0], rare[1], rare[2], 255]); let o = PaletteOptions { colors: 4, dither: Dither::None, reduction: Reduction::Adaptive, ..Default::default() }; assert!(!quantize(&px, 64, 16, &o).palette.contains(&rare), "a lone pixel loses its colour"); let ix = quantize_locked(&px, 64, 16, &o, &[rare]); assert!(ix.palette.contains(&rare) && ix.palette.len() <= 4, "{:?}", ix.palette); assert_eq!(colours(&ix)[0], rare); } #[test] fn snapping_to_web_colours() { assert_eq!(snap([50, 103, 0], 0), None); assert_eq!(snap([50, 103, 0], 10), Some([51, 102, 0])); assert_eq!(snap([60, 103, 0], 10), None, "9 levels off is beyond 10%"); assert_eq!(snap([60, 103, 0], 100), Some([51, 102, 0])); assert_eq!(snap([25, 25, 25], 100), Some([0, 0, 0])); } #[test] fn mapped_colours_become_the_transparent_entry() { let px = image(4, 1, |x, _| [[255, 0, 0], [0, 0, 255], [0, 255, 0], [255, 0, 0]][x as usize]); let mut ix = quantize(&px, 4, 1, &PaletteOptions::default()); assert_eq!(ix.transparent, None); let red = ix.palette.iter().position(|c| *c == [255, 0, 0]).unwrap(); let map = to_transparent(&mut ix, |i| i == red); assert_eq!(map[red], None); assert_eq!(ix.transparent, Some(0)); assert_eq!(ix.palette.len(), 3, "the transparent entry and blue and green"); assert_eq!((ix.indices[0], ix.indices[3]), (0, 0)); assert_eq!(ix.palette[ix.indices[1] as usize], [0, 0, 255]); // Mapping nothing changes nothing. let before = ix.clone(); to_transparent(&mut ix, |_| false); assert_eq!(ix, before); } #[test] fn sorting_keeps_every_pixel_colour() { let px: Vec = image(8, 8, |x, y| [(x * 30) as u8, 255 - (y * 30) as u8, ((x + y) * 15) as u8]) .chunks(4) .enumerate() .flat_map(|(i, p)| [p[0], p[1], p[2], if i == 5 { 0 } else { 255 }]) .collect(); let ix = quantize(&px, 8, 8, &PaletteOptions { colors: 16, ..Default::default() }); for order in ColorSort::ALL { let mut s = ix.clone(); let new = sort(&mut s, order); assert_eq!(s.transparent, Some(0), "{order:?}: the transparent entry stays first"); assert_eq!(new[0], 0); for (i, (&a, &b)) in ix.indices.iter().zip(&s.indices).enumerate() { assert_eq!(ix.palette[a as usize], s.palette[b as usize], "{order:?} pixel {i}"); } } let mut s = ix.clone(); sort(&mut s, ColorSort::Luminance); let luma = |c: &[u8; 3]| 299 * c[0] as u32 + 587 * c[1] as u32 + 114 * c[2] as u32; assert!(s.palette[1..].windows(2).all(|w| luma(&w[0]) <= luma(&w[1]))); assert_eq!(ColorSort::from_id("popularity"), Some(ColorSort::Popularity)); } #[test] fn lossy_makes_runs_and_smaller_gifs() { let px = image(96, 64, |x, y| [(x * 2 + y) as u8, (y * 3) as u8, ((x * y) % 255) as u8]); let ix = quantize(&px, 96, 64, &PaletteOptions { colors: 64, dither: Dither::Diffusion, ..Default::default() }); let mut lossy_ix = ix.clone(); lossy(&mut lossy_ix, &px, 80); let runs = |ix: &Indexed| ix.indices.windows(2).filter(|w| w[0] != w[1]).count(); assert!(runs(&lossy_ix) < runs(&ix)); let size = |ix: &Indexed| gif::encode(ix, false).unwrap().len(); assert!(size(&lossy_ix) < size(&ix), "{} vs {}", size(&lossy_ix), size(&ix)); let mut none = ix.clone(); lossy(&mut none, &px, 0); assert_eq!(none, ix); } #[test] fn comments_are_valid_files() { let ix = quantize(&image(5, 4, |x, _| [x as u8 * 40, 0, 0]), 5, 4, &PaletteOptions::default()); let file = gif_comment(gif::encode(&ix, false).unwrap(), &"Copyright: Someone ".repeat(20)); assert!(file.windows(2).any(|w| w == [0x21, 0xFE])); assert_eq!(image::load_from_memory(&file).unwrap().width(), 5); let px = vec![200u8; 5 * 4 * 3]; let jpg = jpeg::encode(&px, 5, 4, 80, Some(72.0), &jpeg::JpegOptions::default()).unwrap(); let with = jpeg_comment(jpg.clone(), "Copyright: Someone"); assert_eq!(with.len(), jpg.len() + 4 + 18); assert!(with.windows(2).any(|w| w == [0xFF, 0xFE])); assert_eq!(image::load_from_memory(&with).unwrap().height(), 4); assert_eq!(jpeg_comment(vec![1, 2, 3], "x"), vec![1, 2, 3], "not a JPEG"); let png = png::with_srgb(png::encode(&[0, 0, 0, 255], 1, 1, &png::PngOptions::default()).unwrap()); assert!(png.windows(4).any(|w| w == b"sRGB")); assert_eq!(image::load_from_memory(&png).unwrap().width(), 1); }