1use std::path::Path;
5
6pub fn load_image(path: &Path) -> Option<image::DynamicImage> {
10 if path
11 .extension()
12 .is_some_and(|e| e.eq_ignore_ascii_case("svg"))
13 {
14 return load_svg(&std::fs::read(path).ok()?);
15 }
16 image::ImageReader::open(path)
17 .ok()?
18 .with_guessed_format()
19 .ok()?
20 .decode()
21 .ok()
22}
23
24pub fn load_svg(data: &[u8]) -> Option<image::DynamicImage> {
35 const SVG_TARGET_PX: f32 = 640.0;
39
40 let tree = svg_tree(data)?;
41 let size = tree.size();
42 let longest = size.width().max(size.height()).max(1.0);
43 let scale = (SVG_TARGET_PX / longest).clamp(0.05, 16.0);
46 render_svg(&tree, scale)
47}
48
49pub fn rasterize_svg(data: &[u8], scale: f32) -> Option<image::DynamicImage> {
56 let tree = svg_tree(data)?;
57 render_svg(&tree, scale.clamp(0.05, 16.0))
58}
59
60fn svg_tree(data: &[u8]) -> Option<resvg::usvg::Tree> {
63 use resvg::usvg;
64
65 fn svg_fontdb() -> std::sync::Arc<resvg::usvg::fontdb::Database> {
70 use std::sync::{Arc, OnceLock};
71 static FONTS: OnceLock<Arc<resvg::usvg::fontdb::Database>> = OnceLock::new();
72 FONTS
73 .get_or_init(|| {
74 let mut db = resvg::usvg::fontdb::Database::new();
75 db.load_system_fonts();
76 Arc::new(db)
77 })
78 .clone()
79 }
80
81 let opt = usvg::Options {
82 fontdb: svg_fontdb(),
83 ..Default::default()
84 };
85 usvg::Tree::from_data(data, &opt).ok()
86}
87
88fn render_svg(tree: &resvg::usvg::Tree, scale: f32) -> Option<image::DynamicImage> {
91 use resvg::tiny_skia;
92
93 let size = tree.size();
94 let w = (size.width() * scale).ceil().max(1.0) as u32;
95 let h = (size.height() * scale).ceil().max(1.0) as u32;
96
97 let mut pixmap = tiny_skia::Pixmap::new(w, h)?;
98 resvg::render(
99 tree,
100 tiny_skia::Transform::from_scale(scale, scale),
101 &mut pixmap.as_mut(),
102 );
103
104 let mut rgba = Vec::with_capacity((w as usize) * (h as usize) * 4);
107 for px in pixmap.pixels() {
108 let c = px.demultiply();
109 rgba.extend_from_slice(&[c.red(), c.green(), c.blue(), c.alpha()]);
110 }
111 Some(image::DynamicImage::ImageRgba8(image::RgbaImage::from_raw(
112 w, h, rgba,
113 )?))
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn load_svg_rasterizes_to_straight_alpha_rgba() {
122 let svg = br##"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10"><rect width="20" height="10" fill="#ff0000"/></svg>"##;
125 let img = load_svg(svg).expect("valid SVG should rasterize");
126 assert!(
129 img.width() >= 20 && img.height() >= 10,
130 "got {}×{}",
131 img.width(),
132 img.height()
133 );
134 assert_eq!(img.width(), img.height() * 2, "aspect ratio preserved");
135 let rgba = img.to_rgba8();
137 let center = rgba.get_pixel(rgba.width() / 2, rgba.height() / 2).0;
138 assert_eq!(center, [255, 0, 0, 255], "center pixel is opaque red");
139 }
140
141 #[test]
142 fn load_svg_rejects_garbage() {
143 assert!(load_svg(b"not an svg at all").is_none());
144 assert!(rasterize_svg(b"not an svg at all", 1.0).is_none());
145 }
146
147 #[test]
148 fn rasterize_svg_takes_the_picture_s_own_size_times_the_scale() {
149 let svg = br##"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10"><rect width="20" height="10" fill="#ff0000"/></svg>"##;
150 let one = rasterize_svg(svg, 1.0).unwrap();
151 assert_eq!((one.width(), one.height()), (20, 10));
152 let two = rasterize_svg(svg, 2.0).unwrap();
153 assert_eq!((two.width(), two.height()), (40, 20));
154 let svg = br##"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10"><rect width="10" height="10" fill="#ff0000"/></svg>"##;
156 let img = rasterize_svg(svg, 1.0).unwrap().to_rgba8();
157 assert_eq!(
158 img.get_pixel(15, 5).0[3],
159 0,
160 "transparent where nothing is drawn"
161 );
162 }
163}