use icy_sixel::*;
#[test]
fn shared_palette_survives_between_images() {
let mut decoder = SixelDecoder::new();
decoder.decode_from_dcs(b"#42;2;100;0;0#42~", DcsSettings::default()).unwrap();
let image = decoder.decode_from_dcs(b"#42~", DcsSettings::default()).unwrap();
assert_eq!(&image.pixels[..4], &[255, 0, 0, 255]);
}
#[test]
fn decoders_do_not_share_palettes() {
let mut first = SixelDecoder::new();
let mut second = SixelDecoder::new();
first.decode_from_dcs(b"#42;2;100;0;0#42~", DcsSettings::default()).unwrap();
let image = second.decode_from_dcs(b"#42~", DcsSettings::default()).unwrap();
assert_ne!(&image.pixels[..4], &[255, 0, 0, 255]);
}
#[test]
fn reset_palette_restores_defaults() {
let mut decoder = SixelDecoder::new();
decoder.decode_from_dcs(b"#42;2;100;0;0#42~", DcsSettings::default()).unwrap();
decoder.reset_palette();
let image = decoder.decode_from_dcs(b"#42~", DcsSettings::default()).unwrap();
assert_ne!(&image.pixels[..4], &[255, 0, 0, 255]);
}
#[test]
fn failed_frame_does_not_mutate_shared_palette() {
let mut decoder = SixelDecoder::new();
decoder.decode_from_dcs(b"#42;2;100;0;0#42~", DcsSettings::default()).unwrap();
assert!(decoder.decode_from_dcs(b"#42;2;0;100;0!65536~", DcsSettings::default()).is_err());
let image = decoder.decode_from_dcs(b"#42~", DcsSettings::default()).unwrap();
assert_eq!(&image.pixels[..4], &[255, 0, 0, 255]);
}
#[test]
fn frame_state_is_reset_between_images() {
let mut decoder = SixelDecoder::new();
decoder
.decode_from_dcs(b"\"1;1;10;20!5~", DcsSettings::default().with_background_mode(BackgroundMode::Transparent))
.unwrap();
let image = decoder.decode_from_dcs(b"~", DcsSettings::default()).unwrap();
assert_eq!((image.width, image.height), (1, 6));
}
#[test]
fn unsized_growth_matches_pre_sized_decode() {
let width = 1500;
let mut unsized_data = Vec::from(*b"\x1bPq#1;2;100;0;0");
unsized_data.resize(unsized_data.len() + width, b'~');
unsized_data.extend_from_slice(b"\x1b\\");
let mut raster_data = Vec::from(*b"\x1bPq");
raster_data.extend_from_slice(format!("\"1;1;{};6", width).as_bytes());
raster_data.extend_from_slice(b"#1;2;100;0;0");
raster_data.resize(raster_data.len() + width, b'~');
raster_data.extend_from_slice(b"\x1b\\");
let grown = SixelImage::decode(&unsized_data).unwrap();
let pre_sized = SixelImage::decode(&raster_data).unwrap();
assert_eq!((grown.width, grown.height), (width, 6));
assert_eq!((grown.width, grown.height), (pre_sized.width, pre_sized.height));
assert_eq!(grown.pixels.len(), width * 6 * 4);
assert_eq!(grown.pixels, pre_sized.pixels);
assert!(grown.pixels.chunks_exact(4).all(|px| px == [255, 0, 0, 255]));
}
#[test]
fn partial_rows_survive_canvas_growth() {
let mut data = Vec::from(*b"\x1bPq#1;2;100;0;0#1!3~-#2;2;0;0;100#2!300~");
data.extend_from_slice(b"\x1b\\");
let image = SixelImage::decode(&data).unwrap();
assert_eq!((image.width, image.height), (300, 12));
let px = |x: usize, y: usize| &image.pixels[(y * image.width + x) * 4..(y * image.width + x) * 4 + 4];
assert_eq!(px(0, 0), &[255, 0, 0, 255], "first band keeps its red pixels");
assert_eq!(px(2, 5), &[255, 0, 0, 255], "first band spans all six rows");
assert_eq!(px(3, 0), &[0, 0, 0, 255], "area beyond the first band is background");
assert_eq!(px(299, 6), &[0, 0, 255, 255], "second band reaches the full width");
}
#[test]
fn reported_background_mode_matches_pixels() {
let image = SixelImage::decode(b"\x1bPq#1;2;100;0;0#1@\x1b\\").unwrap();
let undrawn = &image.pixels[image.width * 4..image.width * 4 + 4];
assert_eq!(image.background_mode, BackgroundMode::Opaque);
assert_eq!(undrawn[3], 255);
let image = SixelImage::decode(b"\x1bP0;1;0q#1;2;100;0;0#1@\x1b\\").unwrap();
let undrawn = &image.pixels[image.width * 4..image.width * 4 + 4];
assert_eq!(image.background_mode, BackgroundMode::Transparent);
assert_eq!(undrawn[3], 0);
let image = SixelImage::decode(b"\x1bP0;2;0q#1;2;100;0;0#1@\x1b\\").unwrap();
assert_eq!(image.background_mode, BackgroundMode::Opaque);
}
#[test]
fn test_decode_simple_sixel() {
let sixel_data = b"\x1bPq\"1;1;2;2#0;2;0;0;0#0~~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok(), "Decoding should succeed");
let image = result.unwrap();
let (pixels, width, height) = (image.pixels, image.width, image.height);
assert!(width > 0, "Width should be positive");
assert!(height > 0, "Height should be positive");
assert_eq!(pixels.len(), width * height * 4, "Pixel buffer size should match dimensions * 4 (RGBA)");
}
#[test]
fn test_decode_with_aspect_ratio() {
let sixel_data = b"\x1bP2q#0;2;100;0;0#0~~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok(), "Decoding should succeed");
let image = result.unwrap();
assert!(image.width > 0);
assert!(image.height >= 6);
}
#[test]
fn test_decode_with_colors() {
let sixel_data = b"\x1bPq#0;2;100;0;0#0~~@@~~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (pixels, width, height) = (image.pixels, image.width, image.height);
assert!(width > 0);
assert!(height >= 6);
assert!(pixels.len() >= 3);
}
#[test]
fn test_decode_multicolor() {
let sixel_data = b"\x1bPq#0;2;100;0;0#1;2;0;100;0#0~#1~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (_pixels, width, height) = (image.pixels, image.width, image.height);
assert!(width > 0);
assert!(height > 0);
}
#[test]
fn test_decode_with_repeat() {
let sixel_data = b"\x1bPq#0!5~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (_pixels, width, _height) = (image.pixels, image.width, image.height);
assert_eq!(width, 5, "Width should be 5 (repeat count)");
}
#[test]
fn test_decode_carriage_return() {
let sixel_data = b"\x1bPq#0~~$~~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (_pixels, width, _height) = (image.pixels, image.width, image.height);
assert_eq!(width, 2, "Width should be 2");
}
#[test]
fn test_decode_color_overlay_preserves_previous_pixels() {
let sixel_data = b"\x1bPq#2~$#3_\x1b\\";
let image = SixelImage::decode(sixel_data).expect("Decoding overlay should work");
let (pixels, width, height) = (image.pixels, image.width, image.height);
assert_eq!(width, 1, "Overlay sample should be one column wide");
assert!(height >= 6, "Single sixel column must span six pixels vertically");
let stride = width * 4;
let top = &pixels[0..4];
let bottom = &pixels[(height - 1) * stride..(height - 1) * stride + 4];
assert_eq!(top, &[204, 33, 33, 255], "Top rows must keep the red color from the first pass");
assert_eq!(bottom, &[51, 204, 51, 255], "Bottom row must reflect the green overlay");
}
#[test]
fn test_decode_newline() {
let sixel_data = b"\x1bPq#0~~-~~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let height = image.height;
assert!(height >= 12, "Height should be at least 12 (two sixel rows)");
}
#[test]
fn test_decode_hls_color() {
let sixel_data = b"\x1bPq#0;1;120;50;100#0~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let pixels = image.pixels;
assert!(pixels.len() >= 3);
}
#[test]
fn test_decode_rgb_color() {
let sixel_data = b"\x1bPq#0;2;100;50;0#0~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let pixels = image.pixels;
assert!(pixels.len() >= 4);
let r = pixels[0];
let g = pixels[1];
let b = pixels[2];
let a = pixels[3];
assert!(r > 200, "Red should be high");
assert!(g > 100 && g < 150, "Green should be medium");
assert!(b < 50, "Blue should be low");
assert_eq!(a, 255, "Alpha should be 255");
}
#[test]
fn test_decode_raster_attributes() {
let sixel_data = b"\x1bPq\"1;1;10;20#0~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (_pixels, width, height) = (image.pixels, image.width, image.height);
assert!(width >= 10, "Width should be at least 10");
assert!(height >= 20, "Height should be at least 20");
}
#[test]
fn test_decode_empty() {
let sixel_data = b"\x1bPq\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (_pixels, width, height) = (image.pixels, image.width, image.height);
assert!(width > 0);
assert!(height > 0);
}
#[test]
fn test_decode_all_sixel_chars() {
let sixel_data = b"\x1bPq#0?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (_pixels, width, _height) = (image.pixels, image.width, image.height);
assert!(width > 60, "Should have decoded many characters");
}
#[test]
fn test_decode_roundtrip_simple() {
let original_pixels = vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, ];
let image = SixelImage::from_rgba(original_pixels.clone(), 2, 2);
let encoded = image.encode();
assert!(encoded.is_ok());
let sixel_str = encoded.unwrap();
let decoded = SixelImage::decode(sixel_str.as_bytes());
assert!(decoded.is_ok());
let image = decoded.unwrap();
let (pixels, width, height) = (image.pixels, image.width, image.height);
assert_eq!(width, 2, "Width should match");
assert!(height >= 2, "Height should be at least 2");
assert_eq!(pixels.len(), width * height * 4);
}
#[test]
fn test_decode_vertical_patterns() {
let sixel_data = b"\x1bPq#0?@A~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (_pixels, width, height) = (image.pixels, image.width, image.height);
assert_eq!(width, 4);
assert!(height >= 6);
}
#[test]
fn test_decode_large_repeat() {
let sixel_data = b"\x1bPq#0!100~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (_pixels, width, _height) = (image.pixels, image.width, image.height);
assert_eq!(width, 100);
}
#[test]
fn test_decode_palette_bounds() {
let sixel_data = b"\x1bPq#255;2;50;50;50#255~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let _ = result.unwrap();
}
#[test]
fn test_decode_escape_sequences() {
let sixel_data = b"\x1bP0;0;0q#0~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let sixel_data = b"\x90q#0~\x9c";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
}
#[test]
fn test_decode_rgb() {
let sixel_data = b"\x1bPq#2;2;100;0;0~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (pixels, width, height) = (image.pixels, image.width, image.height);
assert_eq!(width, 1);
assert_eq!(height, 6);
assert_eq!(pixels[0], 255); assert_eq!(pixels[1], 0); assert_eq!(pixels[2], 0); assert_eq!(pixels[3], 255); }
#[test]
fn test_decode_color_redefinition() {
let sixel_data = b"\x1bPq\
#0;2;100;0;0~$-\
#0;2;0;100;0~$-\
#0;2;0;0;100~\
\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (pixels, width, height) = (image.pixels, image.width, image.height);
assert_eq!(width, 1);
assert_eq!(height, 18);
assert_eq!(pixels[0], 255); assert_eq!(pixels[1], 0); assert_eq!(pixels[2], 0); assert_eq!(pixels[3], 255);
let offset = width * 6 * 4; assert_eq!(pixels[offset], 0); assert_eq!(pixels[offset + 1], 255); assert_eq!(pixels[offset + 2], 0); assert_eq!(pixels[offset + 3], 255);
let offset = width * 12 * 4; assert_eq!(pixels[offset], 0); assert_eq!(pixels[offset + 1], 0); assert_eq!(pixels[offset + 2], 255); assert_eq!(pixels[offset + 3], 255); }
#[test]
fn test_decode_rgb_output() {
let sixel_data = b"\x1bPq#1;2;50;50;0#2;2;0;50;50#1~#2~\x1b\\";
let result = SixelImage::decode(sixel_data);
assert!(result.is_ok());
let image = result.unwrap();
let (pixels, width, height) = (image.pixels, image.width, image.height);
assert_eq!(width, 2);
assert_eq!(height, 6);
assert_eq!(pixels.len(), width * height * 4); }
fn compare_images(original: &[u8], decoded: &[u8], width: usize, height: usize) -> (f64, u8) {
let compare_len = (width * height * 4).min(original.len()).min(decoded.len());
let mut total_diff: u64 = 0;
let mut max_diff: u8 = 0;
for i in 0..compare_len {
if i % 4 == 3 {
continue;
}
let diff = (original[i] as i32 - decoded[i] as i32).unsigned_abs().min(255) as u8;
total_diff += diff as u64;
max_diff = max_diff.max(diff);
}
let rgb_count = (compare_len / 4) * 3;
let avg_diff = if rgb_count > 0 { total_diff as f64 / rgb_count as f64 } else { 0.0 };
(avg_diff, max_diff)
}
#[test]
fn test_roundtrip_test_page_png() {
let img = image::open("tests/data/test_page.png").expect("Failed to open test_page.png");
let rgba_img = img.to_rgba8();
let (width, height) = rgba_img.dimensions();
let original_pixels = rgba_img.into_raw();
let image = SixelImage::from_rgba(original_pixels.clone(), width as usize, height as usize);
let sixel = image.encode().expect("Failed to encode test_page.png");
assert!(!sixel.is_empty(), "SIXEL output should not be empty");
assert!(sixel.starts_with("\x1bP9;1;0q"), "SIXEL should start with DCS introducer");
assert!(sixel.ends_with("\x1b\\"), "SIXEL should end with string terminator");
let decoded = SixelImage::decode(sixel.as_bytes()).expect("Failed to decode test_page.png sixel");
let (decoded_pixels, decoded_width, decoded_height) = (decoded.pixels, decoded.width, decoded.height);
assert_eq!(decoded_width, width as usize, "Width should match");
assert!(decoded_height >= height as usize, "Decoded height should be >= original");
let (avg_diff, max_diff) = compare_images(&original_pixels, &decoded_pixels, width as usize, height as usize);
println!("test_page.png roundtrip: avg_diff={:.2}, max_diff={}", avg_diff, max_diff);
assert!(avg_diff < 15.0, "Average pixel difference should be < 15, got {:.2}", avg_diff);
}
#[test]
fn test_roundtrip_transparency_png() {
let img = image::open("tests/data/transparency.png").expect("Failed to open transparency.png");
let rgba_img = img.to_rgba8();
let (width, height) = rgba_img.dimensions();
let original_pixels = rgba_img.into_raw();
let image = SixelImage::from_rgba(original_pixels.clone(), width as usize, height as usize);
let sixel = image.encode().expect("Failed to encode transparency.png");
assert!(!sixel.is_empty(), "SIXEL output should not be empty");
assert!(sixel.starts_with("\x1bP"), "SIXEL should start with DCS introducer");
assert!(sixel.contains('q'), "SIXEL should contain 'q' command");
assert!(sixel.ends_with("\x1b\\"), "SIXEL should end with string terminator");
let decoded_image = SixelImage::decode(sixel.as_bytes()).expect("Failed to decode transparency.png sixel");
let (decoded_pixels, decoded_width, decoded_height) = (decoded_image.pixels, decoded_image.width, decoded_image.height);
assert_eq!(decoded_width, width as usize, "Width should match");
println!("transparency.png: original {}x{}, decoded {}x{}", width, height, decoded_width, decoded_height);
let mut total_diff: u64 = 0;
let mut max_diff: u8 = 0;
let mut opaque_pixel_count = 0u64;
let mut transparent_match_count = 0u64;
let compare_height = height.min(decoded_height as u32);
for y in 0..compare_height {
for x in 0..width {
let orig_idx = ((y * width + x) * 4) as usize;
let dec_idx = ((y * decoded_width as u32 + x) * 4) as usize;
let orig_alpha = original_pixels[orig_idx + 3];
let dec_alpha = decoded_pixels[dec_idx + 3];
if orig_alpha >= 128 {
opaque_pixel_count += 1;
for c in 0..3 {
let diff = (original_pixels[orig_idx + c] as i32 - decoded_pixels[dec_idx + c] as i32)
.unsigned_abs()
.min(255) as u8;
total_diff += diff as u64;
max_diff = max_diff.max(diff);
}
} else {
if dec_alpha < 128 {
transparent_match_count += 1;
}
}
}
}
let avg_diff = if opaque_pixel_count > 0 {
total_diff as f64 / (opaque_pixel_count * 3) as f64
} else {
0.0
};
println!(
"transparency.png roundtrip: avg_diff={:.2}, max_diff={}, opaque_pixels={}, transparent_matches={}",
avg_diff, max_diff, opaque_pixel_count, transparent_match_count
);
assert!(avg_diff < 15.0, "Average pixel difference should be < 15, got {:.2}", avg_diff);
assert!(opaque_pixel_count > 0, "Should have some opaque pixels to compare");
}
#[test]
fn test_encode_beelitz_heilstaetten_png() {
let img = image::open("tests/data/beelitz_heilstätten.png").expect("Failed to open beelitz_heilstätten.png");
let rgba_img = img.to_rgba8();
let (width, height) = rgba_img.dimensions();
let original_pixels = rgba_img.into_raw();
println!("beelitz_heilstätten.png: {}x{}", width, height);
let image = SixelImage::from_rgba(original_pixels, width as usize, height as usize);
let sixel = image.encode().expect("Failed to encode beelitz_heilstätten.png");
assert!(!sixel.is_empty(), "SIXEL output should not be empty");
assert!(sixel.starts_with("\x1bP9;1;0q"), "SIXEL should start with DCS introducer");
assert!(sixel.ends_with("\x1b\\"), "SIXEL should end with string terminator");
println!("beelitz_heilstätten.png encoded to {} bytes of SIXEL", sixel.len());
let result = SixelImage::decode(sixel.as_bytes());
assert!(result.is_ok(), "Encoded SIXEL should be decodable");
let decoded_image = result.unwrap();
let (_, decoded_width, decoded_height) = (decoded_image.pixels, decoded_image.width, decoded_image.height);
assert_eq!(decoded_width, width as usize, "Decoded width should match");
assert!(decoded_height >= height as usize, "Decoded height should be >= original");
}