devela/media/visual/image/sixel/
color.rs1use crate::{Cmp, Digits, Display, FmtResult, Formatter, format_buf, is, write_at};
7
8#[doc = crate::_tags!(color term)]
9#[doc = crate::_doc_meta!{location("media/visual/image")}]
11#[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 pub const MAX_VALUE: u8 = 99;
24
25 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 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 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';'); 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 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';'); 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 #[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 #[must_use]
112 #[inline(always)]
113 pub const fn r(self) -> u8 {
114 self.data[0]
115 }
116 #[must_use]
118 #[inline(always)]
119 pub const fn g(self) -> u8 {
120 self.data[1]
121 }
122 #[must_use]
124 #[inline(always)]
125 pub const fn b(self) -> u8 {
126 self.data[2]
127 }
128
129 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 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 #[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 dr < 1 && dg < 1 && db < 1
167 }
168
169 #[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 pub const fn is_black(self) -> bool {
179 self.r() == 0 && self.g() == 0 && self.b() == 0
180 }
181
182 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 pub const fn luminance(self) -> u8 {
189 (self.r() * 3 + self.g() * 6 + self.b()) / 10 }
192
193 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 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
212impl SixelColor {
214 pub const BLACK: Self = Self::new_rgb(0, 0, 0);
216 pub const WHITE: Self = Self::new_rgb(99, 99, 99);
218 pub const RED: Self = Self::new_rgb(99, 0, 0);
220 pub const GREEN: Self = Self::new_rgb(0, 99, 0);
222 pub const BLUE: Self = Self::new_rgb(0, 0, 99);
224 pub const YELLOW: Self = Self::new_rgb(99, 99, 0);
226 pub const MAGENTA: Self = Self::new_rgb(99, 0, 99);
228 pub const CYAN: Self = Self::new_rgb(0, 99, 99);
230
231 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 fn write_definition_bytes() {
253 let mut buf = [0; 20];
254 let c = SixelColor::new_rgb(100, 000, 009); 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 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 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}