libsixel_rs/
device_control_string.rs

1//! Types and functions for encoding Sixel image control strings.
2
3use alloc::vec::Vec;
4
5use crate::std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
6use crate::{dimension::*, dither::*, quant::MethodForDiffuse, std::fmt, Error, Result};
7
8mod color_map;
9mod constants;
10mod raster;
11mod repeat;
12mod scrolling_mode;
13mod selector;
14mod sixel_char;
15mod sixel_control;
16
17pub use color_map::*;
18pub use constants::*;
19pub use raster::*;
20pub use repeat::*;
21pub use scrolling_mode::*;
22pub use selector::*;
23pub use sixel_char::*;
24pub use sixel_control::*;
25
26static CUR_CHAR: AtomicU8 = AtomicU8::new(0);
27static CUR_COLOR: AtomicUsize = AtomicUsize::new(0);
28static SAVE_CHAR_COUNT: AtomicUsize = AtomicUsize::new(0);
29
30const COLOR_THRESHOLD: usize = 24;
31
32fn current_char() -> SixelChar {
33    CUR_CHAR.load(Ordering::Relaxed).into()
34}
35
36fn set_current_char(sixel_char: SixelChar) {
37    CUR_CHAR.store(sixel_char.into(), Ordering::SeqCst);
38}
39
40fn current_color() -> BasicColor {
41    CUR_COLOR.load(Ordering::Relaxed).into()
42}
43
44fn set_current_color(color: usize) {
45    CUR_COLOR.store(color, Ordering::SeqCst);
46}
47
48fn save_char_count() -> usize {
49    SAVE_CHAR_COUNT.load(Ordering::Relaxed)
50}
51
52fn set_save_char_count(count: usize) {
53    SAVE_CHAR_COUNT.store(count, Ordering::SeqCst);
54}
55
56fn increment_save_char_count() {
57    SAVE_CHAR_COUNT.store(save_char_count() + 1, Ordering::SeqCst);
58}
59
60/// Variants of valid Sixel data structures.
61pub enum SixelItem {
62    Color(BasicColor),
63    Char(SixelChar),
64    Raster(Raster),
65    Repeat(Repeat),
66    Control(SixelControl),
67}
68
69impl fmt::Display for SixelItem {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            Self::Color(data) => write!(f, "{data}"),
73            Self::Char(data) => write!(f, "{data}"),
74            Self::Raster(data) => write!(f, "{data}"),
75            Self::Repeat(data) => write!(f, "{data}"),
76            Self::Control(data) => write!(f, "{data}"),
77        }
78    }
79}
80
81/// Common counts for Sixel encoding.
82pub struct SixelCounts {
83    pub rows: usize,
84    pub sixel_len: usize,
85    pub sixel_rem: usize,
86    pub rgb_len: usize,
87    pub hits: Vec<u8>,
88    pub threshold: u8,
89}
90
91impl SixelCounts {
92    /// Creates a new [SixelCounts].
93    pub const fn new() -> Self {
94        Self {
95            rows: 0,
96            sixel_len: 0,
97            sixel_rem: 0,
98            rgb_len: 0,
99            hits: Vec::new(),
100            threshold: 1,
101        }
102    }
103
104    /// Creates a new [SixelCounts] with the provided counts.
105    pub fn create(
106        rows: usize,
107        sixel_len: usize,
108        sixel_rem: usize,
109        rgb_len: usize,
110        pixels: usize,
111    ) -> Self {
112        Self {
113            rows,
114            sixel_len,
115            sixel_rem,
116            rgb_len,
117            hits: vec![0; pixels],
118            threshold: 1,
119        }
120    }
121}
122
123// Collection of common sixel state for passing to inner functions.
124struct SixelState<'s> {
125    pub pixels: &'s mut [u8],
126    pub sixel_data: &'s mut Vec<SixelItem>,
127    pub color_map: &'s mut ColorMap,
128    pub cur_color: &'s mut [u8; 3],
129    pub pal_idx: &'s mut usize,
130}
131
132/// Device Control String
133///
134/// The format for the device control string is as follows.
135///
136/// | DCS | P1   | ;    | P2;  | P3;  |  q   | s..s  | ST   |
137/// |-----|------|------|------|------|------|-------|------|
138/// | 9/0 | `**` | 3/11 | `**` | `**` | 7/1  | `***` | 9/12 |
139///
140/// where:
141///
142/// - **DCS** is a C1 control character that introduces the sixel data sequence. You can also express DCS as the 7-bit escape sequence ESC P for a 7-bit environment.
143/// - **P1** is the macro parameter. This parameter indicates the pixel aspect ratio used by the application or terminal. The pixel aspect ratio defines the shape of the pixel dots the terminal uses to draw images. For example, a pixel that is twice as high as it is wide has as aspect ratio of 2:1. The following list shows the values you can use for P1.
144/// - **;** is a semicolon (3/11). This character separates numeric parameters in a DCS string.
145/// - **P2** selects how the terminal draws the background color. You can use one of three values.
146/// - **P3** is the horizontal grid size parameter. The horizontal grid size is the horizontal distance between two pixel dots. The VT300 ignores this parameter because the horizontal grid size is fixed at 0.0195 cm (0.0075 in).
147/// - **q** indicates that this device control string is a sixel command.
148/// - **s...s** is the sixel-encoded data string. The sixel data characters are characters in the range of ? (hex 3F) to ~ (hex 7E). Each sixel data character represents six vertical pixels of data. Each sixel data character represents a binary value equal to the character code value minus hex 3F.
149/// - **ST** is the string terminator. ST is a C1 control character. You can also express ST as the 7-bit escape sequence ESC \ for a 7-bit environment ([`DcsMode::SevenBit`](DcsMode::SevenBit)).
150///
151/// **s..s** sixel encoding is typically preceded by color map definitions. This is useful for
152/// using the [`BasicColor`](SixelControl::Color) selector sequence in the sixel data.
153///
154/// See [`ColorIntroducer`](SixelControl::Color) for more details.
155pub struct DeviceControlString {
156    mode: DcsMode,
157    ratio: PixelAspectRatio,
158    background_color: DcsBackground,
159    raster: Raster,
160    color_map: ColorMap,
161    sixel_data: Vec<SixelItem>,
162}
163
164impl DeviceControlString {
165    /// Creates a new [DeviceControlString].
166    pub const fn new() -> Self {
167        Self {
168            mode: DcsMode::new(),
169            ratio: PixelAspectRatio::new(),
170            background_color: DcsBackground::new(),
171            raster: Raster::new(),
172            color_map: ColorMap::new(),
173            sixel_data: Vec::new(),
174        }
175    }
176
177    /// Gets the [DcsMode] selector.
178    pub const fn mode(&self) -> DcsMode {
179        self.mode
180    }
181
182    /// Sets the [DcsMode] selector.
183    pub fn set_mode(&mut self, mode: DcsMode) {
184        self.mode = mode;
185    }
186
187    /// Builder function that sets the [DcsMode] selector.
188    pub fn with_mode(mut self, mode: DcsMode) -> Self {
189        self.set_mode(mode);
190        self
191    }
192
193    /// Gets the [PixelAspectRatio] selector.
194    pub const fn ratio(&self) -> PixelAspectRatio {
195        self.ratio
196    }
197
198    /// Sets the [PixelAspectRatio] selector.
199    pub fn set_ratio(&mut self, ratio: PixelAspectRatio) {
200        self.ratio = ratio;
201    }
202
203    /// Builder function that sets the [PixelAspectRatio] selector.
204    pub fn with_ratio(mut self, ratio: PixelAspectRatio) -> Self {
205        self.set_ratio(ratio);
206        self
207    }
208
209    /// Gets the [DcsBackground] selector.
210    pub const fn background_color(&self) -> DcsBackground {
211        self.background_color
212    }
213
214    /// Sets the [DcsBackground] selector.
215    pub fn set_background_color(&mut self, background_color: DcsBackground) {
216        self.background_color = background_color;
217    }
218
219    /// Builder function that sets the [DcsBackground] selector.
220    pub fn with_background_color(mut self, background_color: DcsBackground) -> Self {
221        self.set_background_color(background_color);
222        self
223    }
224
225    /// Gets the [Raster] attributes.
226    pub const fn raster(&self) -> Raster {
227        self.raster
228    }
229
230    /// Sets the [Raster] attributes.
231    pub fn set_raster(&mut self, raster: Raster) {
232        self.raster = raster;
233    }
234
235    /// Builder function that sets the [Raster] attributes.
236    pub fn with_raster(mut self, raster: Raster) -> Self {
237        self.set_raster(raster);
238        self
239    }
240
241    /// Gets the [ColorMap].
242    pub const fn color_map(&self) -> &ColorMap {
243        &self.color_map
244    }
245
246    /// Sets the [ColorMap].
247    pub fn set_color_map(&mut self, color_map: ColorMap) {
248        self.color_map = color_map;
249    }
250
251    /// Builder function that sets the [ColorMap].
252    pub fn with_color_map(mut self, color_map: ColorMap) -> Self {
253        self.set_color_map(color_map);
254        self
255    }
256
257    /// Gets the [SixelItem] list.
258    pub fn sixel_data(&self) -> &[SixelItem] {
259        self.sixel_data.as_ref()
260    }
261
262    /// Sets the [SixelItem] list.
263    pub fn set_sixel_data<S: Into<Vec<SixelItem>>>(&mut self, sixel_data: S) {
264        self.sixel_data = sixel_data.into();
265    }
266
267    /// Builder function that sets the [SixelItem] list.
268    pub fn with_sixel_data<S: Into<Vec<SixelItem>>>(mut self, sixel_data: S) -> Self {
269        self.set_sixel_data(sixel_data);
270        self
271    }
272
273    /// Converts RGB pixel data into [DeviceControlString].
274    pub fn from_rgb(
275        pixels: &mut [u8],
276        width: usize,
277        height: usize,
278        diffuse_method: MethodForDiffuse,
279    ) -> Result<Self> {
280        let depth = 3;
281        let area = width.saturating_mul(height).saturating_mul(depth);
282        let pixel_len = pixels.len();
283
284        if pixel_len < area {
285            Err(Error::Output(format!("invalid area [width*height*depth(3)] ({area}) is out-of-bounds, pixel buffer length ({pixel_len})")))
286        } else if width == 0 || height == 0 {
287            Err(Error::Output(format!(
288                "invalid width/height, must be non-zero | width: {width}, height: {height}"
289            )))
290        } else {
291            let mut color_map = ColorMap::new();
292            // Add black as the first palette color
293            color_map.inner_mut().push(ColorMapItem::new());
294
295            let mut pal_idx = 0;
296            let mut cur_color = [0, 0, 0];
297
298            // RGB pixels are three bytes wide
299            let rgb_len = width.saturating_mul(3);
300
301            // length of six rows of pixels, one sixel == six pixels
302            // width * 6 * depth(3)
303            let sixel_len = rgb_len.saturating_mul(6);
304            let sixel_rows = area / sixel_len;
305            let sixel_rem = area % sixel_len;
306
307            let max_sixels = width.saturating_mul(sixel_rows).saturating_add(sixel_rem);
308            let mut sixel_data = Vec::with_capacity(max_sixels);
309
310            let mut sixel_counts = SixelCounts::create(
311                sixel_rows,
312                sixel_len,
313                sixel_rem,
314                rgb_len,
315                width.saturating_mul(height),
316            );
317
318            let mut sd = SpaceDimension {
319                width,
320                height,
321                ..Default::default()
322            };
323
324            let mut state = SixelState {
325                pixels,
326                sixel_data: &mut sixel_data,
327                color_map: &mut color_map,
328                cur_color: &mut cur_color,
329                pal_idx: &mut pal_idx,
330            };
331
332            state.build_color_map();
333
334            state.encode_body(&mut sd, &mut sixel_counts, diffuse_method)?;
335
336            Ok(Self::new()
337                .with_background_color(DcsBackground::Background)
338                .with_color_map(color_map)
339                .with_raster(Raster::create(1, 1, width, height))
340                .with_sixel_data(sixel_data))
341        }
342    }
343}
344
345impl<'s> SixelState<'s> {
346    fn build_color_map(&mut self) {
347        let mut counts: hashbrown::HashMap<u16, usize> = hashbrown::HashMap::with_capacity(1 << 15);
348
349        self.pixels.chunks_exact_mut(3).for_each(|c| {
350            // take upper five bits from each RGB value, and combine them into a single u16
351            c[0] &= 0xf8;
352            c[1] &= 0xf8;
353            c[2] &= 0xf8;
354
355            let idx = ((c[0] as u16) << 7) | ((c[1] as u16) << 2) | (c[2] >> 3) as u16;
356
357            let count = counts.entry(idx).or_insert(0);
358            *count += 1;
359        });
360
361        for (key, val) in counts.iter() {
362            let item = ColorMapItem::create_rgb(
363                0,
364                ((key & 0b111_1100_0000_0000) >> 7) as u8,
365                ((key & 0b11_1110_0000) >> 2) as u8,
366                ((key & 0b11_111) << 3) as u8,
367            );
368
369            let has_item = self
370                .color_map
371                .items()
372                .iter()
373                .any(|c| c.x() == item.x() && c.y() == item.y() && c.z() == item.z());
374
375            if val >= &COLOR_THRESHOLD && !has_item {
376                let idx = self.color_map.items().len();
377                self.color_map.inner_mut().push(item.with_number(idx));
378            }
379        }
380    }
381
382    fn encode_body(
383        &mut self,
384        sd: &mut SpaceDimension,
385        sixel_counts: &mut SixelCounts,
386        diffuse_method: MethodForDiffuse,
387    ) -> Result<()> {
388        let (sixel_rows, sixel_len, sixel_rem) = (
389            sixel_counts.rows,
390            sixel_counts.sixel_len,
391            sixel_counts.sixel_rem,
392        );
393
394        for row in 0..sixel_rows {
395            let row_idx = row * sixel_len;
396
397            // iterate over each row in a sixel plane to output the color of the row pixel
398            for six_idx in 0..6 {
399                self.encode_row(sd, sixel_counts, six_idx, row_idx, diffuse_method);
400
401                put_flash(self.sixel_data);
402
403                if six_idx < 5 {
404                    // overwrite the row for each sixel index
405                    self.sixel_data
406                        .push(SixelItem::Control(SixelControl::CarriageReturn));
407                }
408            }
409
410            self.sixel_data
411                .push(SixelItem::Control(SixelControl::NewLine));
412        }
413
414        // encode any remaining pixel rows
415        if sixel_rem != 0 {
416            for six_idx in 0..sixel_rem {
417                self.encode_remainder_row(sd, sixel_counts, six_idx, diffuse_method);
418
419                put_flash(self.sixel_data);
420
421                if six_idx < sixel_rem - 1 {
422                    // overwrite the row for each sixel index
423                    self.sixel_data
424                        .push(SixelItem::Control(SixelControl::CarriageReturn));
425                }
426            }
427
428            self.sixel_data
429                .push(SixelItem::Control(SixelControl::NewLine));
430        }
431
432        Ok(())
433    }
434
435    fn encode_row(
436        &mut self,
437        sd: &mut SpaceDimension,
438        sixel_counts: &mut SixelCounts,
439        six_idx: usize,
440        row_idx: usize,
441        diffuse_method: MethodForDiffuse,
442    ) -> bool {
443        let mut overwrite = false;
444
445        for col_idx in (0..sixel_counts.rgb_len).step_by(3) {
446            sd.x = col_idx;
447            sd.y = row_idx;
448
449            let pos =
450                (row_idx + col_idx).saturating_add(six_idx.saturating_mul(sixel_counts.sixel_len));
451            if pos.saturating_add(sixel_counts.sixel_len) < self.pixels.len() {
452                method::apply_15bpp_dither(&mut self.pixels[pos..], *sd, diffuse_method).ok();
453            }
454
455            // sixels are six pixels high, one pixel wide
456            // calculate offsets for each row in the sixel column
457            let off = row_idx + col_idx;
458
459            overwrite |= self.inner_encode(sixel_counts, six_idx, off);
460        }
461
462        overwrite
463    }
464
465    fn inner_encode(&mut self, sixel_counts: &mut SixelCounts, six_idx: usize, off: usize) -> bool {
466        let rgb_len = sixel_counts.rgb_len;
467        let offset = off + (rgb_len * six_idx);
468
469        let (r, g, b) = (
470            self.pixels[offset],
471            self.pixels[offset + 1],
472            self.pixels[offset + 2],
473        );
474
475        let mut color_item = ColorMapItem::create_rgb(0, r, g, b);
476
477        if color_item.is_black() {
478            *self.pal_idx = 0;
479        } else {
480            // check for a color change
481            let mut delta = i16::MAX;
482
483            // search for the closest color map entry to the current RGB value
484            self.color_map.items().iter().for_each(|c| {
485                let id = (color_item.x() as i16 - c.x() as i16).abs()
486                    + (color_item.y() as i16 - c.y() as i16).abs()
487                    + (color_item.z() as i16 - c.z() as i16).abs();
488
489                if id <= delta {
490                    delta = id;
491                    color_item.set_number(c.number());
492                }
493            });
494
495            let idx = color_item.number();
496            if idx != 0 {
497                *self.pal_idx = idx;
498            }
499        }
500
501        // use hit counts to not overwrite a pixel that has already been written
502        // **NOTE** divide the pixel offset by three to account for RGB width
503        let sixel_char = if color_item.is_black() {
504            SixelChar::full()
505        } else {
506            SixelChar::from_index(six_idx)
507        };
508        put_sixel_char(self.sixel_data, sixel_char, *self.pal_idx);
509
510        true
511    }
512
513    fn encode_remainder_row(
514        &mut self,
515        sd: &mut SpaceDimension,
516        sixel_counts: &SixelCounts,
517        six_idx: usize,
518        diffuse_method: MethodForDiffuse,
519    ) -> bool {
520        let (sixel_rows, sixel_len, sixel_rem, rgb_len) = (
521            sixel_counts.rows,
522            sixel_counts.sixel_len,
523            sixel_counts.sixel_rem,
524            sixel_counts.rgb_len,
525        );
526        let row_idx = sixel_rows * sixel_len;
527
528        let mut overwrite = false;
529
530        for col_idx in (0..rgb_len).step_by(3) {
531            sd.x = col_idx;
532            sd.y = row_idx;
533
534            let pos =
535                (row_idx + col_idx).saturating_add(six_idx.saturating_mul(sixel_counts.sixel_len));
536            if pos < self.pixels.len() {
537                method::apply_15bpp_dither(&mut self.pixels[pos..], *sd, diffuse_method).ok();
538            }
539
540            // sixels are six pixels high, one pixel wide
541            // calculate offsets for each row in the sixel column
542            let p1_off = row_idx * col_idx;
543            let p2_off = row_idx * col_idx + rgb_len;
544            let p3_off = row_idx * col_idx + (rgb_len * 2);
545            let p4_off = row_idx * col_idx + (rgb_len * 3);
546            let p5_off = row_idx * col_idx + (rgb_len * 4);
547
548            let sixel_plane = [
549                [
550                    self.pixels[p1_off],
551                    self.pixels[p1_off + 1],
552                    self.pixels[p1_off + 2],
553                ],
554                if sixel_rem >= 2 {
555                    [
556                        self.pixels[p2_off],
557                        self.pixels[p2_off + 1],
558                        self.pixels[p2_off + 2],
559                    ]
560                } else {
561                    [0, 0, 0]
562                },
563                if sixel_rem >= 3 {
564                    [
565                        self.pixels[p3_off],
566                        self.pixels[p3_off + 1],
567                        self.pixels[p3_off + 2],
568                    ]
569                } else {
570                    [0, 0, 0]
571                },
572                if sixel_rem >= 4 {
573                    [
574                        self.pixels[p4_off],
575                        self.pixels[p4_off + 1],
576                        self.pixels[p4_off + 2],
577                    ]
578                } else {
579                    [0, 0, 0]
580                },
581                if sixel_rem >= 5 {
582                    [
583                        self.pixels[p5_off],
584                        self.pixels[p5_off + 1],
585                        self.pixels[p5_off + 2],
586                    ]
587                } else {
588                    [0, 0, 0]
589                },
590                [0, 0, 0],
591            ];
592
593            // use hit counts to not overwrite a pixel that has already been written
594            // **NOTE** divide the pixel offset by three to account for RGB width
595            let sixel_char = SixelChar::from_plane(
596                &sixel_plane,
597                six_idx,
598                &sixel_counts.hits[(p1_off / 3)..],
599                sixel_counts.threshold,
600            );
601            put_sixel_char(self.sixel_data, sixel_char, *self.pal_idx);
602
603            overwrite |= sixel_plane
604                .iter()
605                .map(|c| (c != self.cur_color) as u8)
606                .sum::<u8>()
607                != 0;
608        }
609
610        overwrite
611    }
612}
613
614fn put_sixel_char(sixel_data: &mut Vec<SixelItem>, sixel_char: SixelChar, color_idx: usize) {
615    let cur_char = current_char();
616    let color = BasicColor::from(color_idx);
617
618    // check for a color change
619    let cur_color = current_color();
620    let color_change = cur_color != color;
621    if color_change {
622        log::trace!("Changing color: current {cur_color}, new {color}");
623        sixel_data.push(SixelItem::Color(color));
624        set_current_color(color.into());
625    }
626
627    if cur_char == sixel_char && !color_change {
628        increment_save_char_count();
629
630        let count = save_char_count();
631        log::trace!("Incrementing current character: char {cur_char}, count {count}");
632    } else {
633        let count = save_char_count();
634        log::trace!("Flashing character from put_sixel_char: char {sixel_char}, count {count}, saved char {cur_char}");
635
636        put_flash(sixel_data);
637
638        set_current_char(sixel_char);
639        set_save_char_count(1);
640
641        let cur_char = current_char();
642        log::trace!("Flashed character from put_sixel_char, new saved char: {cur_char}");
643    }
644}
645
646fn put_flash(sixel_data: &mut Vec<SixelItem>) {
647    let cur_char = current_char();
648    let save_count = save_char_count();
649
650    if save_count >= 3 {
651        log::trace!("Outputting repeat: count {save_count}, char {cur_char}");
652        sixel_data.push(SixelItem::Repeat(Repeat::create(save_count, cur_char)));
653    } else {
654        log::trace!("Outputting manual repeat: count {save_count}, char {cur_char}");
655        for _ in 0..save_count {
656            sixel_data.push(SixelItem::Char(cur_char));
657        }
658    }
659
660    set_current_char(SixelChar::new());
661    set_save_char_count(0);
662}
663
664impl fmt::Display for DeviceControlString {
665    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
666        let dcs = DcsFunction::from(self.mode);
667        let st = StFunction::from(self.mode);
668
669        write!(f, "{dcs}q{}", self.raster)?;
670        write!(f, "{}", self.color_map)?;
671        for s in self.sixel_data.iter() {
672            write!(f, "{s}")?;
673        }
674        write!(f, "{st}")
675    }
676}