Skip to main content

devela/media/visual/image/sixel/
color.rs

1// devela::media::visual::image::sixel::color
2//
3//! Defines [`SixelColor`].
4//
5
6use crate::{Cmp, Digits, Display, FmtResult, Formatter, format_buf, is, write_at};
7
8#[doc = crate::_tags!(color term)]
9/// Sixel color representation.
10#[doc = crate::_doc_meta!{location("media/visual/image")}]
11///
12/// It stores r, g, b components (0-99 each).
13///
14/// The default color is black.
15#[must_use]
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct SixelColor {
18    data: [u8; 3],
19}
20
21impl SixelColor {
22    /// The maximum value per color channel.
23    pub const MAX_VALUE: u8 = 99;
24
25    /// Create a new RGB color
26    ///
27    /// # Arguments
28    /// * `r` - Red component (0-99)
29    /// * `g` - Green component (0-99)
30    /// * `b` - Blue component (0-99)
31    pub const fn new_rgb(r: u8, g: u8, b: u8) -> Self {
32        Self {
33            data: [
34                Cmp(r).min(Self::MAX_VALUE),
35                Cmp(g).min(Self::MAX_VALUE),
36                Cmp(b).min(Self::MAX_VALUE),
37            ],
38        }
39    }
40
41    /// Convert from standard RGB888 (0-255 range) to sixel 0-99 range.
42    pub const fn from_rgb888(r: u8, g: u8, b: u8) -> Self {
43        Self {
44            data: [
45                (r as u16 * Self::MAX_VALUE as u16 / 255) as u8,
46                (g as u16 * Self::MAX_VALUE as u16 / 255) as u8,
47                (b as u16 * Self::MAX_VALUE as u16 / 255) as u8,
48            ],
49        }
50    }
51
52    /// Writes the sixel color definition bytes for a given color index,
53    /// returning the number of bytes written.
54    ///
55    /// Format: `#IDX;2;R;G;B`
56    ///
57    /// This function writes the minimum number of bytes necessary, omitting any 0 values.
58    /// The bytes written can vary between 6 and 15, depending on the values themselves.
59    pub const fn write_definition_bytes(
60        self,
61        idx: u16,
62        buf: &mut [u8],
63        mut offset: usize,
64    ) -> usize {
65        let start = offset;
66        write_at!(buf, +=offset, b'#');
67        offset += Digits(idx).write_digits10_nonzero(buf, offset);
68        write_at!(buf, +=offset, b';', b'2', b';'); // 2=RGB, 1=HSL
69        offset += Digits(self.r()).write_digits10_nonzero(buf, offset);
70        write_at!(buf, +=offset, b';');
71        offset += Digits(self.g()).write_digits10_nonzero(buf, offset);
72        write_at!(buf, +=offset, b';');
73        offset += Digits(self.b()).write_digits10_nonzero(buf, offset);
74        offset - start
75    }
76    /// Writes the sixel color definition bytes for a given color index,
77    /// returning the number of bytes written.
78    ///
79    /// Returns `None` if there's not at least 15 bytes free in the given buffer.
80    ///
81    /// Format: `#ID;2;R;G;B`
82    ///
83    /// This function writes the minimum number of bytes necessary, omitting any 0 values.
84    /// The bytes written can vary between 6 and 15, depending on the values themselves.
85    pub const fn write_definition_bytes_checked(
86        self,
87        idx: u16,
88        buf: &mut [u8],
89        mut offset: usize,
90    ) -> Option<usize> {
91        is![offset + 15 > buf.len(), return None];
92        let start = offset;
93        write_at!(buf, +=offset, b'#');
94        offset += Digits(idx).write_digits10_nonzero(buf, offset);
95        write_at!(buf, +=offset, b';', b'2', b';'); // 2=RGB, 1=HSL
96        offset += Digits(self.r()).write_digits10_nonzero(buf, offset);
97        write_at!(buf, +=offset, b';');
98        offset += Digits(self.g()).write_digits10_nonzero(buf, offset);
99        write_at!(buf, +=offset, b';');
100        offset += Digits(self.b()).write_digits10_nonzero(buf, offset);
101        Some(offset - start)
102    }
103
104    /// Get color components.
105    #[must_use]
106    #[inline(always)]
107    pub const fn components(self) -> (u8, u8, u8) {
108        (self.data[0], self.data[1], self.data[2])
109    }
110    /// Get red component (0-99)
111    #[must_use]
112    #[inline(always)]
113    pub const fn r(self) -> u8 {
114        self.data[0]
115    }
116    /// Get green component (0-99)
117    #[must_use]
118    #[inline(always)]
119    pub const fn g(self) -> u8 {
120        self.data[1]
121    }
122    /// Get blue component (0-99)
123    #[must_use]
124    #[inline(always)]
125    pub const fn b(self) -> u8 {
126        self.data[2]
127    }
128
129    /// Convert from sixel 0-99 range to standard RGB 0-255 range.
130    pub const fn to_rgb888(self) -> (u8, u8, u8) {
131        let (r, g, b) = self.components();
132        (
133            (r as u16 * 255 / Self::MAX_VALUE as u16) as u8,
134            (g as u16 * 255 / Self::MAX_VALUE as u16) as u8,
135            (b as u16 * 255 / Self::MAX_VALUE as u16) as u8,
136        )
137    }
138    /// Convert from sixel 0-99 range to standard RGB 0x00-FF range.
139    pub const fn to_rgb888_hex(self) -> [u8; 6] {
140        let r = Digits(self.r()).digits16();
141        let g = Digits(self.g()).digits16();
142        let b = Digits(self.b()).digits16();
143        [r[0], r[1], g[0], g[1], b[0], b[1]]
144    }
145
146    // /// Simple color distance metric (Manhattan distance in RGB space)
147    // #[must_use]
148    // #[inline(always)]
149    // pub const fn distance(self, other: SixelColor) -> u16 {
150    //     let dr = (self.r() as i16 - other.r() as i16).unsigned_abs();
151    //     let dg = (self.g() as i16 - other.g() as i16).unsigned_abs();
152    //     let db = (self.b() as i16 - other.b() as i16).unsigned_abs();
153    //     dr + dg + db
154    // }
155    /// Simple color distance metric (Manhattan distance in RGB space)
156    #[must_use]
157    #[inline(always)]
158    pub const fn is_similar_to(self, other: SixelColor) -> bool {
159        let dr = (self.r() as i16 - other.r() as i16).unsigned_abs();
160        let dg = (self.g() as i16 - other.g() as i16).unsigned_abs();
161        let db = (self.b() as i16 - other.b() as i16).unsigned_abs();
162        // IMPROVE
163        // dr < 10 && dg < 10 && db < 10
164        // dr < 6 && dg < 6 && db < 6
165        // dr < 3 && dg < 3 && db < 3
166        dr < 1 && dg < 1 && db < 1
167    }
168
169    /// Compile-time comparison.
170    #[must_use]
171    pub const fn eq(self, other: Self) -> bool {
172        self.data[0] == other.data[0]
173            && self.data[1] == other.data[1]
174            && self.data[2] == other.data[2]
175    }
176
177    /// Check if this color is black.
178    pub const fn is_black(self) -> bool {
179        self.r() == 0 && self.g() == 0 && self.b() == 0
180    }
181
182    /// Check if this color is white.
183    pub const fn is_white(self) -> bool {
184        self.r() == Self::MAX_VALUE && self.g() == Self::MAX_VALUE && self.b() == Self::MAX_VALUE
185    }
186
187    /// Calculate luminance (quick and dirty approximation).
188    pub const fn luminance(self) -> u8 {
189        (self.r() * 3 + self.g() * 6 + self.b()) / 10 // ± 0.299, 0.587, 0.114
190        // (self.r() + self.g() + self.b()) / 3 // average
191    }
192
193    /// Create a brighter version of this color
194    pub const fn brighten(self, amount: u8) -> Self {
195        Self::new_rgb(
196            Cmp(self.r().saturating_add(amount)).min(Self::MAX_VALUE),
197            Cmp(self.g().saturating_add(amount)).min(Self::MAX_VALUE),
198            Cmp(self.b().saturating_add(amount)).min(Self::MAX_VALUE),
199        )
200    }
201
202    /// Create a darker version of this color
203    pub const fn darken(self, amount: u8) -> Self {
204        Self::new_rgb(
205            self.r().saturating_sub(amount),
206            self.g().saturating_sub(amount),
207            self.b().saturating_sub(amount),
208        )
209    }
210}
211
212/// # Constants
213impl SixelColor {
214    /// Black color
215    pub const BLACK: Self = Self::new_rgb(0, 0, 0);
216    /// White color
217    pub const WHITE: Self = Self::new_rgb(99, 99, 99);
218    /// Red color
219    pub const RED: Self = Self::new_rgb(99, 0, 0);
220    /// Green color
221    pub const GREEN: Self = Self::new_rgb(0, 99, 0);
222    /// Blue color
223    pub const BLUE: Self = Self::new_rgb(0, 0, 99);
224    /// Yellow color
225    pub const YELLOW: Self = Self::new_rgb(99, 99, 0);
226    /// Magenta color
227    pub const MAGENTA: Self = Self::new_rgb(99, 0, 99);
228    /// Cyan color
229    pub const CYAN: Self = Self::new_rgb(0, 99, 99);
230
231    /// Grayscale color helper.
232    pub const fn grayscale(intensity: u8) -> Self {
233        let intensity = Cmp(intensity).min(Self::MAX_VALUE);
234        Self::new_rgb(intensity, intensity, intensity)
235    }
236}
237
238impl Display for SixelColor {
239    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult<()> {
240        let mut buf = [0u8; 12];
241        let (r, g, b) = self.components();
242        f.write_str(format_buf!(&mut buf, "{r:02x}{g:02x}{b:02x}").unwrap())
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::SixelColor;
249
250    #[test]
251    // Format: `#ID;2;R;G;B`
252    fn write_definition_bytes() {
253        let mut buf = [0; 20];
254        let c = SixelColor::new_rgb(100, 000, 009); // the maximum is capped to 99
255        let idx = 32;
256        let bytes = c.write_definition_bytes(idx, &mut buf, 0);
257        assert_eq![bytes, 11];
258        assert_eq![&buf[0..bytes], b"#32;2;99;;9"];
259
260        // test minimum size: 6
261        let c = SixelColor::new_rgb(0, 0, 0);
262        let bytes = c.write_definition_bytes(0, &mut buf, 0);
263        assert_eq![bytes, 6];
264        assert_eq![&buf[0..bytes], b"#;2;;;"];
265
266        // test maximum size: 15
267        let c = SixelColor::new_rgb(255, 255, 255);
268        let bytes = c.write_definition_bytes(255, &mut buf, 0);
269        assert_eq![bytes, 15];
270        assert_eq![&buf[0..bytes], b"#255;2;99;99;99"];
271    }
272}