anstyle_lossy/
lib.rs

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) => ansi_to_rgb(color, palette),
19        anstyle::Color::Ansi256(color) => xterm_to_rgb(color, palette),
20        anstyle::Color::Rgb(color) => 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) => anstyle::Ansi256Color::from_ansi(color),
31        anstyle::Color::Ansi256(color) => color,
32        anstyle::Color::Rgb(color) => 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) => color,
43        anstyle::Color::Ansi256(color) => xterm_to_ansi(color, palette),
44        anstyle::Color::Rgb(color) => 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) => 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 = XTERM_COLORS[color.0 as usize];
100            palette.find_match(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 = 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 = 16;
125    let mut best_distance = distance(color, XTERM_COLORS[best_index]);
126
127    let mut index = best_index + 1;
128    while index < XTERM_COLORS.len() {
129        let distance = distance(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 = c1.r() as i32;
144    let c1_g = c1.g() as i32;
145    let c1_b = c1.b() as i32;
146    let c2_r = c2.r() as i32;
147    let c2_g = c2.g() as i32;
148    let c2_b = c2.b() as i32;
149
150    let r_sum = c1_r + c2_r;
151    let r_delta = c1_r - c2_r;
152    let g_delta = c1_g - c2_g;
153    let b_delta = c1_b - c2_b;
154
155    let r = (2 * 512 + r_sum) * r_delta * r_delta;
156    let g = 4 * g_delta * g_delta * (1 << 8);
157    let b = (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
426#[doc = include_str!("../README.md")]
427#[cfg(doctest)]
428pub struct ReadmeDoctests;