Skip to main content

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