#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ImageDimensions {
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone)]
pub struct ImageOptions {
pub id: Option<u32>,
pub width_cells: Option<u16>,
pub height_cells: Option<u16>,
pub x: u16,
pub y: u16,
pub place: bool,
}
impl Default for ImageOptions {
fn default() -> Self {
ImageOptions {
id: None,
width_cells: None,
height_cells: None,
x: 0,
y: 0,
place: true,
}
}
}
pub fn detect_dimensions(data: &[u8]) -> Option<ImageDimensions> {
if data.len() < 10 {
return None;
}
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) && data.len() >= 24 {
let width = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
let height = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
return Some(ImageDimensions { width, height });
}
if data[0] == 0xFF && data[1] == 0xD8 {
let mut i = 2;
while i + 9 <= data.len() {
if data[i] == 0xFF {
let marker = data[i + 1];
if marker == 0xC0 || marker == 0xC2 {
let height = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
let width = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32;
return Some(ImageDimensions { width, height });
}
if i + 3 <= data.len() && marker != 0x00 && marker != 0xFF {
let seg_len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
i += 2 + seg_len;
} else {
i += 2;
}
} else {
i += 1;
}
}
}
if (data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a")) && data.len() >= 10 {
let width = u16::from_le_bytes([data[6], data[7]]) as u32;
let height = u16::from_le_bytes([data[8], data[9]]) as u32;
return Some(ImageDimensions { width, height });
}
if data.starts_with(b"RIFF") && data.len() >= 30 && data[8..12].starts_with(b"WEBP") {
if data[12..16].starts_with(b"VP8 ") && data.len() >= 30 {
let width = (u16::from_le_bytes([data[26], data[27]]) & 0x3FFF) as u32;
let height = (u16::from_le_bytes([data[28], data[29]]) & 0x3FFF) as u32;
return Some(ImageDimensions { width, height });
}
if data[12..16].starts_with(b"VP8L") && data.len() >= 25 {
let bits = u32::from_le_bytes([data[21], data[22], data[23], data[24]]);
let width = (bits & 0x3FFF) + 1;
let height = ((bits >> 14) & 0x3FFF) + 1;
return Some(ImageDimensions { width, height });
}
}
None
}
pub fn encode_kitty(base64_data: &str, opts: &ImageOptions) -> String {
let mut sequences = Vec::new();
let chunk_size = 4096;
let total_len = base64_data.len();
let mut offset = 0;
let mut params = vec![
"a=T".to_string(), "f=100".to_string(), "q=2".to_string(), ];
if let Some(id) = opts.id {
params.push(format!("i={}", id));
}
if opts.width_cells.is_some() || opts.height_cells.is_some() {
let w = opts.width_cells.map_or(0, |w| w);
let h = opts.height_cells.map_or(0, |h| h);
if w > 0 && h > 0 {
params.push(format!("c={},r={}", w, h));
} else if w > 0 {
params.push(format!("c={}", w));
} else if h > 0 {
params.push(format!("r={}", h));
}
}
while offset < total_len {
let end = (offset + chunk_size).min(total_len);
let chunk = &base64_data[offset..end];
let is_first = offset == 0;
let _is_last = end >= total_len;
let m = if is_first { 0 } else { 1 };
let mut chunk_params = params.clone();
if !is_first {
chunk_params.retain(|p| !p.starts_with("a=") && !p.starts_with("f="));
}
chunk_params.push(format!("m={}", m));
let param_str = chunk_params.join(",");
sequences.push(format!("\x1b_G{};{}\x1b\\", param_str, chunk));
offset = end;
}
if opts.place && (opts.x > 0 || opts.y > 0) {
sequences.insert(0, format!("\x1b[{};{}H", opts.y + 1, opts.x + 1));
}
sequences.join("")
}
pub fn encode_iterm2(base64_data: &str, opts: &ImageOptions) -> String {
let mut params = vec![
"inline=1".to_string(),
format!("size={}", base64_data.len()),
];
if let Some(w) = opts.width_cells {
params.push(format!("width={}c", w));
}
if let Some(h) = opts.height_cells {
params.push(format!("height={}c", h));
}
let param_str = params.join(";");
let mut result = String::new();
if opts.place && (opts.x > 0 || opts.y > 0) {
result.push_str(&format!("\x1b[{};{}H", opts.y + 1, opts.x + 1));
}
result.push_str(&format!("\x1b]1337;File={}:{}\x07", param_str, base64_data));
result
}
pub fn kitty_delete_image(id: u32) -> String {
format!("\x1b_Ga=d,d=i,i={}\x1b\\", id)
}
pub fn kitty_delete_all() -> String {
"\x1b_Ga=d,d=A\x1b\\".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_png_dimensions() {
let mut png = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
png.extend_from_slice(&[0, 0, 0, 13]); png.extend_from_slice(b"IHDR"); png.extend_from_slice(&[0, 0, 0, 100]); png.extend_from_slice(&[0, 0, 0, 200]); png.extend_from_slice(&[8]); png.extend_from_slice(&[2]); png.extend_from_slice(&[0, 0, 0]); png.extend_from_slice(&[0, 0, 0, 0]);
let dims = detect_dimensions(&png).unwrap();
assert_eq!(dims.width, 100);
assert_eq!(dims.height, 200);
}
#[test]
fn test_detect_gif_dimensions() {
let mut gif = b"GIF89a".to_vec();
gif.extend_from_slice(&[100, 0]); gif.extend_from_slice(&[50, 0]); gif.extend_from_slice(&[0x80]);
let dims = detect_dimensions(&gif).unwrap();
assert_eq!(dims.width, 100);
assert_eq!(dims.height, 50);
}
#[test]
fn test_detect_too_small() {
let data = [0u8; 5];
assert!(detect_dimensions(&data).is_none());
}
#[test]
fn test_detect_unknown_format() {
let data = vec![0u8; 100];
assert!(detect_dimensions(&data).is_none());
}
#[test]
fn test_encode_kitty_single_chunk() {
let base64 = "dGVzdA=="; let opts = ImageOptions::default();
let result = encode_kitty(base64, &opts);
assert!(result.contains("\x1b_G"));
assert!(result.contains("a=T"));
assert!(result.contains(base64));
assert!(result.contains("\x1b\\"));
}
#[test]
fn test_encode_kitty_with_id() {
let base64 = "dGVzdA==";
let opts = ImageOptions {
id: Some(42),
..Default::default()
};
let result = encode_kitty(base64, &opts);
assert!(result.contains("i=42"));
}
#[test]
fn test_encode_kitty_multi_chunk() {
let base64: String = "A".repeat(5000);
let opts = ImageOptions::default();
let result = encode_kitty(&base64, &opts);
let count = result.matches("\x1b_G").count();
assert_eq!(count, 2);
}
#[test]
fn test_encode_iterm2() {
let base64 = "dGVzdA==";
let opts = ImageOptions {
width_cells: Some(20),
height_cells: Some(10),
..Default::default()
};
let result = encode_iterm2(base64, &opts);
assert!(result.contains("\x1b]1337;File="));
assert!(result.contains("width=20c"));
assert!(result.contains("height=10c"));
assert!(result.contains(base64));
assert!(result.contains("\x07"));
}
#[test]
fn test_kitty_delete_image() {
let result = kitty_delete_image(42);
assert!(result.contains("a=d"));
assert!(result.contains("i=42"));
}
#[test]
fn test_kitty_delete_all() {
let result = kitty_delete_all();
assert!(result.contains("a=d"));
assert!(result.contains("d=A"));
}
}