1//! Lossy conversion between ANSI Color Codes
2
3#![cfg_attr(docsrs, feature(doc_auto_cfg))]
4#![warn(missing_docs)]
5#![warn(clippy::print_stderr)]
6#![warn(clippy::print_stdout)]
7
8pub mod palette;
9
10use anstyle::RgbColor as Rgb;
11
12/// Lossily convert from any color to RGB
13///
14/// As the palette for 4-bit colors is terminal/user defined, a [`palette::Palette`] must be
15/// provided to match against.
16pub const fn color_to_rgb(color: anstyle::Color, palette: palette::Palette) -> anstyle::RgbColor {
17 match color {
18 anstyle::Color::Ansi(color: AnsiColor) => ansi_to_rgb(color, palette),
19 anstyle::Color::Ansi256(color: Ansi256Color) => xterm_to_rgb(color, palette),
20 anstyle::Color::Rgb(color: RgbColor) => color,
21 }
22}
23
24/// Lossily convert from any color to 256-color
25///
26/// As the palette for 4-bit colors is terminal/user defined, a [`palette::Palette`] must be
27/// provided to match against.
28pub const fn color_to_xterm(color: anstyle::Color) -> anstyle::Ansi256Color {
29 match color {
30 anstyle::Color::Ansi(color: AnsiColor) => anstyle::Ansi256Color::from_ansi(color),
31 anstyle::Color::Ansi256(color: Ansi256Color) => color,
32 anstyle::Color::Rgb(color: RgbColor) => rgb_to_xterm(color),
33 }
34}
35
36/// Lossily convert from any color to 4-bit color
37///
38/// As the palette for 4-bit colors is terminal/user defined, a [`palette::Palette`] must be
39/// provided to match against.
40pub const fn color_to_ansi(color: anstyle::Color, palette: palette::Palette) -> anstyle::AnsiColor {
41 match color {
42 anstyle::Color::Ansi(color: AnsiColor) => color,
43 anstyle::Color::Ansi256(color: Ansi256Color) => xterm_to_ansi(color, palette),
44 anstyle::Color::Rgb(color: RgbColor) => rgb_to_ansi(color, palette),
45 }
46}
47
48/// Lossily convert from 4-bit color to RGB
49///
50/// As the palette for 4-bit colors is terminal/user defined, a [`palette::Palette`] must be
51/// provided to match against.
52pub const fn ansi_to_rgb(
53 color: anstyle::AnsiColor,
54 palette: palette::Palette,
55) -> anstyle::RgbColor {
56 palette.rgb_from_ansi(color)
57}
58
59/// Lossily convert from 256-color to RGB
60///
61/// As 256-color palette is a superset of 4-bit colors and since the palette for 4-bit colors is
62/// terminal/user defined, a [`palette::Palette`] must be provided to match against.
63pub const fn xterm_to_rgb(
64 color: anstyle::Ansi256Color,
65 palette: palette::Palette,
66) -> anstyle::RgbColor {
67 match palette.rgb_from_index(color.0) {
68 Some(rgb: RgbColor) => rgb,
69 None => XTERM_COLORS[color.0 as usize],
70 }
71}
72
73/// Lossily convert from the 256-color palette to 4-bit color
74///
75/// As the palette for 4-bit colors is terminal/user defined, a [`palette::Palette`] must be
76/// provided to match against.
77pub const fn xterm_to_ansi(
78 color: anstyle::Ansi256Color,
79 palette: palette::Palette,
80) -> anstyle::AnsiColor {
81 match color.0 {
82 0 => anstyle::AnsiColor::Black,
83 1 => anstyle::AnsiColor::Red,
84 2 => anstyle::AnsiColor::Green,
85 3 => anstyle::AnsiColor::Yellow,
86 4 => anstyle::AnsiColor::Blue,
87 5 => anstyle::AnsiColor::Magenta,
88 6 => anstyle::AnsiColor::Cyan,
89 7 => anstyle::AnsiColor::White,
90 8 => anstyle::AnsiColor::BrightBlack,
91 9 => anstyle::AnsiColor::BrightRed,
92 10 => anstyle::AnsiColor::BrightGreen,
93 11 => anstyle::AnsiColor::BrightYellow,
94 12 => anstyle::AnsiColor::BrightBlue,
95 13 => anstyle::AnsiColor::BrightMagenta,
96 14 => anstyle::AnsiColor::BrightCyan,
97 15 => anstyle::AnsiColor::BrightWhite,
98 _ => {
99 let rgb: RgbColor = XTERM_COLORS[color.0 as usize];
100 palette.find_match(color:rgb)
101 }
102 }
103}
104
105/// Lossily convert an RGB value to a 4-bit color
106///
107/// As the palette for 4-bit colors is terminal/user defined, a [`palette::Palette`] must be
108/// provided to match against.
109pub const fn rgb_to_ansi(
110 color: anstyle::RgbColor,
111 palette: palette::Palette,
112) -> anstyle::AnsiColor {
113 palette.find_match(color)
114}
115
116/// Lossily convert an RGB value to the 256-color palette
117pub const fn rgb_to_xterm(color: anstyle::RgbColor) -> anstyle::Ansi256Color {
118 // Skip placeholders
119 let index: usize = find_xterm_match(color);
120 anstyle::Ansi256Color(index as u8)
121}
122
123const fn find_xterm_match(color: anstyle::RgbColor) -> usize {
124 let mut best_index: usize = 16;
125 let mut best_distance: u32 = distance(c1:color, XTERM_COLORS[best_index]);
126
127 let mut index: usize = best_index + 1;
128 while index < XTERM_COLORS.len() {
129 let distance: u32 = distance(c1:color, XTERM_COLORS[index]);
130 if distance < best_distance {
131 best_index = index;
132 best_distance = distance;
133 }
134
135 index += 1;
136 }
137
138 best_index
139}
140
141/// Low-cost approximation from <https://www.compuphase.com/cmetric.htm>, modified to avoid sqrt
142pub(crate) const fn distance(c1: anstyle::RgbColor, c2: anstyle::RgbColor) -> u32 {
143 let c1_r: i32 = c1.r() as i32;
144 let c1_g: i32 = c1.g() as i32;
145 let c1_b: i32 = c1.b() as i32;
146 let c2_r: i32 = c2.r() as i32;
147 let c2_g: i32 = c2.g() as i32;
148 let c2_b: i32 = c2.b() as i32;
149
150 let r_sum: i32 = c1_r + c2_r;
151 let r_delta: i32 = c1_r - c2_r;
152 let g_delta: i32 = c1_g - c2_g;
153 let b_delta: i32 = c1_b - c2_b;
154
155 let r: i32 = (2 * 512 + r_sum) * r_delta * r_delta;
156 let g: i32 = 4 * g_delta * g_delta * (1 << 8);
157 let b: i32 = (2 * 767 - r_sum) * b_delta * b_delta;
158
159 (r + g + b) as u32
160}
161
162const XTERM_COLORS: [anstyle::RgbColor; 256] = [
163 // Placeholders to make the index work. See instead `palette` for these fields
164 Rgb(0, 0, 0),
165 Rgb(0, 0, 0),
166 Rgb(0, 0, 0),
167 Rgb(0, 0, 0),
168 Rgb(0, 0, 0),
169 Rgb(0, 0, 0),
170 Rgb(0, 0, 0),
171 Rgb(0, 0, 0),
172 Rgb(0, 0, 0),
173 Rgb(0, 0, 0),
174 Rgb(0, 0, 0),
175 Rgb(0, 0, 0),
176 Rgb(0, 0, 0),
177 Rgb(0, 0, 0),
178 Rgb(0, 0, 0),
179 Rgb(0, 0, 0),
180 // 6x6x6 cube. One each axis, the six indices map to [0, 95, 135, 175,
181 // 215, 255] RGB component values.
182 Rgb(0, 0, 0),
183 Rgb(0, 0, 95),
184 Rgb(0, 0, 135),
185 Rgb(0, 0, 175),
186 Rgb(0, 0, 215),
187 Rgb(0, 0, 255),
188 Rgb(0, 95, 0),
189 Rgb(0, 95, 95),
190 Rgb(0, 95, 135),
191 Rgb(0, 95, 175),
192 Rgb(0, 95, 215),
193 Rgb(0, 95, 255),
194 Rgb(0, 135, 0),
195 Rgb(0, 135, 95),
196 Rgb(0, 135, 135),
197 Rgb(0, 135, 175),
198 Rgb(0, 135, 215),
199 Rgb(0, 135, 255),
200 Rgb(0, 175, 0),
201 Rgb(0, 175, 95),
202 Rgb(0, 175, 135),
203 Rgb(0, 175, 175),
204 Rgb(0, 175, 215),
205 Rgb(0, 175, 255),
206 Rgb(0, 215, 0),
207 Rgb(0, 215, 95),
208 Rgb(0, 215, 135),
209 Rgb(0, 215, 175),
210 Rgb(0, 215, 215),
211 Rgb(0, 215, 255),
212 Rgb(0, 255, 0),
213 Rgb(0, 255, 95),
214 Rgb(0, 255, 135),
215 Rgb(0, 255, 175),
216 Rgb(0, 255, 215),
217 Rgb(0, 255, 255),
218 Rgb(95, 0, 0),
219 Rgb(95, 0, 95),
220 Rgb(95, 0, 135),
221 Rgb(95, 0, 175),
222 Rgb(95, 0, 215),
223 Rgb(95, 0, 255),
224 Rgb(95, 95, 0),
225 Rgb(95, 95, 95),
226 Rgb(95, 95, 135),
227 Rgb(95, 95, 175),
228 Rgb(95, 95, 215),
229 Rgb(95, 95, 255),
230 Rgb(95, 135, 0),
231 Rgb(95, 135, 95),
232 Rgb(95, 135, 135),
233 Rgb(95, 135, 175),
234 Rgb(95, 135, 215),
235 Rgb(95, 135, 255),
236 Rgb(95, 175, 0),
237 Rgb(95, 175, 95),
238 Rgb(95, 175, 135),
239 Rgb(95, 175, 175),
240 Rgb(95, 175, 215),
241 Rgb(95, 175, 255),
242 Rgb(95, 215, 0),
243 Rgb(95, 215, 95),
244 Rgb(95, 215, 135),
245 Rgb(95, 215, 175),
246 Rgb(95, 215, 215),
247 Rgb(95, 215, 255),
248 Rgb(95, 255, 0),
249 Rgb(95, 255, 95),
250 Rgb(95, 255, 135),
251 Rgb(95, 255, 175),
252 Rgb(95, 255, 215),
253 Rgb(95, 255, 255),
254 Rgb(135, 0, 0),
255 Rgb(135, 0, 95),
256 Rgb(135, 0, 135),
257 Rgb(135, 0, 175),
258 Rgb(135, 0, 215),
259 Rgb(135, 0, 255),
260 Rgb(135, 95, 0),
261 Rgb(135, 95, 95),
262 Rgb(135, 95, 135),
263 Rgb(135, 95, 175),
264 Rgb(135, 95, 215),
265 Rgb(135, 95, 255),
266 Rgb(135, 135, 0),
267 Rgb(135, 135, 95),
268 Rgb(135, 135, 135),
269 Rgb(135, 135, 175),
270 Rgb(135, 135, 215),
271 Rgb(135, 135, 255),
272 Rgb(135, 175, 0),
273 Rgb(135, 175, 95),
274 Rgb(135, 175, 135),
275 Rgb(135, 175, 175),
276 Rgb(135, 175, 215),
277 Rgb(135, 175, 255),
278 Rgb(135, 215, 0),
279 Rgb(135, 215, 95),
280 Rgb(135, 215, 135),
281 Rgb(135, 215, 175),
282 Rgb(135, 215, 215),
283 Rgb(135, 215, 255),
284 Rgb(135, 255, 0),
285 Rgb(135, 255, 95),
286 Rgb(135, 255, 135),
287 Rgb(135, 255, 175),
288 Rgb(135, 255, 215),
289 Rgb(135, 255, 255),
290 Rgb(175, 0, 0),
291 Rgb(175, 0, 95),
292 Rgb(175, 0, 135),
293 Rgb(175, 0, 175),
294 Rgb(175, 0, 215),
295 Rgb(175, 0, 255),
296 Rgb(175, 95, 0),
297 Rgb(175, 95, 95),
298 Rgb(175, 95, 135),
299 Rgb(175, 95, 175),
300 Rgb(175, 95, 215),
301 Rgb(175, 95, 255),
302 Rgb(175, 135, 0),
303 Rgb(175, 135, 95),
304 Rgb(175, 135, 135),
305 Rgb(175, 135, 175),
306 Rgb(175, 135, 215),
307 Rgb(175, 135, 255),
308 Rgb(175, 175, 0),
309 Rgb(175, 175, 95),
310 Rgb(175, 175, 135),
311 Rgb(175, 175, 175),
312 Rgb(175, 175, 215),
313 Rgb(175, 175, 255),
314 Rgb(175, 215, 0),
315 Rgb(175, 215, 95),
316 Rgb(175, 215, 135),
317 Rgb(175, 215, 175),
318 Rgb(175, 215, 215),
319 Rgb(175, 215, 255),
320 Rgb(175, 255, 0),
321 Rgb(175, 255, 95),
322 Rgb(175, 255, 135),
323 Rgb(175, 255, 175),
324 Rgb(175, 255, 215),
325 Rgb(175, 255, 255),
326 Rgb(215, 0, 0),
327 Rgb(215, 0, 95),
328 Rgb(215, 0, 135),
329 Rgb(215, 0, 175),
330 Rgb(215, 0, 215),
331 Rgb(215, 0, 255),
332 Rgb(215, 95, 0),
333 Rgb(215, 95, 95),
334 Rgb(215, 95, 135),
335 Rgb(215, 95, 175),
336 Rgb(215, 95, 215),
337 Rgb(215, 95, 255),
338 Rgb(215, 135, 0),
339 Rgb(215, 135, 95),
340 Rgb(215, 135, 135),
341 Rgb(215, 135, 175),
342 Rgb(215, 135, 215),
343 Rgb(215, 135, 255),
344 Rgb(215, 175, 0),
345 Rgb(215, 175, 95),
346 Rgb(215, 175, 135),
347 Rgb(215, 175, 175),
348 Rgb(215, 175, 215),
349 Rgb(215, 175, 255),
350 Rgb(215, 215, 0),
351 Rgb(215, 215, 95),
352 Rgb(215, 215, 135),
353 Rgb(215, 215, 175),
354 Rgb(215, 215, 215),
355 Rgb(215, 215, 255),
356 Rgb(215, 255, 0),
357 Rgb(215, 255, 95),
358 Rgb(215, 255, 135),
359 Rgb(215, 255, 175),
360 Rgb(215, 255, 215),
361 Rgb(215, 255, 255),
362 Rgb(255, 0, 0),
363 Rgb(255, 0, 95),
364 Rgb(255, 0, 135),
365 Rgb(255, 0, 175),
366 Rgb(255, 0, 215),
367 Rgb(255, 0, 255),
368 Rgb(255, 95, 0),
369 Rgb(255, 95, 95),
370 Rgb(255, 95, 135),
371 Rgb(255, 95, 175),
372 Rgb(255, 95, 215),
373 Rgb(255, 95, 255),
374 Rgb(255, 135, 0),
375 Rgb(255, 135, 95),
376 Rgb(255, 135, 135),
377 Rgb(255, 135, 175),
378 Rgb(255, 135, 215),
379 Rgb(255, 135, 255),
380 Rgb(255, 175, 0),
381 Rgb(255, 175, 95),
382 Rgb(255, 175, 135),
383 Rgb(255, 175, 175),
384 Rgb(255, 175, 215),
385 Rgb(255, 175, 255),
386 Rgb(255, 215, 0),
387 Rgb(255, 215, 95),
388 Rgb(255, 215, 135),
389 Rgb(255, 215, 175),
390 Rgb(255, 215, 215),
391 Rgb(255, 215, 255),
392 Rgb(255, 255, 0),
393 Rgb(255, 255, 95),
394 Rgb(255, 255, 135),
395 Rgb(255, 255, 175),
396 Rgb(255, 255, 215),
397 Rgb(255, 255, 255),
398 // 6x6x6 cube. One each axis, the six indices map to [0, 95, 135, 175,
399 // 215, 255] RGB component values.
400 Rgb(8, 8, 8),
401 Rgb(18, 18, 18),
402 Rgb(28, 28, 28),
403 Rgb(38, 38, 38),
404 Rgb(48, 48, 48),
405 Rgb(58, 58, 58),
406 Rgb(68, 68, 68),
407 Rgb(78, 78, 78),
408 Rgb(88, 88, 88),
409 Rgb(98, 98, 98),
410 Rgb(108, 108, 108),
411 Rgb(118, 118, 118),
412 Rgb(128, 128, 128),
413 Rgb(138, 138, 138),
414 Rgb(148, 148, 148),
415 Rgb(158, 158, 158),
416 Rgb(168, 168, 168),
417 Rgb(178, 178, 178),
418 Rgb(188, 188, 188),
419 Rgb(198, 198, 198),
420 Rgb(208, 208, 208),
421 Rgb(218, 218, 218),
422 Rgb(228, 228, 228),
423 Rgb(238, 238, 238),
424];
425