turbopack_image/process/
svg.rs

1// Ported from https://github.com/image-size/image-size/blob/94e9c1ee913b71222d7583dc904ac0116ae00834/lib/types/svg.ts
2// see SVG_LICENSE for license info
3
4use anyhow::{Result, anyhow, bail};
5use once_cell::sync::Lazy;
6use regex::Regex;
7use rustc_hash::FxHashMap;
8
9const INCH_CM: f64 = 2.54;
10static UNITS: Lazy<FxHashMap<&str, f64>> = Lazy::new(|| {
11    FxHashMap::from_iter([
12        ("in", 96.0),
13        ("cm", 96.0 / INCH_CM),
14        ("em", 16.0),
15        ("ex", 8.0),
16        ("m", 96.0 / INCH_CM * 100.0),
17        ("mm", 96.0 / INCH_CM / 10.0),
18        ("pc", 96.0 / 72.0 / 12.0),
19        ("pt", 96.0 / 72.0),
20        ("px", 1.0),
21        ("", 1.0),
22    ])
23});
24
25static UNIT_REGEX: Lazy<Regex> =
26    Lazy::new(|| Regex::new(r"^([0-9.]+(?:e-?\d+)?)((?:in|cm|em|ex|m|mm|pc|pt|px)?)$").unwrap());
27
28static ROOT_REGEX: Lazy<Regex> =
29    Lazy::new(|| Regex::new(r#"<svg\s([^>"']|"[^"]*"|'[^']*')*>"#).unwrap());
30static WIDTH_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\swidth=['"]([^%]+?)['"]"#).unwrap());
31static HEIGHT_REGEX: Lazy<Regex> =
32    Lazy::new(|| Regex::new(r#"\sheight=['"]([^%]+?)['"]"#).unwrap());
33static VIEW_BOX_REGEX: Lazy<Regex> =
34    Lazy::new(|| Regex::new(r#"\sviewBox=['"](.+?)['"]"#).unwrap());
35static VIEW_BOX_CONTENT_REGEX: Lazy<Regex> = Lazy::new(|| {
36    Regex::new(r"^\s*((?:\w|\.|-)+)\s+((?:\w|\.|-)+)\s+((?:\w|\.|-)+)\s+((?:\w|\.|-)+)\s*$")
37        .unwrap()
38});
39
40fn parse_length(len: &str) -> Result<f64> {
41    let captures = UNIT_REGEX
42        .captures(len)
43        .ok_or_else(|| anyhow!("Unknown syntax for length, expected value with unit ({len})"))?;
44    let val = captures[1].parse::<f64>()?;
45    let unit = &captures[2];
46    let unit_scale = UNITS
47        .get(unit)
48        .ok_or_else(|| anyhow!("Unknown unit {unit}"))?;
49    Ok(val * unit_scale)
50}
51
52fn parse_viewbox(viewbox: &str) -> Result<(f64, f64)> {
53    let captures = VIEW_BOX_CONTENT_REGEX
54        .captures(viewbox)
55        .ok_or_else(|| anyhow!("Unknown syntax for viewBox ({viewbox})"))?;
56    let width = parse_length(&captures[3])?;
57    let height = parse_length(&captures[4])?;
58    Ok((width, height))
59}
60
61fn calculate_by_viewbox(
62    view_box: (f64, f64),
63    width: Option<Result<f64>>,
64    height: Option<Result<f64>>,
65) -> Result<(u32, u32)> {
66    let ratio = view_box.0 / view_box.1;
67    if let Some(width) = width {
68        let width = width?.round() as u32;
69        let height = (width as f64 / ratio).round() as u32;
70        return Ok((width, height));
71    }
72    if let Some(height) = height {
73        let height = height?.round() as u32;
74        let width = (height as f64 * ratio).round() as u32;
75        return Ok((width, height));
76    }
77    Ok((view_box.0.round() as u32, view_box.1.round() as u32))
78}
79
80pub fn calculate(content: &str) -> Result<(u32, u32)> {
81    let Some(root) = ROOT_REGEX.find(content) else {
82        bail!("Source code does not contain a <svg> root element");
83    };
84    let root = root.as_str();
85    let width = WIDTH_REGEX.captures(root).map(|c| parse_length(&c[1]));
86    let height = HEIGHT_REGEX.captures(root).map(|c| parse_length(&c[1]));
87    let viewbox = VIEW_BOX_REGEX.captures(root).map(|c| parse_viewbox(&c[1]));
88    if let Some(width) = width {
89        if let Some(height) = height {
90            Ok((width?.round() as u32, height?.round() as u32))
91        } else {
92            bail!("SVG source code contains only a width attribute but not height attribute");
93        }
94    } else if let Some(viewbox) = viewbox {
95        calculate_by_viewbox(viewbox?, width, height)
96    } else {
97        bail!("SVG source code does not contain width and height or viewBox attribute");
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use anyhow::Result;
104
105    use super::calculate;
106
107    #[test]
108    fn test_calculate() {
109        let svg1 = r#"<svg width="100" height="50"></svg>"#;
110        assert_eq!(calculate(svg1).unwrap(), (100, 50));
111
112        let svg2 = r#"<svg width="100" height="50" viewBox="0 0 200 100"></svg>"#;
113        assert_eq!(calculate(svg2).unwrap(), (100, 50));
114
115        let svg3 = r#"<svg viewBox="0 0 200 100"></svg>"#;
116        assert_eq!(calculate(svg3).unwrap(), (200, 100));
117
118        let svg4 = r#"<svg width="100px" height="50px"></svg>"#;
119        assert_eq!(calculate(svg4).unwrap(), (100, 50));
120
121        let svg5 = r#"<svg width="100" height="50" viewBox="0 0 200 100"></svg>"#;
122        assert_eq!(calculate(svg5).unwrap(), (100, 50));
123
124        let svg6 = r#"<svg></svg>"#;
125        assert!(calculate(svg6).is_err());
126
127        let svg7 = r#"<svg width="100"></svg>"#;
128        assert!(calculate(svg7).is_err());
129
130        let svg8 = r#"<svg height="50"></svg>"#;
131        assert!(calculate(svg8).is_err());
132
133        let svg9 = r#"<svg viewBox="0 0 200"></svg>"#;
134        assert!(calculate(svg9).is_err());
135
136        let svg10 = r#"<svg width="100" height="invalid"></svg>"#;
137        assert!(calculate(svg10).is_err());
138    }
139
140    #[test]
141    fn test_calculate_with_units() -> Result<()> {
142        let svg = r#"<svg width="2cm" height="50mm"></svg>"#;
143        let result = calculate(svg)?;
144        assert_eq!(result, (76, 189));
145        Ok(())
146    }
147
148    #[test]
149    fn test_calculate_with_em() -> Result<()> {
150        let svg = r#"<svg width="20em" height="10em"></svg>"#;
151        let result = calculate(svg)?;
152        assert_eq!(result, (320, 160));
153        Ok(())
154    }
155
156    #[test]
157    fn test_calculate_with_ex() -> Result<()> {
158        let svg = r#"<svg width="20ex" height="10ex"></svg>"#;
159        let result = calculate(svg)?;
160        assert_eq!(result, (160, 80));
161        Ok(())
162    }
163
164    #[test]
165    fn test_calculate_complex_viewbox() -> Result<()> {
166        let svg = r#"<svg viewBox="-100 -10.5 5000e-2 50.42e3"></svg>"#;
167        let result = calculate(svg)?;
168        assert_eq!(result, (50, 50420));
169        Ok(())
170    }
171}