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