native_theme/
rasterize.rs1use crate::IconData;
7use resvg::tiny_skia;
8use resvg::usvg;
9
10pub fn rasterize_svg(svg_bytes: &[u8], size: u32) -> crate::Result<IconData> {
40 let options = usvg::Options::default();
41 let tree =
42 usvg::Tree::from_data(svg_bytes, &options).map_err(|e| crate::Error::ReaderFailed {
43 reader: "svg_rasterizer",
44 source: Box::new(e),
45 })?;
46
47 let original_size = tree.size();
48 let scale_x = size as f32 / original_size.width();
49 let scale_y = size as f32 / original_size.height();
50 let scale = scale_x.min(scale_y);
51
52 let scaled_w = original_size.width() * scale;
54 let scaled_h = original_size.height() * scale;
55 let offset_x = (size as f32 - scaled_w) / 2.0;
56 let offset_y = (size as f32 - scaled_h) / 2.0;
57
58 let mut pixmap =
59 tiny_skia::Pixmap::new(size, size).ok_or_else(|| crate::Error::ReaderFailed {
60 reader: "svg_rasterizer",
61 source: format!("invalid rasterization size {size}x{size}").into(),
62 })?;
63 let transform =
64 tiny_skia::Transform::from_translate(offset_x, offset_y).post_scale(scale, scale);
65 resvg::render(&tree, transform, &mut pixmap.as_mut());
66
67 let mut data = pixmap.take();
69 crate::color::unpremultiply_alpha(&mut data);
70
71 Ok(IconData::Rgba {
72 width: size,
73 height: size,
74 data,
75 })
76}
77
78#[cfg(test)]
79#[allow(clippy::unwrap_used, clippy::expect_used)]
80mod tests {
81 use super::*;
82
83 const VALID_SVG: &[u8] = b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='red'/></svg>";
84
85 #[test]
86 fn rasterize_valid_svg_returns_rgba() {
87 let result = rasterize_svg(VALID_SVG, 24);
88 assert!(result.is_ok(), "valid SVG should produce Ok");
89 match result.unwrap() {
90 IconData::Rgba {
91 width,
92 height,
93 data,
94 } => {
95 assert_eq!(width, 24);
96 assert_eq!(height, 24);
97 assert_eq!(data.len(), 24 * 24 * 4);
98 }
99 _ => panic!("expected IconData::Rgba"),
100 }
101 }
102
103 #[test]
104 fn rasterize_invalid_svg_returns_err() {
105 let result = rasterize_svg(b"not svg", 24);
106 assert!(result.is_err(), "invalid SVG should return Err");
107 }
108
109 #[test]
110 fn rasterize_output_length_matches_size() {
111 for size in [16, 24, 32, 48, 64] {
112 let result = rasterize_svg(VALID_SVG, size);
113 let icon = result.unwrap_or_else(|_| panic!("should produce output for size {size}"));
114 if let IconData::Rgba { data, .. } = icon {
115 assert_eq!(
116 data.len(),
117 (size * size * 4) as usize,
118 "output length mismatch for size {size}"
119 );
120 }
121 }
122 }
123
124 #[test]
125 fn rasterize_produces_non_empty_pixels() {
126 let result = rasterize_svg(VALID_SVG, 24).unwrap();
127 if let IconData::Rgba { data, .. } = result {
128 let non_zero = data.iter().any(|&b| b != 0);
130 assert!(non_zero, "rasterized output should contain non-zero pixels");
131 }
132 }
133
134 #[test]
135 fn rasterize_straight_alpha() {
136 let svg = b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 2'><rect width='2' height='2' fill='white' fill-opacity='0.5'/></svg>";
138 let result = rasterize_svg(svg, 2).unwrap();
139 if let IconData::Rgba { data, .. } = result {
140 for pixel in data.as_chunks::<4>().0 {
142 let &[r, _g, _b, a] = pixel;
143 if a > 0 {
144 assert!(
147 r > 200,
148 "expected straight alpha (R near 255), got R={r} A={a}"
149 );
150 }
151 }
152 }
153 }
154
155 #[test]
156 fn rasterize_zero_size_returns_err() {
157 let result = rasterize_svg(VALID_SVG, 0);
158 assert!(result.is_err(), "zero size should return Err");
159 }
160}