1 | // (c) Dean McNamee <dean@gmail.com>, 2012. |
2 | // (c) Rust port by Katkov Oleksandr <alexx.katkoff@gmail.com>, 2016. |
3 | // |
4 | // https://github.com/deanm/css-color-parser-js |
5 | // https://github.com/7thSigil/css-color-parser-rs |
6 | // |
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy |
8 | // of this software and associated documentation files (the "Software"), to |
9 | // deal in the Software without restriction, including without limitation the |
10 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or |
11 | // sell copies of the Software, and to permit persons to whom the Software is |
12 | // furnished to do so, subject to the following conditions: |
13 | // |
14 | // The above copyright notice and this permission notice shall be included in |
15 | // all copies or substantial portions of the Software. |
16 | // |
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
22 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
23 | // IN THE SOFTWARE. |
24 | |
25 | use std::str; |
26 | use std::error; |
27 | use std::fmt; |
28 | use std::num; |
29 | use std::str::FromStr; |
30 | |
31 | use crate::color::named_colors::NAMED_COLORS; |
32 | |
33 | /// Color in rgba format, |
34 | /// where {red,green,blue} in 0..255, |
35 | /// alpha in 0.0..1.0 |
36 | #[derive (Copy, Clone, Debug, PartialEq)] |
37 | pub struct Color { |
38 | /// red channel, ranges from 0 to 255 |
39 | pub r: u8, |
40 | /// green channel, ranges from 0 to 255 |
41 | pub g: u8, |
42 | /// blue channel, ranges from 0 to 255 |
43 | pub b: u8, |
44 | /// alpha channel, ranges from 0.0 to 1.0 |
45 | pub a: f32, |
46 | } |
47 | |
48 | impl fmt::Display for Color { |
49 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
50 | write!(f, |
51 | "Color(r: {}, g: {}, b: {}, a: {})" , |
52 | self.r, |
53 | self.g, |
54 | self.b, |
55 | self.a) |
56 | } |
57 | } |
58 | |
59 | #[derive (Debug)] |
60 | pub struct ColorParseError; |
61 | |
62 | impl From<num::ParseIntError> for ColorParseError { |
63 | #[allow (unused_variables)] |
64 | fn from(err: num::ParseIntError) -> ColorParseError { |
65 | return ColorParseError; |
66 | } |
67 | } |
68 | |
69 | impl From<num::ParseFloatError> for ColorParseError { |
70 | #[allow (unused_variables)] |
71 | fn from(err: num::ParseFloatError) -> ColorParseError { |
72 | return ColorParseError; |
73 | } |
74 | } |
75 | |
76 | impl error::Error for ColorParseError { |
77 | fn description(&self) -> &str { |
78 | "Failed to parse color" |
79 | } |
80 | |
81 | fn cause(&self) -> Option<&dyn error::Error> { |
82 | None |
83 | } |
84 | } |
85 | |
86 | impl fmt::Display for ColorParseError { |
87 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
88 | write!(f, "ColorParseError: Invalid format" ) |
89 | } |
90 | } |
91 | |
92 | // TODO(7thSigil): check if platform byte order affects parsing |
93 | // TODO(7thSigil): maybe rewrite error handling into something more informative? |
94 | /// Parses CSS3 color strings into rgba Color. |
95 | /// Handles all errors to avoid any panic!s |
96 | impl str::FromStr for Color { |
97 | type Err = ColorParseError; |
98 | |
99 | fn from_str(s: &str) -> Result<Self, ColorParseError> { |
100 | let s = s.trim(); |
101 | if s.is_empty() { |
102 | return Err(ColorParseError); |
103 | } |
104 | |
105 | // Remove all whitespace, not compliant, but should just be more accepting. |
106 | let mut string = s.replace(' ' , "" ); |
107 | string.make_ascii_lowercase(); |
108 | |
109 | if let Some(&color) = NAMED_COLORS.get(&*string) { |
110 | return Ok(color); |
111 | } |
112 | |
113 | if string.starts_with("#" ) { |
114 | let string_char_count = string.chars().count(); |
115 | |
116 | if string_char_count == 4 { |
117 | let (_, value_string) = string.split_at(1); |
118 | |
119 | let iv = u64::from_str_radix(value_string, 16)?; |
120 | |
121 | // unlike original js code, NaN is impossible () |
122 | if !(iv <= 0xfff) { |
123 | return Err(ColorParseError); |
124 | } |
125 | |
126 | return Ok(Color { |
127 | r: (((iv & 0xf00) >> 4) | ((iv & 0xf00) >> 8)) as u8, |
128 | g: ((iv & 0xf0) | ((iv & 0xf0) >> 4)) as u8, |
129 | b: ((iv & 0xf) | ((iv & 0xf) << 4)) as u8, |
130 | a: 1.0, |
131 | }); |
132 | } else if string_char_count == 7 { |
133 | let (_, value_string) = string.split_at(1); |
134 | |
135 | let iv = u64::from_str_radix(value_string, 16)?; |
136 | |
137 | // (7thSigil) unlike original js code, NaN is impossible |
138 | if !(iv <= 0xffffff) { |
139 | return Err(ColorParseError); |
140 | } |
141 | |
142 | return Ok(Color { |
143 | r: ((iv & 0xff0000) >> 16) as u8, |
144 | g: ((iv & 0xff00) >> 8) as u8, |
145 | b: (iv & 0xff) as u8, |
146 | a: 1.0, |
147 | }); |
148 | } |
149 | |
150 | return Err(ColorParseError); |
151 | } |
152 | |
153 | let op = string.find("(" ).ok_or(ColorParseError)?; |
154 | let ep = string.find(")" ).ok_or(ColorParseError)?; |
155 | |
156 | // (7thSigil) validating format |
157 | // ')' bracket should be at the end |
158 | // and always after the opening bracket |
159 | if (ep + 1) != string.len() || ep < op { |
160 | return Err(ColorParseError); |
161 | } |
162 | |
163 | // (7thSigil) extracting format |
164 | let (fmt, right_string_half) = string.split_at(op); |
165 | |
166 | // (7thSigil) validating format |
167 | if fmt.is_empty() { |
168 | return Err(ColorParseError); |
169 | } |
170 | |
171 | // removing brackets |
172 | let mut filtered_right_string_half = right_string_half.to_string(); |
173 | |
174 | // removing brackets |
175 | filtered_right_string_half.remove(0); |
176 | filtered_right_string_half.pop(); |
177 | |
178 | let params: Vec<&str> = filtered_right_string_half.split("," ).collect(); |
179 | |
180 | // (7thSigil) validating format |
181 | if params.len() < 3 || params.len() > 4 { |
182 | return Err(ColorParseError); |
183 | } |
184 | |
185 | if fmt == "rgba" { |
186 | return parse_rgba(params); |
187 | } else if fmt == "rgb" { |
188 | return parse_rgb(params); |
189 | } else if fmt == "hsla" { |
190 | return parse_hsla(params); |
191 | } else if fmt == "hsl" { |
192 | return parse_hsl(params); |
193 | } |
194 | |
195 | return Err(ColorParseError); |
196 | } |
197 | } |
198 | |
199 | fn parse_rgba(mut rgba: Vec<&str>) -> Result<Color, ColorParseError> { |
200 | |
201 | if rgba.len() != 4 { |
202 | return Err(ColorParseError); |
203 | } |
204 | |
205 | let a_str: &str = rgba.pop().ok_or(err:ColorParseError)?; |
206 | |
207 | let a: f32 = parse_css_float(fv_str:a_str)?; |
208 | |
209 | let mut rgb_color: Color = parse_rgb(rgba)?; |
210 | |
211 | rgb_color = Color { a: a, ..rgb_color }; |
212 | |
213 | return Ok(rgb_color); |
214 | } |
215 | |
216 | fn parse_rgb(mut rgb: Vec<&str>) -> Result<Color, ColorParseError> { |
217 | |
218 | if rgb.len() != 3 { |
219 | return Err(ColorParseError); |
220 | } |
221 | |
222 | let b_str: &str = rgb.pop().ok_or(err:ColorParseError)?; |
223 | let g_str: &str = rgb.pop().ok_or(err:ColorParseError)?; |
224 | let r_str: &str = rgb.pop().ok_or(err:ColorParseError)?; |
225 | |
226 | let r: u8 = parse_css_int(iv_or_percentage_str:r_str)?; |
227 | let g: u8 = parse_css_int(iv_or_percentage_str:g_str)?; |
228 | let b: u8 = parse_css_int(iv_or_percentage_str:b_str)?; |
229 | |
230 | return Ok(Color { |
231 | r: r, |
232 | g: g, |
233 | b: b, |
234 | a: 1.0, |
235 | }); |
236 | } |
237 | |
238 | fn parse_hsla(mut hsla: Vec<&str>) -> Result<Color, ColorParseError> { |
239 | |
240 | if hsla.len() != 4 { |
241 | return Err(ColorParseError); |
242 | } |
243 | |
244 | let a_str: &str = hsla.pop().ok_or(err:ColorParseError)?; |
245 | |
246 | let a: f32 = parse_css_float(fv_str:a_str)?; |
247 | |
248 | // (7thSigil) Parsed from hsl to rgb representation |
249 | let mut rgb_color: Color = parse_hsl(hsla)?; |
250 | |
251 | rgb_color = Color { a: a, ..rgb_color }; |
252 | |
253 | return Ok(rgb_color); |
254 | } |
255 | |
256 | fn parse_hsl(mut hsl: Vec<&str>) -> Result<Color, ColorParseError> { |
257 | |
258 | if hsl.len() != 3 { |
259 | return Err(ColorParseError); |
260 | } |
261 | |
262 | let l_str = hsl.pop().ok_or(ColorParseError)?; |
263 | let s_str = hsl.pop().ok_or(ColorParseError)?; |
264 | let h_str = hsl.pop().ok_or(ColorParseError)?; |
265 | |
266 | let mut h = f32::from_str(h_str)?; |
267 | |
268 | // 0 .. 1 |
269 | h = (((h % 360.0) + 360.0) % 360.0) / 360.0; |
270 | |
271 | // NOTE(deanm): According to the CSS spec s/l should only be |
272 | // percentages, but we don't bother and let float or percentage. |
273 | |
274 | let s = parse_css_float(s_str)?; |
275 | let l = parse_css_float(l_str)?; |
276 | |
277 | let m2: f32; |
278 | |
279 | if l <= 0.5 { |
280 | m2 = l * (s + 1.0) |
281 | } else { |
282 | m2 = l + s - l * s; |
283 | } |
284 | |
285 | let m1 = l * 2.0 - m2; |
286 | |
287 | let r = clamp_css_byte_from_float(css_hue_to_rgb(m1, m2, h + 1.0 / 3.0) * 255.0); |
288 | let g = clamp_css_byte_from_float(css_hue_to_rgb(m1, m2, h) * 255.0); |
289 | let b = clamp_css_byte_from_float(css_hue_to_rgb(m1, m2, h - 1.0 / 3.0) * 255.0); |
290 | |
291 | return Ok(Color { |
292 | r: r, |
293 | g: g, |
294 | b: b, |
295 | a: 1.0, |
296 | }); |
297 | } |
298 | |
299 | // float or percentage. |
300 | fn parse_css_float(fv_str: &str) -> Result<f32, num::ParseFloatError> { |
301 | |
302 | let fv: f32; |
303 | |
304 | if fv_str.ends_with("%" ) { |
305 | let mut percentage_string: String = fv_str.to_string(); |
306 | percentage_string.pop(); |
307 | fv = f32::from_str(&percentage_string)?; |
308 | return Ok(clamp_css_float(fv:fv / 100.0)); |
309 | } |
310 | |
311 | fv = f32::from_str(fv_str)?; |
312 | return Ok(clamp_css_float(fv)); |
313 | } |
314 | |
315 | // int or percentage. |
316 | fn parse_css_int(iv_or_percentage_str: &str) -> Result<u8, ColorParseError> { |
317 | if iv_or_percentage_str.ends_with("%" ) { |
318 | |
319 | let mut percentage_string: String = iv_or_percentage_str.to_string(); |
320 | percentage_string.pop(); |
321 | let fv: f32 = f32::from_str(&percentage_string)?; |
322 | // Seems to be what Chrome does (round vs truncation). |
323 | return Ok(clamp_css_byte_from_float(fv:fv / 100.0 * 255.0)); |
324 | } |
325 | |
326 | let iv: u32 = u32::from_str(iv_or_percentage_str)?; |
327 | |
328 | return Ok(clamp_css_byte(iv)); |
329 | } |
330 | |
331 | // Clamp to float 0.0 .. 1.0. |
332 | fn clamp_css_float(fv: f32) -> f32 { |
333 | // return fv < 0 ? 0 : fv > 1 ? 1 : fv; |
334 | if fv < 0.0 { |
335 | 0.0 |
336 | } else if fv > 1.0 { |
337 | 1.0 |
338 | } else { |
339 | fv |
340 | } |
341 | } |
342 | |
343 | fn clamp_css_byte_from_float(mut fv: f32) -> u8 { |
344 | // Clamp to integer 0 .. 255. |
345 | // Seems to be what Chrome does (vs truncation). |
346 | fv = fv.round(); |
347 | |
348 | // return iv < 0 ? 0 : iv > 255 ? 255 : iv; |
349 | if fv < 0.0 { |
350 | 0 |
351 | } else if fv > 255.0 { |
352 | 255 |
353 | } else { |
354 | fv as u8 |
355 | } |
356 | } |
357 | |
358 | fn clamp_css_byte(iv: u32) -> u8 { |
359 | // Clamp to integer 0 .. 255. |
360 | // return iv < 0 ? 0 : iv > 255 ? 255 : iv; |
361 | if iv > 255 { 255 } else { iv as u8 } |
362 | } |
363 | |
364 | fn css_hue_to_rgb(m1: f32, m2: f32, mut h: f32) -> f32 { |
365 | if h < 0.0 { |
366 | h += 1.0; |
367 | } else if h > 1.0 { |
368 | h -= 1.0; |
369 | } |
370 | |
371 | if h * 6.0 < 1.0 { |
372 | return m1 + (m2 - m1) * h * 6.0; |
373 | } |
374 | if h * 2.0 < 1.0 { |
375 | return m2; |
376 | } |
377 | if h * 3.0 < 2.0 { |
378 | return m1 + (m2 - m1) * (2.0 / 3.0 - h) * 6.0; |
379 | } |
380 | |
381 | return m1; |
382 | } |
383 | |