buup/transformers/
color_code_convert.rs

1use crate::{Transform, TransformError, TransformerCategory};
2
3/// Color Code Converter transformer
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct ColorCodeConvert;
6
7#[derive(Debug, Clone)]
8struct Color {
9    r: u8,
10    g: u8,
11    b: u8,
12    a: Option<u8>,
13}
14
15impl Color {
16    fn from_hex(hex: &str) -> Result<Self, TransformError> {
17        let hex = hex.trim_start_matches('#');
18        if hex.len() != 6 && hex.len() != 8 {
19            return Err(TransformError::InvalidArgument(
20                "Invalid hex color format".into(),
21            ));
22        }
23
24        let r = u8::from_str_radix(&hex[0..2], 16)
25            .map_err(|_| TransformError::InvalidArgument("Invalid hex color".into()))?;
26        let g = u8::from_str_radix(&hex[2..4], 16)
27            .map_err(|_| TransformError::InvalidArgument("Invalid hex color".into()))?;
28        let b = u8::from_str_radix(&hex[4..6], 16)
29            .map_err(|_| TransformError::InvalidArgument("Invalid hex color".into()))?;
30        let a = if hex.len() == 8 {
31            Some(
32                u8::from_str_radix(&hex[6..8], 16)
33                    .map_err(|_| TransformError::InvalidArgument("Invalid hex color".into()))?,
34            )
35        } else {
36            None
37        };
38
39        Ok(Color { r, g, b, a })
40    }
41
42    fn from_rgb(rgb: &str) -> Result<Self, TransformError> {
43        let rgb = rgb.trim_start_matches("rgb(").trim_end_matches(')');
44        let parts: Vec<&str> = rgb.split(',').map(|s| s.trim()).collect();
45
46        if parts.len() != 3 && parts.len() != 4 {
47            return Err(TransformError::InvalidArgument("Invalid RGB format".into()));
48        }
49
50        let r = parts[0]
51            .parse::<u8>()
52            .map_err(|_| TransformError::InvalidArgument("Invalid RGB value".into()))?;
53        let g = parts[1]
54            .parse::<u8>()
55            .map_err(|_| TransformError::InvalidArgument("Invalid RGB value".into()))?;
56        let b = parts[2]
57            .parse::<u8>()
58            .map_err(|_| TransformError::InvalidArgument("Invalid RGB value".into()))?;
59        let a = if parts.len() == 4 {
60            Some(
61                parts[3]
62                    .parse::<u8>()
63                    .map_err(|_| TransformError::InvalidArgument("Invalid RGB value".into()))?,
64            )
65        } else {
66            None
67        };
68
69        Ok(Color { r, g, b, a })
70    }
71
72    fn from_hsl(hsl: &str) -> Result<Self, TransformError> {
73        let hsl = hsl.trim_start_matches("hsl(").trim_end_matches(')');
74        let parts: Vec<&str> = hsl.split(',').map(|s| s.trim()).collect();
75
76        if parts.len() != 3 && parts.len() != 4 {
77            return Err(TransformError::InvalidArgument("Invalid HSL format".into()));
78        }
79
80        let h = parts[0]
81            .trim_end_matches("deg")
82            .parse::<f64>()
83            .map_err(|_| TransformError::InvalidArgument("Invalid HSL value".into()))?;
84        let s = parts[1]
85            .trim_end_matches('%')
86            .parse::<f64>()
87            .map_err(|_| TransformError::InvalidArgument("Invalid HSL value".into()))?
88            / 100.0;
89        let l = parts[2]
90            .trim_end_matches('%')
91            .parse::<f64>()
92            .map_err(|_| TransformError::InvalidArgument("Invalid HSL value".into()))?
93            / 100.0;
94        let a = if parts.len() == 4 {
95            Some(
96                (parts[3]
97                    .parse::<f64>()
98                    .map_err(|_| TransformError::InvalidArgument("Invalid HSL value".into()))?
99                    * 255.0) as u8,
100            )
101        } else {
102            None
103        };
104
105        // Convert HSL to RGB
106        let (r, g, b) = Self::hsl_to_rgb(h, s, l);
107        Ok(Color { r, g, b, a })
108    }
109
110    fn from_cmyk(cmyk: &str) -> Result<Self, TransformError> {
111        let cmyk = cmyk.trim_start_matches("cmyk(").trim_end_matches(')');
112        let parts: Vec<&str> = cmyk.split(',').map(|s| s.trim()).collect();
113
114        if parts.len() != 4 && parts.len() != 5 {
115            return Err(TransformError::InvalidArgument(
116                "Invalid CMYK format".into(),
117            ));
118        }
119
120        let c = parts[0]
121            .trim_end_matches('%')
122            .parse::<f64>()
123            .map_err(|_| TransformError::InvalidArgument("Invalid CMYK value".into()))?
124            / 100.0;
125        let m = parts[1]
126            .trim_end_matches('%')
127            .parse::<f64>()
128            .map_err(|_| TransformError::InvalidArgument("Invalid CMYK value".into()))?
129            / 100.0;
130        let y = parts[2]
131            .trim_end_matches('%')
132            .parse::<f64>()
133            .map_err(|_| TransformError::InvalidArgument("Invalid CMYK value".into()))?
134            / 100.0;
135        let k = parts[3]
136            .trim_end_matches('%')
137            .parse::<f64>()
138            .map_err(|_| TransformError::InvalidArgument("Invalid CMYK value".into()))?
139            / 100.0;
140        let a = if parts.len() == 5 {
141            Some(
142                (parts[4]
143                    .parse::<f64>()
144                    .map_err(|_| TransformError::InvalidArgument("Invalid CMYK value".into()))?
145                    * 255.0) as u8,
146            )
147        } else {
148            None
149        };
150
151        // Convert CMYK to RGB
152        let r = ((1.0 - c) * (1.0 - k) * 255.0) as u8;
153        let g = ((1.0 - m) * (1.0 - k) * 255.0) as u8;
154        let b = ((1.0 - y) * (1.0 - k) * 255.0) as u8;
155
156        Ok(Color { r, g, b, a })
157    }
158
159    fn to_hex(&self) -> String {
160        if let Some(a) = self.a {
161            format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, a)
162        } else {
163            format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
164        }
165    }
166
167    fn to_rgb(&self) -> String {
168        if let Some(a) = self.a {
169            format!("rgb({},{},{},{})", self.r, self.g, self.b, a)
170        } else {
171            format!("rgb({},{},{})", self.r, self.g, self.b)
172        }
173    }
174
175    fn to_hsl(&self) -> String {
176        let (h, s, l) = Self::rgb_to_hsl(self.r, self.g, self.b);
177        if let Some(a) = self.a {
178            format!(
179                "hsl({:.0}deg,{:.0}%,{:.0}%,{:.2})",
180                h,
181                s * 100.0,
182                l * 100.0,
183                a as f64 / 255.0
184            )
185        } else {
186            format!("hsl({:.0}deg,{:.0}%,{:.0}%)", h, s * 100.0, l * 100.0)
187        }
188    }
189
190    fn to_cmyk(&self) -> String {
191        let (c, m, y, k) = Self::rgb_to_cmyk(self.r, self.g, self.b);
192        if let Some(a) = self.a {
193            format!(
194                "cmyk({:.0}%,{:.0}%,{:.0}%,{:.0}%,{:.2})",
195                c * 100.0,
196                m * 100.0,
197                y * 100.0,
198                k * 100.0,
199                a as f64 / 255.0
200            )
201        } else {
202            format!(
203                "cmyk({:.0}%,{:.0}%,{:.0}%,{:.0}%)",
204                c * 100.0,
205                m * 100.0,
206                y * 100.0,
207                k * 100.0
208            )
209        }
210    }
211
212    fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (u8, u8, u8) {
213        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
214        let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
215        let m = l - c / 2.0;
216
217        let (r, g, b) = match (h / 60.0) as u8 {
218            0 => (c, x, 0.0),
219            1 => (x, c, 0.0),
220            2 => (0.0, c, x),
221            3 => (0.0, x, c),
222            4 => (x, 0.0, c),
223            _ => (c, 0.0, x),
224        };
225
226        (
227            ((r + m) * 255.0) as u8,
228            ((g + m) * 255.0) as u8,
229            ((b + m) * 255.0) as u8,
230        )
231    }
232
233    fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f64, f64, f64) {
234        let r = r as f64 / 255.0;
235        let g = g as f64 / 255.0;
236        let b = b as f64 / 255.0;
237
238        let max = r.max(g).max(b);
239        let min = r.min(g).min(b);
240        let l = (max + min) / 2.0;
241
242        let s = if max == min {
243            0.0
244        } else if l <= 0.5 {
245            (max - min) / (max + min)
246        } else {
247            (max - min) / (2.0 - max - min)
248        };
249
250        let h = if max == min {
251            0.0
252        } else if max == r {
253            60.0 * ((g - b) / (max - min))
254        } else if max == g {
255            60.0 * (2.0 + (b - r) / (max - min))
256        } else {
257            60.0 * (4.0 + (r - g) / (max - min))
258        };
259
260        (h.rem_euclid(360.0), s, l)
261    }
262
263    fn rgb_to_cmyk(r: u8, g: u8, b: u8) -> (f64, f64, f64, f64) {
264        let r = r as f64 / 255.0;
265        let g = g as f64 / 255.0;
266        let b = b as f64 / 255.0;
267
268        let k = 1.0 - r.max(g).max(b);
269        if (k - 1.0).abs() < f64::EPSILON {
270            // Black
271            (0.0, 0.0, 0.0, 1.0)
272        } else {
273            let c = (1.0 - r - k) / (1.0 - k);
274            let m = (1.0 - g - k) / (1.0 - k);
275            let y = (1.0 - b - k) / (1.0 - k);
276            (c, m, y, k)
277        }
278    }
279}
280
281impl Transform for ColorCodeConvert {
282    fn name(&self) -> &'static str {
283        "Color Code Converter"
284    }
285
286    fn id(&self) -> &'static str {
287        "color_code_convert"
288    }
289
290    fn category(&self) -> TransformerCategory {
291        TransformerCategory::Other
292    }
293
294    fn description(&self) -> &'static str {
295        "Converts between different color formats (HEX, RGB, HSL, CMYK)"
296    }
297
298    fn transform(&self, input: &str) -> Result<String, TransformError> {
299        let input = input.trim();
300        let color = if input.starts_with('#') {
301            Color::from_hex(input)?
302        } else if input.starts_with("rgb(") {
303            Color::from_rgb(input)?
304        } else if input.starts_with("hsl(") {
305            Color::from_hsl(input)?
306        } else if input.starts_with("cmyk(") {
307            Color::from_cmyk(input)?
308        } else {
309            return Err(TransformError::InvalidArgument(
310                "Unsupported color format".into(),
311            ));
312        };
313
314        // Convert to all formats
315        Ok(format!(
316            "HEX: {}\nRGB: {}\nHSL: {}\nCMYK: {}",
317            color.to_hex(),
318            color.to_rgb(),
319            color.to_hsl(),
320            color.to_cmyk()
321        ))
322    }
323
324    fn default_test_input(&self) -> &'static str {
325        "#FF0000"
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_hex_conversion() {
335        let transformer = ColorCodeConvert;
336        let result = transformer.transform("#FF0000").unwrap();
337        assert!(result.contains("HEX: #ff0000"));
338        assert!(result.contains("RGB: rgb(255,0,0)"));
339        assert!(result.contains("HSL: hsl(0deg,100%,50%)"));
340        assert!(result.contains("CMYK: cmyk(0%,100%,100%,0%)"));
341    }
342
343    #[test]
344    fn test_rgb_conversion() {
345        let transformer = ColorCodeConvert;
346        let result = transformer.transform("rgb(0, 255, 0)").unwrap();
347        assert!(result.contains("HEX: #00ff00"));
348        assert!(result.contains("RGB: rgb(0,255,0)"));
349        assert!(result.contains("HSL: hsl(120deg,100%,50%)"));
350        assert!(result.contains("CMYK: cmyk(100%,0%,100%,0%)"));
351    }
352
353    #[test]
354    fn test_hsl_conversion() {
355        let transformer = ColorCodeConvert;
356        let result = transformer.transform("hsl(240deg, 100%, 50%)").unwrap();
357        assert!(result.contains("HEX: #0000ff"));
358        assert!(result.contains("RGB: rgb(0,0,255)"));
359        assert!(result.contains("HSL: hsl(240deg,100%,50%)"));
360        assert!(result.contains("CMYK: cmyk(100%,100%,0%,0%)"));
361    }
362
363    #[test]
364    fn test_cmyk_conversion() {
365        let transformer = ColorCodeConvert;
366        let result = transformer.transform("cmyk(0%, 0%, 0%, 100%)").unwrap();
367        println!("CMYK conversion result: {}", result);
368        assert!(result.contains("HEX: #000000"));
369        assert!(result.contains("RGB: rgb(0,0,0)"));
370        assert!(result.contains("HSL: hsl(0deg,0%,0%)"));
371        assert!(result.contains("CMYK: cmyk(0%,0%,0%,100%)"));
372    }
373
374    #[test]
375    fn test_invalid_input() {
376        let transformer = ColorCodeConvert;
377        assert!(transformer.transform("invalid").is_err());
378        assert!(transformer.transform("#GG0000").is_err());
379        assert!(transformer.transform("rgb(300,0,0)").is_err());
380    }
381}