1use crate::{Transform, TransformError, TransformerCategory};
2
3#[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 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 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 (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 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}