1 //! `svg_metadata` is a Rust crate for parsing metadata information of SVG files.
2 //! In can be useful for getting information from SVG graphics without using
3 //! a full-blown parser.
4 //!
5 //! As such, it has a very narrow scope and only provides access to the fields
6 //! defined below.
7 
8 #[macro_use]
9 extern crate lazy_static;
10 
11 use regex::Regex;
12 use roxmltree::Document;
13 use std::convert::{AsRef, TryFrom};
14 use std::fs;
15 use std::path::PathBuf;
16 
17 mod error;
18 use crate::error::MetadataError;
19 
20 lazy_static! {
21     // Initialize the regex to split a list of elements in the viewBox
22     static ref VBOX_ELEMENTS: Regex = Regex::new(r",?\s+").unwrap();
23 
24     // Extract dimension information (e.g. 100em)
25     static ref DIMENSION: Regex = Regex::new(r"([\+|-]?\d+\.?\d*)(\D\D?)?").unwrap();
26 }
27 
28 #[derive(Debug, PartialEq, Copy, Clone)]
29 /// Specifies the dimensions of an SVG image.
30 pub struct ViewBox {
31     pub min_x: f64,
32     pub min_y: f64,
33     pub width: f64,
34     pub height: f64,
35 }
36 
37 #[derive(Debug, PartialEq, Copy, Clone)]
38 /// Supported units for dimensions
39 pub enum Unit {
40     /// The default font size - usually the height of a character.
41     Em,
42     /// The height of the character x
43     Ex,
44     /// Pixels
45     Px,
46     /// Points (1 / 72 of an inch)
47     Pt,
48     /// Picas (1 / 6 of an inch)
49     Pc,
50     /// Centimeters
51     Cm,
52     /// Millimeters
53     Mm,
54     /// Inches
55     In,
56     /// Percent
57     Percent,
58 }
59 
60 impl TryFrom<&str> for Unit {
61     type Error = MetadataError;
try_from(s: &str) -> Result<Unit, MetadataError>62     fn try_from(s: &str) -> Result<Unit, MetadataError> {
63         let unit = match s.to_lowercase().as_ref() {
64             "em" => Unit::Em,
65             "ex" => Unit::Ex,
66             "px" => Unit::Px,
67             "pt" => Unit::Pt,
68             "pc" => Unit::Pc,
69             "cm" => Unit::Cm,
70             "mm" => Unit::Mm,
71             "in" => Unit::In,
72             "%" => Unit::Percent,
73             _ => return Err(MetadataError::new(&format!("Unknown unit: {}", s))),
74         };
75         Ok(unit)
76     }
77 }
78 
79 #[derive(Debug, PartialEq, Copy, Clone)]
80 /// Specifies the width of an SVG image.
81 pub struct Width {
82     pub width: f64,
83     pub unit: Unit,
84 }
85 
parse_dimension(s: &str) -> Result<(f64, Unit), MetadataError>86 fn parse_dimension(s: &str) -> Result<(f64, Unit), MetadataError> {
87     let caps = DIMENSION
88         .captures(s)
89         .ok_or_else(|| MetadataError::new("Cannot read dimensions"))?;
90 
91     let val: &str = caps
92         .get(1)
93         .ok_or_else(|| MetadataError::new("No width specified"))?
94         .as_str();
95     let unit = caps.get(2).map_or("em", |m| m.as_str());
96 
97     Ok((val.parse::<f64>()?, Unit::try_from(unit)?))
98 }
99 
100 impl TryFrom<&str> for Width {
101     type Error = MetadataError;
try_from(s: &str) -> Result<Width, MetadataError>102     fn try_from(s: &str) -> Result<Width, MetadataError> {
103         let (width, unit) = parse_dimension(s)?;
104         Ok(Width { width, unit })
105     }
106 }
107 
108 #[derive(Debug, PartialEq, Copy, Clone)]
109 /// Specifies the height of an SVG image.
110 pub struct Height {
111     pub height: f64,
112     pub unit: Unit,
113 }
114 
115 impl TryFrom<&str> for Height {
116     type Error = MetadataError;
try_from(s: &str) -> Result<Height, MetadataError>117     fn try_from(s: &str) -> Result<Height, MetadataError> {
118         let (height, unit) = parse_dimension(s)?;
119         Ok(Height { height, unit })
120     }
121 }
122 
123 impl TryFrom<&str> for ViewBox {
124     type Error = MetadataError;
try_from(s: &str) -> Result<ViewBox, MetadataError>125     fn try_from(s: &str) -> Result<ViewBox, MetadataError> {
126         let elem: Vec<&str> = VBOX_ELEMENTS.split(s).collect();
127 
128         if elem.len() != 4 {
129             return Err(MetadataError::new(&format!(
130                 "Invalid view_box: Expected four elements, got {}",
131                 elem.len()
132             )));
133         }
134         let min_x = elem[0].parse::<f64>()?;
135         let min_y = elem[1].parse::<f64>()?;
136         let width = elem[2].parse::<f64>()?;
137         let height = elem[3].parse::<f64>()?;
138 
139         Ok(ViewBox {
140             min_x,
141             min_y,
142             width,
143             height,
144         })
145     }
146 }
147 
148 #[derive(Debug, PartialEq, Copy, Clone)]
149 /// Contains all metadata that was
150 /// extracted from an SVG image.
151 pub struct Metadata {
152     pub view_box: Option<ViewBox>,
153     pub width: Option<Width>,
154     pub height: Option<Height>,
155 }
156 
157 impl Metadata {
158     /// Parse an SVG file and extract metadata from it.
parse_file<T: Into<PathBuf>>(path: T) -> Result<Metadata, MetadataError>159     pub fn parse_file<T: Into<PathBuf>>(path: T) -> Result<Metadata, MetadataError> {
160         let input = fs::read_to_string(path.into())?;
161         Self::parse(input)
162     }
163 
164     /// Parse SVG data and extract metadata from it.
parse<T: AsRef<str>>(input: T) -> Result<Metadata, MetadataError>165     pub fn parse<T: AsRef<str>>(input: T) -> Result<Metadata, MetadataError> {
166         let doc = Document::parse(input.as_ref())?;
167         let svg_elem = doc.root_element();
168         let view_box = match svg_elem.attribute("viewBox") {
169             Some(val) => ViewBox::try_from(val).ok(),
170             None => None,
171         };
172 
173         let width = match svg_elem.attribute("width") {
174             Some(val) => Width::try_from(val).ok(),
175             None => None,
176         };
177 
178         let height = match svg_elem.attribute("height") {
179             Some(val) => Height::try_from(val).ok(),
180             None => None,
181         };
182 
183         Ok(Metadata {
184             view_box,
185             width,
186             height,
187         })
188     }
189 
190     /// Returns the value of the `width` attribute.
191     /// If the width is set to 100% then this refers to
192     /// the width of the viewbox.
width(&self) -> Option<f64>193     pub fn width(&self) -> Option<f64> {
194         if let Some(w) = self.width {
195             if w.unit == Unit::Percent {
196                 if let Some(v) = self.view_box {
197                     return Some(w.width / 100.0 * (v.width as f64));
198                 }
199             }
200         }
201         match self.width {
202             Some(w) => Some(w.width),
203             None => None,
204         }
205     }
206 
207     /// Returns the value of the `height` attribute.
208     /// If the height is set to 100% then this refers to
209     /// the height of the viewbox.
height(&self) -> Option<f64>210     pub fn height(&self) -> Option<f64> {
211         if let Some(h) = self.height {
212             if h.unit == Unit::Percent {
213                 if let Some(v) = self.view_box {
214                     return Some(h.height / 100.0 * (v.height as f64));
215                 }
216             }
217         }
218         match self.height {
219             Some(h) => Some(h.height),
220             None => None,
221         }
222     }
223 
224     /// Return view_box
view_box(&self) -> Option<ViewBox>225     pub fn view_box(&self) -> Option<ViewBox> {
226         self.view_box
227     }
228 }
229 
230 #[cfg(test)]
231 mod tests {
232     use super::*;
233 
234     #[test]
test_view_box_separators()235     fn test_view_box_separators() {
236         // Values can be separated by whitespace and/or a comma
237         let cases = vec!["0 1 99 100", "0, 1, 99, 100", "0, 1  99 100"];
238         for case in cases {
239             assert_eq!(
240                 ViewBox::try_from(case).unwrap(),
241                 ViewBox {
242                     min_x: 0.0,
243                     min_y: 1.0,
244                     width: 99.0,
245                     height: 100.0
246                 }
247             )
248         }
249     }
250 
251     #[test]
test_view_box_negative()252     fn test_view_box_negative() {
253         assert_eq!(
254             ViewBox::try_from("-0, 1, -99.00001, -100.3").unwrap(),
255             ViewBox {
256                 min_x: 0.0,
257                 min_y: 1.0,
258                 width: -99.00001,
259                 height: -100.3
260             }
261         )
262     }
263 
264     #[test]
test_width()265     fn test_width() {
266         let tests = vec![
267             (
268                 "100em",
269                 Width {
270                     width: 100.0,
271                     unit: Unit::Em,
272                 },
273             ),
274             (
275                 "100",
276                 Width {
277                     width: 100.0,
278                     unit: Unit::Em,
279                 },
280             ),
281             (
282                 "-10.0px",
283                 Width {
284                     width: -10.0,
285                     unit: Unit::Px,
286                 },
287             ),
288             (
289                 "100em",
290                 Width {
291                     width: 100.0,
292                     unit: Unit::Em,
293                 },
294             ),
295         ];
296         for (input, expected) in tests {
297             assert_eq!(Width::try_from(input).unwrap(), expected);
298         }
299     }
300 
301     #[test]
test_height()302     fn test_height() {
303         let tests = vec![
304             (
305                 "100em",
306                 Height {
307                     height: 100.0,
308                     unit: Unit::Em,
309                 },
310             ),
311             (
312                 "100",
313                 Height {
314                     height: 100.0,
315                     unit: Unit::Em,
316                 },
317             ),
318             (
319                 "-10.0px",
320                 Height {
321                     height: -10.0,
322                     unit: Unit::Px,
323                 },
324             ),
325             (
326                 "100em",
327                 Height {
328                     height: 100.0,
329                     unit: Unit::Em,
330                 },
331             ),
332         ];
333         for (input, expected) in tests {
334             assert_eq!(Height::try_from(input).unwrap(), expected);
335         }
336     }
337 
338     #[test]
test_width_height_percent()339     fn test_width_height_percent() {
340         let svg = r#"<svg viewBox="0 1 99 100" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
341   <rect x="0" y="0" width="100%" height="100%"/>
342 </svg>"#;
343 
344         let meta = Metadata::parse(svg).unwrap();
345         assert_eq!(meta.width(), Some(99.0));
346         assert_eq!(meta.height(), Some(100.0));
347 
348         let svg = r#"<svg viewBox="0 1 80 200" width="50%" height="20%" xmlns="http://www.w3.org/2000/svg"></svg>"#;
349 
350         let meta = Metadata::parse(svg).unwrap();
351         assert_eq!(meta.width(), Some(40.0));
352         assert_eq!(meta.height(), Some(40.0));
353     }
354 
355     #[test]
test_metadata()356     fn test_metadata() {
357         // separated by whitespace and/or a comma
358         let svg = r#"<svg viewBox="0 1 99 100" width="2em" height="10cm" xmlns="http://www.w3.org/2000/svg">
359   <rect x="0" y="0" width="100%" height="100%"/>
360 </svg>"#;
361 
362         let meta = Metadata::parse(svg).unwrap();
363         assert_eq!(
364             meta.view_box,
365             Some(ViewBox {
366                 min_x: 0.0,
367                 min_y: 1.0,
368                 width: 99.0,
369                 height: 100.0
370             })
371         );
372 
373         assert_eq!(
374             meta.view_box(),
375             Some(ViewBox {
376                 min_x: 0.0,
377                 min_y: 1.0,
378                 width: 99.0,
379                 height: 100.0
380             })
381         );
382 
383         assert_eq!(
384             meta.width,
385             Some(Width {
386                 width: 2.0,
387                 unit: Unit::Em
388             })
389         );
390         assert_eq!(
391             meta.height,
392             Some(Height {
393                 height: 10.0,
394                 unit: Unit::Cm
395             })
396         )
397     }
398 }
399 
400 #[cfg(doctest)]
401 #[macro_use]
402 extern crate doc_comment;
403 
404 #[cfg(doctest)]
405 doctest!("../README.md");
406