Skip to main content

badterm_core/
sixel.rs

1//! Sixel graphics: `DCS p1 ; p2 q <data> ST`, as DEC's VT340 drew them and
2//! SyncTERM (CTerm) and xterm do today. Decoded here to an [`Image`], which
3//! the screen places at the cursor (see [`crate::screen::Screen::place_image`]).
4//!
5//! Each data character `?`..`~` is six pixels in a column (bit 0 at the
6//! top) in the current color; `!n` repeats the next one, `#n` picks a color
7//! and `#n;2;r;g;b` (or `;1;h;l;s`) defines one, `$` returns to the start
8//! of the six-pixel band and `-` starts the next band. `"pan;pad;w;h` sets
9//! the pixels' shape and the picture's size.
10
11use crate::palette::Rgb;
12
13/// A picture: 0xFFRRGGBB pixels, 0 where nothing was drawn.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct Image {
16    pub width: usize,
17    pub height: usize,
18    pub pixels: Vec<u32>,
19}
20
21/// An opaque pixel.
22pub const fn solid(rgb: Rgb) -> u32 {
23    0xFF00_0000 | (rgb & 0x00FF_FFFF)
24}
25
26/// Larger pictures are cut off (no screen is anywhere near this size).
27const MAX_SIZE: usize = 2048;
28
29/// The VT340's default palette, in percent.
30const DEFAULT: [(u32, u32, u32); 16] = [
31    (0, 0, 0),
32    (20, 20, 80),
33    (80, 13, 13),
34    (20, 80, 20),
35    (80, 20, 80),
36    (20, 80, 80),
37    (80, 80, 20),
38    (53, 53, 53),
39    (26, 26, 26),
40    (33, 33, 60),
41    (60, 26, 26),
42    (33, 60, 33),
43    (60, 33, 60),
44    (33, 60, 60),
45    (60, 60, 33),
46    (80, 80, 80),
47];
48
49fn percent(r: u32, g: u32, b: u32) -> Rgb {
50    let c = |v: u32| v.min(100) * 255 / 100;
51    c(r) << 16 | c(g) << 8 | c(b)
52}
53
54/// DEC's HLS: hue 0 is blue, 120 red, 240 green; lightness and saturation
55/// in percent.
56fn hls(h: u32, l: u32, s: u32) -> Rgb {
57    let h = ((h + 240) % 360) as f64;
58    let (l, s) = (l.min(100) as f64 / 100.0, s.min(100) as f64 / 100.0);
59    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
60    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
61    let (r, g, b) = match h as u32 / 60 {
62        0 => (c, x, 0.0),
63        1 => (x, c, 0.0),
64        2 => (0.0, c, x),
65        3 => (0.0, x, c),
66        4 => (x, 0.0, c),
67        _ => (c, 0.0, x),
68    };
69    let m = l - c / 2.0;
70    let v = |f: f64| (((f + m) * 255.0).round().clamp(0.0, 255.0)) as u32;
71    v(r) << 16 | v(g) << 8 | v(b)
72}
73
74/// The numbers after a control character: digits and `;`, stopping at
75/// anything else. Returns them and how many bytes they took.
76fn numbers(data: &[u8]) -> (Vec<u32>, usize) {
77    let mut out = Vec::new();
78    let mut cur: Option<u32> = None;
79    let mut i = 0;
80    while i < data.len() {
81        match data[i] {
82            d @ b'0'..=b'9' => cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add((d - b'0') as u32)),
83            b';' => out.push(cur.take().unwrap_or(0)),
84            _ => break,
85        }
86        i += 1;
87    }
88    if let Some(v) = cur {
89        out.push(v);
90    }
91    (out, i)
92}
93
94/// Decode a sixel string: `params` are the DCS parameters before `q`,
95/// `data` what follows it. Unset pixels are `background` (opaque) unless
96/// `p2` is 1, when they stay see-through.
97pub fn decode(params: &[u32], data: &[u8], background: Rgb) -> Image {
98    let p1 = params.first().copied().unwrap_or(0);
99    let transparent = params.get(1).copied() == Some(1);
100    // Pixel height from p1 (deprecated; raster attributes override it).
101    let mut aspect = match p1 {
102        2 => 5,
103        3 | 4 => 3,
104        7..=9 => 1,
105        _ => 2,
106    };
107    let mut palette: [Rgb; 256] = std::array::from_fn(|i| {
108        let (r, g, b) = DEFAULT[i % 16];
109        percent(r, g, b)
110    });
111    let mut color = palette[0];
112    let mut rows: Vec<Vec<u32>> = Vec::new();
113    let (mut x, mut band) = (0usize, 0usize);
114    let (mut width, mut height) = (0usize, 0usize);
115    let mut raster = (0usize, 0usize);
116    let mut drawn = false;
117
118    let mut i = 0;
119    while i < data.len() {
120        let c = data[i];
121        i += 1;
122        match c {
123            b'?'..=b'~' | b'!' => {
124                let (count, bits) = if c == b'!' {
125                    let (n, used) = numbers(&data[i..]);
126                    i += used;
127                    let Some(&ch) = data.get(i).filter(|ch| (b'?'..=b'~').contains(ch)) else { continue };
128                    i += 1;
129                    (n.first().copied().unwrap_or(1).max(1) as usize, ch - b'?')
130                } else {
131                    (1, c - b'?')
132                };
133                drawn = true;
134                let count = count.min(MAX_SIZE.saturating_sub(x));
135                if bits != 0 {
136                    for bit in 0..6 {
137                        if bits & (1 << bit) == 0 {
138                            continue;
139                        }
140                        for dy in 0..aspect {
141                            let y = (band * 6 + bit) * aspect + dy;
142                            if y >= MAX_SIZE {
143                                continue;
144                            }
145                            if rows.len() <= y {
146                                rows.resize_with(y + 1, Vec::new);
147                            }
148                            let row = &mut rows[y];
149                            if row.len() < x + count {
150                                row.resize(x + count, 0);
151                            }
152                            row[x..x + count].fill(solid(color));
153                        }
154                    }
155                }
156                x += count;
157                width = width.max(x);
158                height = height.max(((band + 1) * 6 * aspect).min(MAX_SIZE));
159            }
160            b'#' => {
161                let (n, used) = numbers(&data[i..]);
162                i += used;
163                let Some(&reg) = n.first() else { continue };
164                let reg = (reg as usize).min(255);
165                if n.len() >= 5 {
166                    palette[reg] = match n[1] {
167                        1 => hls(n[2], n[3], n[4]),
168                        _ => percent(n[2], n[3], n[4]),
169                    };
170                }
171                color = palette[reg];
172            }
173            b'"' => {
174                let (n, used) = numbers(&data[i..]);
175                i += used;
176                if !drawn {
177                    let (pan, pad) = (n.first().copied().unwrap_or(1), n.get(1).copied().unwrap_or(1).max(1));
178                    aspect = ((pan as f64 / pad as f64).round() as usize).clamp(1, 10);
179                    raster = (
180                        n.get(2).copied().unwrap_or(0).min(MAX_SIZE as u32) as usize,
181                        n.get(3).copied().unwrap_or(0).min(MAX_SIZE as u32) as usize,
182                    );
183                }
184            }
185            b'$' => x = 0,
186            b'-' => {
187                x = 0;
188                band += 1;
189            }
190            _ => {}
191        }
192    }
193
194    // A raster size that was given is the least the picture covers.
195    if !transparent {
196        width = width.max(raster.0);
197        height = height.max(raster.1);
198    }
199    let fill = if transparent { 0 } else { solid(background) };
200    let mut pixels = vec![fill; width * height];
201    for (y, row) in rows.iter().enumerate().take(height) {
202        for (x, &p) in row.iter().enumerate().take(width) {
203            if p != 0 {
204                pixels[y * width + x] = p;
205            }
206        }
207    }
208    Image { width, height, pixels }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn bands_colors_and_repeats() {
217        // A 3x12 picture: red column, then two green columns, over two bands.
218        let img = decode(&[0, 1], b"#1;2;100;0;0#2;2;0;100;0\"1;1#1~#2!2~-#1~#2!2~", 0);
219        assert_eq!((img.width, img.height), (3, 12));
220        assert_eq!(img.pixels[0], solid(0xFF0000));
221        assert_eq!(img.pixels[1], solid(0x00FF00));
222        assert_eq!(img.pixels[11 * 3 + 2], solid(0x00FF00));
223    }
224
225    #[test]
226    fn passes_and_transparency() {
227        // Bit 0 red, then over the same band ($) bit 1 blue; bits 2-5 unset.
228        let img = decode(&[0, 1], b"\"1;1#1;2;100;0;0@$#2;2;0;0;100A", 0);
229        assert_eq!(img.pixels[..6], [solid(0xFF0000), solid(0x0000FF), 0, 0, 0, 0]);
230        // Without p2 = 1, unset pixels are the background.
231        let img = decode(&[], b"\"1;1;2;6@", 0x123456);
232        assert_eq!((img.width, img.height), (2, 6));
233        assert_eq!(img.pixels[1], solid(0x123456));
234        assert_eq!(img.pixels[0], solid(0));
235    }
236
237    #[test]
238    fn pixel_shape() {
239        // The default (p1 = 0) is pixels two high.
240        let img = decode(&[], b"~", 0);
241        assert_eq!((img.width, img.height), (1, 12));
242        let img = decode(&[9], b"~", 0);
243        assert_eq!(img.height, 6);
244    }
245
246    #[test]
247    fn random_input_is_harmless() {
248        let mut seed = 0x1234_5678u32;
249        let alphabet = b"?@AB~!#$-\";0123456789;;;";
250        for _ in 0..200 {
251            let data: Vec<u8> = (0..2000)
252                .map(|_| {
253                    seed ^= seed << 13;
254                    seed ^= seed >> 17;
255                    seed ^= seed << 5;
256                    alphabet[seed as usize % alphabet.len()]
257                })
258                .collect();
259            let img = decode(&[0, 0], &data, 0);
260            assert!(img.width <= MAX_SIZE && img.height <= MAX_SIZE);
261            assert_eq!(img.pixels.len(), img.width * img.height);
262        }
263        // A huge repeat or raster size is cut off.
264        let img = decode(&[9], b"\"1;1;99999;99999!999999~", 0);
265        assert_eq!((img.width, img.height), (MAX_SIZE, MAX_SIZE));
266    }
267
268    #[test]
269    fn hls_colors() {
270        assert_eq!(hls(0, 50, 100), 0x0000FF);
271        assert_eq!(hls(120, 50, 100), 0xFF0000);
272        assert_eq!(hls(240, 50, 100), 0x00FF00);
273        assert_eq!(hls(0, 100, 0), 0xFFFFFF);
274    }
275}