1use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
21use std::sync::atomic::{AtomicU32, Ordering};
22
23#[derive(Clone)]
25pub enum ImageSource {
26 Data(Vec<u8>),
28 File(String),
30}
31
32#[derive(Clone)]
34pub struct PendingImage {
35 pub cell_x: u16,
37 pub cell_y: u16,
39 pub data: Vec<u8>,
41 pub id: u32,
43 pub cell_width: u16,
45 pub cell_height: u16,
47}
48
49pub fn next_image_id() -> u32 {
51 static NEXT_ID: AtomicU32 = AtomicU32::new(10000); NEXT_ID.fetch_add(1, Ordering::Relaxed)
53}
54
55pub fn encode_kitty_image(data: &[u8], cell_x: u16, cell_y: u16, image_id: u32) -> String {
60 if data.is_empty() {
61 return String::new();
62 }
63
64 let b64_data = BASE64.encode(data);
66
67 const CHUNK_SIZE: usize = 4096;
69
70 let mut result = String::new();
71 let chunks: Vec<&str> = b64_data
72 .as_bytes()
73 .chunks(CHUNK_SIZE)
74 .map(|c| std::str::from_utf8(c).unwrap_or(""))
75 .collect();
76
77 let total_chunks = chunks.len();
78
79 for (i, chunk) in chunks.iter().enumerate() {
80 let is_first = i == 0;
81 let is_last = i == total_chunks - 1;
82 let more = if is_last { 0 } else { 1 };
83
84 if is_first {
85 result.push_str(&format!(
92 "\x1b_Ga=T,f=100,i={},t=d,m={},q=2;{}\x1b\\",
93 image_id, more, chunk
94 ));
95 } else {
96 result.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
98 }
99 }
100
101 result.insert_str(0, &format!("\x1b[{};{}H", cell_y + 1, cell_x + 1));
103
104 result
105}
106
107pub fn detect_image_dimensions(data: &[u8]) -> Option<(u32, u32)> {
110 if data.len() >= 10 && (data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a")) {
112 let width = u16::from_le_bytes([data[6], data[7]]) as u32;
113 let height = u16::from_le_bytes([data[8], data[9]]) as u32;
114 return Some((width, height));
115 }
116
117 if data.len() >= 24 && data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
120 let width = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
121 let height = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
122 return Some((width, height));
123 }
124
125 if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
127 let mut i = 2;
130 while i + 9 < data.len() {
131 if data[i] == 0xFF {
132 let marker = data[i + 1];
133 if marker == 0xC0 || marker == 0xC2 {
134 let height = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
136 let width = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32;
137 return Some((width, height));
138 }
139 if i + 3 < data.len() {
141 let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
142 i += 2 + len;
143 } else {
144 break;
145 }
146 } else {
147 i += 1;
148 }
149 }
150 }
151
152 None
153}
154
155pub fn pixels_to_cells(width: u32, height: u32) -> (u16, u16) {
158 let cell_width = ((width as f32) / 10.0).ceil() as u16;
159 let cell_height = ((height as f32) / 20.0).ceil() as u16;
160 (cell_width.max(1), cell_height.max(1))
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_png_detection() {
169 let mut png_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
171 png_data.extend_from_slice(&[0, 0, 0, 13]); png_data.extend_from_slice(b"IHDR");
173 png_data.extend_from_slice(&100u32.to_be_bytes()); png_data.extend_from_slice(&50u32.to_be_bytes()); assert_eq!(detect_image_dimensions(&png_data), Some((100, 50)));
177 }
178
179 #[test]
180 fn test_gif_detection() {
181 let mut gif_data = b"GIF89a".to_vec();
183 gif_data.extend_from_slice(&200u16.to_le_bytes()); gif_data.extend_from_slice(&100u16.to_le_bytes()); assert_eq!(detect_image_dimensions(&gif_data), Some((200, 100)));
187 }
188
189 #[test]
190 fn test_pixels_to_cells() {
191 assert_eq!(pixels_to_cells(100, 100), (10, 5));
192 assert_eq!(pixels_to_cells(50, 20), (5, 1));
193 assert_eq!(pixels_to_cells(1, 1), (1, 1));
194 }
195
196 #[test]
197 fn test_next_image_id_increments() {
198 let id1 = next_image_id();
199 let id2 = next_image_id();
200 assert!(id2 > id1);
201 }
202}