turbopack_image/process/
svg.rs1use 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}