use icy_sixel::*;
#[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"
);
}