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
25use std::str;
26use std::error;
27use std::fmt;
28use std::num;
29use std::str::FromStr;
30
31use 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)]
37pub 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
48impl 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)]
60pub struct ColorParseError;
61
62impl From<num::ParseIntError> for ColorParseError {
63 #[allow(unused_variables)]
64 fn from(err: num::ParseIntError) -> ColorParseError {
65 return ColorParseError;
66 }
67}
68
69impl From<num::ParseFloatError> for ColorParseError {
70 #[allow(unused_variables)]
71 fn from(err: num::ParseFloatError) -> ColorParseError {
72 return ColorParseError;
73 }
74}
75
76impl 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
86impl 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
96impl 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
199fn 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
216fn 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
238fn 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
256fn 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.
300fn 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.
316fn 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.
332fn 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
343fn 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
358fn 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
364fn 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