use super::paint::Canvas;
const CHUNK: usize = 4096;
pub const ID: u32 = 1_976_622;
pub struct Image;
impl Image {
#[must_use]
pub fn shown(canvas: &Canvas, at: (u16, u16), cells: (u16, u16)) -> Vec<u8> {
let payload = base64(&canvas.rgb);
let mut out = Vec::with_capacity(payload.len() + payload.len() / CHUNK * 32 + 64);
out.extend_from_slice(b"\x1b7");
out.extend_from_slice(format!("\x1b[{};{}H", at.0, at.1).as_bytes());
out.extend_from_slice(&Self::gone());
let chunks = payload.as_bytes().chunks(CHUNK);
let last = chunks.len().saturating_sub(1);
for (nth, chunk) in chunks.enumerate() {
out.extend_from_slice(b"\x1b_G");
if nth == 0 {
out.extend_from_slice(
format!(
"a=T,q=2,C=1,i={ID},f=24,s={},v={},c={},r={},",
canvas.width, canvas.height, cells.0, cells.1
)
.as_bytes(),
);
}
out.extend_from_slice(if nth == last { b"m=0;" } else { b"m=1;" });
out.extend_from_slice(chunk);
out.extend_from_slice(b"\x1b\\");
}
out.extend_from_slice(b"\x1b8");
out
}
#[must_use]
pub fn gone() -> Vec<u8> {
format!("\x1b_Ga=d,d=I,i={ID},q=2\x1b\\").into_bytes()
}
}
fn base64(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for group in bytes.chunks(3) {
let packed = u32::from(group[0]) << 16
| u32::from(group.get(1).copied().unwrap_or(0)) << 8
| u32::from(group.get(2).copied().unwrap_or(0));
for shift in [18, 12, 6, 0] {
out.push(char::from(ALPHABET[((packed >> shift) & 0x3f) as usize]));
}
let missing = 3 - group.len();
out.truncate(out.len() - missing);
for _ in 0..missing {
out.push('=');
}
}
out
}
#[cfg(test)]
mod tests {
use super::{CHUNK, ID, Image, base64};
use crate::tui::treemap::paint::Canvas;
fn said(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).unwrap()
}
#[test]
fn base64_agrees_with_the_standard_on_every_length_of_tail() {
assert_eq!(base64(b""), "");
assert_eq!(base64(b"f"), "Zg==");
assert_eq!(base64(b"fo"), "Zm8=");
assert_eq!(base64(b"foo"), "Zm9v");
assert_eq!(base64(b"foob"), "Zm9vYg==");
assert_eq!(base64(b"fooba"), "Zm9vYmE=");
assert_eq!(base64(b"foobar"), "Zm9vYmFy");
assert_eq!(base64(&[0x00, 0xff, 0x1a]), "AP8a");
}
#[test]
fn one_placement_says_what_the_bytes_are_where_they_go_and_to_keep_quiet() {
let canvas = Canvas::new(2, 2);
let out = said(&Image::shown(&canvas, (3, 41), (20, 10)));
assert!(out.starts_with("\x1b7\x1b[3;41H"), "{out:?}");
assert!(
out.ends_with("\x1b8"),
"the cursor was not put back: {out:?}"
);
assert!(out.contains("f=24,s=2,v=2,c=20,r=10,"), "{out:?}");
assert!(
out.contains("q=2"),
"the terminal was not told to keep quiet"
);
assert!(
out.contains("C=1"),
"the cursor would be left after the image"
);
assert!(out.contains(&format!("i={ID}")), "{out:?}");
assert!(out.contains("m=0;"), "no chunk was marked as the last");
}
#[test]
fn a_payload_too_big_for_one_chunk_is_split_and_only_the_last_says_so() {
let canvas = Canvas::new(64, 64);
let out = said(&Image::shown(&canvas, (1, 1), (8, 4)));
assert!(
out.matches("m=1;").count() >= 3,
"not chunked: {}",
out.len()
);
assert_eq!(out.matches("m=0;").count(), 1, "more than one last chunk");
let mut counted = 0;
for chunk in out.split("\x1b_G").skip(1) {
let Some((_, rest)) = chunk.split_once(';') else {
continue;
};
let payload = rest.split_once('\x1b').unwrap().0;
assert!(payload.len() <= CHUNK, "a chunk of {}", payload.len());
counted += 1;
}
assert!(counted >= 4, "only {counted} chunks");
assert_eq!(out.matches("a=T").count(), 1);
}
#[test]
fn the_image_can_be_taken_back_with_its_pixels() {
let gone = said(&Image::gone());
assert!(gone.contains("d=I"), "{gone}");
assert!(gone.contains(&format!("i={ID}")), "{gone}");
assert!(gone.contains("q=2"), "{gone}");
let shown = said(&Image::shown(&Canvas::new(2, 2), (1, 1), (1, 1)));
assert!(
shown.contains(&gone),
"a placement left the last one behind"
);
}
}