use abstracttui::base::Rect;
use abstracttui::gfx::{base64, decode_image, mosaic};
use abstracttui::term::Capabilities;
use abstracttui::widgets::Bitmap;
const PROGRESSIVE_JPEG_B64: &str = include_str!("fixtures/progressive_jpeg.b64");
const REFERENCE_PNG_B64: &str = include_str!("fixtures/progressive_jpeg_ref_png.b64");
fn fixture(b64: &str) -> Vec<u8> {
let stripped: String = b64.chars().filter(|c| !c.is_ascii_whitespace()).collect();
base64::decode(&stripped).expect("fixture is valid base64")
}
fn psnr(a: &Bitmap, b: &Bitmap) -> f64 {
assert_eq!(
(a.width(), a.height()),
(b.width(), b.height()),
"PSNR needs matching dimensions"
);
let mut se = 0f64;
let mut n = 0f64;
for y in 0..a.height() {
for x in 0..a.width() {
let (p, q) = (a.get(x, y).unwrap(), b.get(x, y).unwrap());
for (u, v) in [(p.r, q.r), (p.g, q.g), (p.b, q.b)] {
let d = u as f64 - v as f64;
se += d * d;
n += 1.0;
}
}
}
if se == 0.0 {
return f64::INFINITY;
}
10.0 * (255.0 * 255.0 / (se / n)).log10()
}
#[test]
fn progressive_fixture_carries_a_sof2_frame() {
let bytes = fixture(PROGRESSIVE_JPEG_B64);
assert_eq!(&bytes[..2], &[0xFF, 0xD8], "not a JPEG (missing SOI)");
let mut i = 2;
let mut sof = None;
while i + 4 <= bytes.len() && bytes[i] == 0xFF {
let marker = bytes[i + 1];
if (0xC0..=0xCF).contains(&marker) && !matches!(marker, 0xC4 | 0xC8 | 0xCC) {
sof = Some(marker);
break;
}
if marker == 0xDA {
break;
}
i += 2 + u16::from_be_bytes([bytes[i + 2], bytes[i + 3]]) as usize;
}
assert_eq!(
sof,
Some(0xC2),
"fixture must be a PROGRESSIVE JPEG (SOF2); 0xC0 is baseline and would \
make `progressive_jpeg_decodes_to_the_real_picture` vacuous"
);
}
#[test]
fn progressive_jpeg_decodes_to_the_real_picture() {
let decoded = decode_image(&fixture(PROGRESSIVE_JPEG_B64)).unwrap_or_else(|e| {
panic!(
"progressive JPEG must decode (abstracttui >= 0.3.5); the TUI prints \
this message in the image card where the picture belongs: {e}"
)
});
assert_eq!((decoded.width(), decoded.height()), (96, 64));
let reference = decode_image(&fixture(REFERENCE_PNG_B64)).expect("reference PNG decodes");
let db = psnr(&decoded, &reference);
assert!(
db >= 18.0,
"progressive decode is not the reference picture: {db:.1} dB PSNR"
);
}
#[test]
fn progressive_jpeg_reaches_the_transcript_as_painted_cells() {
let decoded = decode_image(&fixture(PROGRESSIVE_JPEG_B64)).expect("progressive JPEG decodes");
let bounded = abstractcode::runner::downscale_for_transcript(decoded);
assert!(bounded.width() <= 1024 && bounded.height() <= 168);
let cells =
mosaic::render_to_cells(&bounded, Rect::new(0, 0, 24, 14), &Capabilities::default());
assert_eq!(
cells.len(),
24 * 14,
"one patch per cell of the target rect"
);
let painted = cells.iter().filter(|c| c.ch != ' ').count();
assert!(
painted > 24 * 14 / 2,
"mosaic produced {painted} painted cells — the picture did not reach the feed"
);
}
#[test]
fn mosaic_density_follows_terminal_capabilities() {
let img = decode_image(&fixture(REFERENCE_PNG_B64)).expect("reference PNG decodes");
let rect = Rect::new(0, 0, 24, 14);
let conservative = Capabilities::default();
assert_eq!(
mosaic::MosaicMode::auto(&conservative).0,
mosaic::MosaicMode::HalfBlock
);
let mut modern = Capabilities::default();
modern.unicode_ok = true;
modern.truecolor = true;
assert_eq!(
mosaic::MosaicMode::auto(&modern).0,
mosaic::MosaicMode::Quadrant,
"a unicode + truecolor terminal must get 2x2 subpixels per cell"
);
let alphabet = |caps: &Capabilities| {
let mut cs: Vec<char> = mosaic::render_to_cells(&img, rect, caps)
.iter()
.map(|c| c.ch)
.collect();
cs.sort_unstable();
cs.dedup();
cs
};
let (half, quad) = (alphabet(&conservative), alphabet(&modern));
assert!(
quad.len() > half.len(),
"quadrant output ({quad:?}) is no richer than half-block output ({half:?})"
);
}