Skip to main content

photon_ui/
image.rs

1use base64::{
2    Engine as _,
3    engine::general_purpose::STANDARD,
4};
5
6/// Encode image data for the Kitty graphics protocol.
7///
8/// The data is base64-encoded and chunked with `m=1` (more data follows)
9/// delimiters. Each chunk is up to 4096 bytes of base64 text.
10pub fn encode_kitty(id: u32, data: &[u8], _mime_type: &str) -> String {
11    let b64 = STANDARD.encode(data);
12    let mut seq = format!("\x1b_Ga=T,f=100,i={},m=1;", id);
13    const CHUNK_SIZE: usize = 4096;
14    for chunk in b64.as_bytes().chunks(CHUNK_SIZE) {
15        seq.push_str(std::str::from_utf8(chunk).unwrap());
16        seq.push_str("\x1b\\");
17    }
18    seq
19}
20
21/// Encode image data for the iTerm2 inline image protocol.
22pub fn encode_iterm2(data: &[u8], _mime_type: &str) -> String {
23    let b64 = STANDARD.encode(data);
24    format!("\x1b]1337;File=inline=1:{}\x07", b64)
25}
26
27/// Generate a Kitty graphics protocol delete command for the given image id.
28pub fn delete_kitty_image(id: u32) -> String {
29    format!("\x1b_Ga=d,d=I,i={}\x1b\\", id)
30}
31
32/// Parse width and height from PNG file header bytes.
33///
34/// Returns `None` if the data is too short or the PNG signature is missing.
35pub fn get_png_dimensions(data: &[u8]) -> Option<(u32, u32)> {
36    if data.len() < 24 || &data[0..8] != b"\x89PNG\r\n\x1a\n" {
37        return None;
38    }
39    let width = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
40    let height = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
41    Some((width, height))
42}
43
44/// Parse width and height from JPEG SOF0 / SOF2 marker segments.
45///
46/// Searches for `0xFFC0` (baseline) or `0xFFC2` (progressive) markers.
47pub fn get_jpeg_dimensions(data: &[u8]) -> Option<(u32, u32)> {
48    let mut i = 2;
49    while i < data.len().saturating_sub(9) {
50        if data[i] == 0xff && (data[i + 1] == 0xc0 || data[i + 1] == 0xc2) {
51            let h = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
52            let w = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32;
53            return Some((w, h));
54        }
55        i += 1;
56    }
57    None
58}
59
60/// Parse width and height from GIF logical screen descriptor.
61pub fn get_gif_dimensions(data: &[u8]) -> Option<(u32, u32)> {
62    if data.len() < 10 {
63        return None;
64    }
65    let w = u16::from_le_bytes([data[6], data[7]]) as u32;
66    let h = u16::from_le_bytes([data[8], data[9]]) as u32;
67    Some((w, h))
68}
69
70/// Parse width and height from a VP8X WebP chunk.
71///
72/// Returns `None` if the RIFF/WEBP signatures are missing or the VP8X chunk
73/// is not present.
74pub fn get_webp_dimensions(data: &[u8]) -> Option<(u32, u32)> {
75    if data.len() < 30 || &data[0..4] != b"RIFF" || &data[8..12] != b"WEBP" {
76        return None;
77    }
78    if &data[12..16] == b"VP8X" {
79        let w = u32::from_le_bytes([data[24], data[25], data[26], 0]) + 1;
80        let h = u32::from_le_bytes([data[27], data[28], data[29], 0]) + 1;
81        return Some((w, h));
82    }
83    None
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn jpeg_dimensions_valid() {
92        let mut data = vec![0xff, 0xd8];
93        data.extend_from_slice(&[0xff, 0xc0]);
94        data.extend_from_slice(&[0x00, 0x0b]);
95        data.extend_from_slice(&[0x08]);
96        data.extend_from_slice(&[0x00, 0x10]);
97        data.extend_from_slice(&[0x00, 0x20]);
98        data.extend_from_slice(&[0x01, 0x01, 0x11, 0x00]);
99        assert_eq!(get_jpeg_dimensions(&data), Some((32, 16)));
100    }
101
102    #[test]
103    fn jpeg_dimensions_sof2() {
104        let mut data = vec![0xff, 0xd8];
105        data.extend_from_slice(&[0xff, 0xc2]);
106        data.extend_from_slice(&[0x00, 0x0b]);
107        data.extend_from_slice(&[0x08]);
108        data.extend_from_slice(&[0x00, 0x20]);
109        data.extend_from_slice(&[0x00, 0x10]);
110        data.extend_from_slice(&[0x01, 0x01, 0x11, 0x00]);
111        assert_eq!(get_jpeg_dimensions(&data), Some((16, 32)));
112    }
113}