svgparser/
color.rs

1// Copyright 2018 Evgeniy Reizner
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use std::str::FromStr;
10use std::cmp;
11
12use xmlparser::{
13    Stream,
14    StrSpan,
15    XmlByteExt,
16};
17
18use error::{
19    StreamError,
20    StreamResult,
21};
22use {
23    LengthUnit,
24    StreamExt,
25};
26use colors;
27
28/// Representation of the [`<color>`] type.
29///
30/// [`<color>`]: https://www.w3.org/TR/SVG/types.html#DataTypeColor
31#[derive(Copy, Clone, PartialEq, Debug)]
32#[allow(missing_docs)]
33pub struct Color {
34    pub red: u8,
35    pub green: u8,
36    pub blue: u8,
37}
38
39impl Color {
40    /// Constructs a new `Color` from `red`, `green` and `blue` values.
41    #[inline]
42    pub fn new(red: u8, green: u8, blue: u8) -> Color {
43        Color { red, green, blue }
44    }
45
46    /// Parses `Color` from `StrSpan`.
47    ///
48    /// Parsing is done according to [spec]:
49    ///
50    /// ```text
51    /// color    ::= "#" hexdigit hexdigit hexdigit (hexdigit hexdigit hexdigit)?
52    ///              | "rgb(" wsp* integer comma integer comma integer wsp* ")"
53    ///              | "rgb(" wsp* integer "%" comma integer "%" comma integer "%" wsp* ")"
54    ///              | color-keyword
55    /// hexdigit ::= [0-9A-Fa-f]
56    /// comma    ::= wsp* "," wsp*
57    /// ```
58    /// \* The SVG spec has an error. There should be `number`,
59    /// not an `integer` for percent values ([details]).
60    ///
61    /// # Errors
62    ///
63    ///  - Returns error if a color has an invalid format.
64    ///
65    ///  - Returns error if `color-keyword` or `rgb` prefix are in in the lowercase.
66    ///    It's not supported.
67    ///
68    ///  - Returns error if `<color>` is followed by `<icccolor>`.
69    ///    It's not supported.
70    ///
71    /// # Notes
72    ///
73    ///  - Any non-`hexdigit` bytes will be treated as `0`.
74    ///  - Allocates heap memory for case-insensitive named colors comparison.
75    ///
76    /// [spec]: http://www.w3.org/TR/SVG/types.html#DataTypeColor
77    /// [details]: https://lists.w3.org/Archives/Public/www-svg/2014Jan/0109.html
78    pub fn from_span(span: StrSpan) -> StreamResult<Color> {
79        let mut s = Stream::from_span(span);
80
81        s.skip_spaces();
82
83        let start = s.pos();
84
85        let mut color = Color::new(0, 0, 0);
86
87        if s.curr_byte()? == b'#' {
88            s.advance(1);
89            let color_str = s.consume_bytes(|_, c| c.is_xml_hex_digit()).to_str().as_bytes();
90            // get color data len until first space or stream end
91            match color_str.len() {
92                6 => {
93                    // #rrggbb
94                    color.red   = hex_pair(color_str[0], color_str[1]);
95                    color.green = hex_pair(color_str[2], color_str[3]);
96                    color.blue  = hex_pair(color_str[4], color_str[5]);
97                }
98                3 => {
99                    // #rgb
100                    color.red = short_hex(color_str[0]);
101                    color.green = short_hex(color_str[1]);
102                    color.blue = short_hex(color_str[2]);
103                }
104                _ => {
105                    return Err(StreamError::InvalidColor(s.gen_error_pos_from(start)));
106                }
107            }
108        } else if is_rgb(&s) {
109            s.advance(4);
110
111            let l = s.parse_list_length()?;
112
113            if l.unit == LengthUnit::Percent {
114                fn from_persent(v: f64) -> u8 {
115                    let d = 255.0 / 100.0;
116                    let n = (v * d).round() as i32;
117                    bound(0, n, 255) as u8
118                }
119
120                color.red = from_persent(l.num);
121                color.green = from_persent(s.parse_list_length()?.num);
122                color.blue = from_persent(s.parse_list_length()?.num);
123            } else {
124                color.red = bound(0, l.num as i32, 255) as u8;
125                color.green = bound(0, s.parse_list_integer()?, 255) as u8;
126                color.blue = bound(0, s.parse_list_integer()?, 255) as u8;
127            }
128
129            s.skip_spaces();
130            s.consume_byte(b')')?;
131        } else {
132            let name = s.consume_name()?.to_str().to_lowercase();
133            match colors::rgb_color_from_name(&name) {
134                Some(c) => {
135                    color = c;
136                }
137                None => {
138                    return Err(StreamError::InvalidColor(s.gen_error_pos_from(start)));
139                }
140            }
141        }
142
143        // Check that we are at the end of the stream. Otherwise color can be followed by icccolor,
144        // which is not supported.
145        s.skip_spaces();
146        if !s.at_end() {
147            return Err(StreamError::InvalidColor(s.gen_error_pos()));
148        }
149
150        Ok(color)
151    }
152}
153
154impl FromStr for Color {
155    type Err = StreamError;
156
157    fn from_str(text: &str) -> StreamResult<Self> {
158        Color::from_span(StrSpan::from_str(text))
159    }
160}
161
162#[inline]
163fn from_hex(c: u8) -> u8 {
164    match c {
165        b'0'...b'9' => c - b'0',
166        b'a'...b'f' => c - b'a' + 10,
167        b'A'...b'F' => c - b'A' + 10,
168        _ => b'0',
169    }
170}
171
172#[inline]
173fn short_hex(c: u8) -> u8 {
174    let h = from_hex(c);
175    (h << 4) | h
176}
177
178#[inline]
179fn hex_pair(c1: u8, c2: u8) -> u8 {
180    let h1 = from_hex(c1);
181    let h2 = from_hex(c2);
182    (h1 << 4) | h2
183}
184
185fn is_rgb(s: &Stream) -> bool {
186    let mut s = s.clone();
187    let prefix = s.consume_bytes(|_, c| c != b'(').to_str();
188    if s.consume_byte(b'(').is_err() {
189        return false;
190    }
191
192    #[allow(unused_imports)]
193    use std::ascii::AsciiExt;
194
195    prefix.eq_ignore_ascii_case("rgb")
196}
197
198#[inline]
199fn bound<T: Ord>(min: T, val: T, max: T) -> T {
200    cmp::max(min, cmp::min(max, val))
201}