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