esvg/
convert.rs

1//! A collection of useful conversion functions
2use std::f64;
3use std::f64::consts::PI;
4use std::str::FromStr;
5
6use crate::error;
7
8/// convert value of inches to pixels given the dpi
9pub fn inches_to_pixels(value: f64, dpi: i32) -> i32 {
10    (value * (dpi as f64)).round() as i32
11}
12
13/// convert value of centimeters to pixels given the dpi
14pub fn cm_to_pixels(value: f64, dpi: i32) -> i32 {
15    inches_to_pixels(cm_to_inches(value), dpi)
16}
17
18/// convert value of millimeters to pixels given the dpi
19pub fn mm_to_pixels(value: f64, dpi: i32) -> i32 {
20    inches_to_pixels(mm_to_inches(value), dpi)
21}
22
23/// Convert centimeters to inches
24pub fn cm_to_inches(value: f64) -> f64 {
25    (value * 10.0) / 25.4
26}
27
28/// Convert millimeters to inches
29pub fn mm_to_inches(value: f64) -> f64 {
30    (value) / 25.4
31}
32
33/// Convert inches to millimeters
34pub fn inches_to_mm(value: f64) -> f64 {
35    value * 25.4
36}
37
38/// Convert inches to centimeters
39pub fn inches_to_cm(value: f64) -> f64 {
40    (value * 25.4) / 10.0
41}
42
43/// Convert a number of pixels to millimeters given the dpi
44pub fn pixels_to_mm(value: i32, dpi: i32) -> f64 {
45    inches_to_mm(value as f64 / dpi as f64)
46}
47
48/// Convert a number of pixels to centimeters given the dpi
49pub fn pixels_to_cm(value: i32, dpi: i32) -> f64 {
50    inches_to_cm(value as f64 / dpi as f64)
51}
52
53/// Convert a number of pixels to inches given the dpi
54pub fn pixels_to_inches(value: i32, dpi: i32) -> f64 {
55    value as f64 / dpi as f64
56}
57
58/// Parse a length. Handles unit suffixes
59///
60/// ```
61/// use esvg::convert::parse_length;
62/// assert_eq!(parse_length("27in", 96).unwrap(), 2592);
63/// ```
64pub fn parse_length(value: &str, dpi: i32) -> Result<i32, error::Error> {
65    let l = value.len();
66
67    match extract_unit(value)? {
68        "mm" => {
69            let mm = f64::from_str(&value[..l - 2])?;
70            Ok(mm_to_pixels(mm, dpi))
71        }
72        "cm" => {
73            let cm = f64::from_str(&value[..l - 2])?;
74            Ok(cm_to_pixels(cm, dpi))
75        }
76        "in" => {
77            let inches = f64::from_str(&value[..l - 2])?;
78            Ok(inches_to_pixels(inches, dpi))
79        }
80        "px" => {
81            let px = i32::from_str(&value[..l - 2])?;
82            Ok(px)
83        }
84        _ => {
85            let inches = f64::from_str(value)?;
86            Ok(inches_to_pixels(inches, dpi))
87        }
88    }
89}
90
91/// Convert a pixel length into a specified unit.
92/// Supports "mm", "cm", "in", and "px" values for units
93pub fn px_to_length(value: i32, unit: &str, dpi: i32) -> Result<String, error::Error> {
94    match unit {
95        "mm" => {
96            let mm = pixels_to_mm(value, dpi);
97            Ok(format!("{mm:.2}mm"))
98        }
99        "cm" => {
100            let cm = pixels_to_cm(value, dpi);
101            Ok(format!("{cm:.2}cm"))
102        }
103        "in" => {
104            let inches = pixels_to_inches(value, dpi);
105            Ok(format!("{inches:.2}in"))
106        }
107        "px" => Ok(format!("{value}px")),
108        _ => {
109            let inches = pixels_to_inches(value, dpi);
110            Ok(format!("{inches:.2}in"))
111        }
112    }
113}
114
115/// get the unit suffix from a string, if it has one.
116pub fn extract_unit(value: &str) -> Result<&str, error::Error> {
117    let l = value.len();
118    if l > 2 {
119        if value[l - 2..].chars().any(char::is_numeric) {
120            Ok("")
121        } else {
122            Ok(&value[l - 2..])
123        }
124    } else {
125        Ok("")
126    }
127}
128
129/// Parse an angle in degrees into radians
130pub fn parse_angle(value: &str) -> Result<f64, error::Error> {
131    let angle = f64::from_str(value)?;
132    if !(0.0..=360.0).contains(&angle) {
133        Err(error::Error::AngleOutOfRange(angle))
134    } else {
135        Ok(angle.to_radians())
136    }
137}
138
139/// Parse a hex string style colour into an R, G, B, A tuple between 0 and 1
140/// If no alpha channel is provided then this will assume 1.0
141/// Note: Does not support three character hex codes
142///
143/// ```
144/// let (r, g, b, a) = esvg::convert::parse_colour("#FF00AA33").unwrap();
145/// assert_eq!(r, 1.0);
146/// assert_eq!(g, 0.0);
147/// assert_eq!(b, 0.6666666666666666);
148/// assert_eq!(a, 0.2);
149/// ```
150pub fn parse_colour(value: &str) -> Result<(f64, f64, f64, f64), error::Error> {
151    if value.len() < 6 {
152        return Err(error::Error::ColourError(value.to_string()));
153    }
154    let mut start = 0;
155    if value.starts_with('#') {
156        start = 1;
157    }
158
159    let red = i32::from_str_radix(&value[start..start + 2], 16)?;
160    let green = i32::from_str_radix(&value[start + 2..start + 4], 16)?;
161    let blue = i32::from_str_radix(&value[start + 4..start + 6], 16)?;
162    let mut alpha = 255;
163    if value.len() > start + 6 {
164        alpha = i32::from_str_radix(&value[start + 6..], 16)?;
165    }
166
167    Ok((
168        red as f64 / 255.0,
169        green as f64 / 255.0,
170        blue as f64 / 255.0,
171        alpha as f64 / 255.0,
172    ))
173}
174
175/// 30 degrees as radians
176pub const DEG_30: f64 = 30.0 * (PI / 180.0);
177/// 45 degrees as radians
178pub const DEG_45: f64 = 45.0 * (PI / 180.0);
179/// 60 degrees as radians
180pub const DEG_60: f64 = 60.0 * (PI / 180.0);
181/// 90 degrees as radians
182pub const DEG_90: f64 = 90.0 * (PI / 180.0);
183/// 120 degrees as radians
184pub const DEG_120: f64 = 120.0 * (PI / 180.0);
185/// 180 degrees as radians
186pub const DEG_180: f64 = 180.0 * (PI / 180.0);
187/// 270 degrees as radians
188pub const DEG_270: f64 = 270.0 * (PI / 180.0);
189/// 360 degrees as radians
190pub const DEG_360: f64 = 360.0 * (PI / 180.0);
191
192#[cfg(test)]
193mod tests {
194
195    use super::parse_colour;
196    #[test]
197    pub fn colour_conversion_invalid() {
198        assert!(parse_colour("invalid").is_err());
199        assert!(parse_colour("i").is_err());
200        assert!(parse_colour("#i").is_err());
201    }
202
203    #[test]
204    pub fn colour_conversion_valid() {
205        let (r, g, b, a) = parse_colour("#0c0c0c").unwrap();
206        assert_eq!(r, 0.047058823529411764);
207        assert_eq!(g, 0.047058823529411764);
208        assert_eq!(b, 0.047058823529411764);
209        assert_eq!(a, 1.0);
210    }
211}