iced_wgpu/image/
vector.rs

1use crate::core::svg;
2use crate::core::{Color, Size};
3use crate::image::atlas::{self, Atlas};
4
5use resvg::tiny_skia;
6use resvg::usvg;
7use rustc_hash::{FxHashMap, FxHashSet};
8use std::fs;
9use std::panic;
10use std::sync::Arc;
11
12/// Entry in cache corresponding to an svg handle
13pub enum Svg {
14    /// Parsed svg
15    Loaded(usvg::Tree),
16    /// Svg not found or failed to parse
17    NotFound,
18}
19
20impl Svg {
21    /// Viewport width and height
22    pub fn viewport_dimensions(&self) -> Size<u32> {
23        match self {
24            Svg::Loaded(tree) => {
25                let size = tree.size();
26
27                Size::new(size.width() as u32, size.height() as u32)
28            }
29            Svg::NotFound => Size::new(1, 1),
30        }
31    }
32}
33
34/// Caches svg vector and raster data
35#[derive(Debug, Default)]
36pub struct Cache {
37    svgs: FxHashMap<u64, Svg>,
38    rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>,
39    svg_hits: FxHashSet<u64>,
40    rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>,
41    should_trim: bool,
42    fontdb: Option<Arc<usvg::fontdb::Database>>,
43}
44
45type ColorFilter = Option<[u8; 4]>;
46
47impl Cache {
48    /// Load svg
49    pub fn load(&mut self, handle: &svg::Handle) -> &Svg {
50        if self.svgs.contains_key(&handle.id()) {
51            return self.svgs.get(&handle.id()).unwrap();
52        }
53
54        // TODO: Reuse `cosmic-text` font database
55        if self.fontdb.is_none() {
56            let mut fontdb = usvg::fontdb::Database::new();
57            fontdb.load_system_fonts();
58
59            self.fontdb = Some(Arc::new(fontdb));
60        }
61
62        let options = usvg::Options {
63            fontdb: self
64                .fontdb
65                .as_ref()
66                .expect("fontdb must be initialized")
67                .clone(),
68            ..usvg::Options::default()
69        };
70
71        let svg = match handle.data() {
72            svg::Data::Path(path) => fs::read_to_string(path)
73                .ok()
74                .and_then(|contents| usvg::Tree::from_str(&contents, &options).ok())
75                .map(Svg::Loaded)
76                .unwrap_or(Svg::NotFound),
77            svg::Data::Bytes(bytes) => match usvg::Tree::from_data(bytes, &options) {
78                Ok(tree) => Svg::Loaded(tree),
79                Err(_) => Svg::NotFound,
80            },
81        };
82
83        self.should_trim = true;
84
85        let _ = self.svgs.insert(handle.id(), svg);
86        self.svgs.get(&handle.id()).unwrap()
87    }
88
89    /// Load svg and upload raster data
90    pub fn upload(
91        &mut self,
92        device: &wgpu::Device,
93        encoder: &mut wgpu::CommandEncoder,
94        belt: &mut wgpu::util::StagingBelt,
95        handle: &svg::Handle,
96        color: Option<Color>,
97        size: Size,
98        scale: f32,
99        atlas: &mut Atlas,
100    ) -> Option<&atlas::Entry> {
101        let id = handle.id();
102
103        let (width, height) = (
104            (scale * size.width).ceil() as u32,
105            (scale * size.height).ceil() as u32,
106        );
107
108        let color = color.map(Color::into_rgba8);
109        let key = (id, width, height, color);
110
111        // TODO: Optimize!
112        // We currently rerasterize the SVG when its size changes. This is slow
113        // as heck. A GPU rasterizer like `pathfinder` may perform better.
114        // It would be cool to be able to smooth resize the `svg` example.
115        if self.rasterized.contains_key(&key) {
116            let _ = self.svg_hits.insert(id);
117            let _ = self.rasterized_hits.insert(key);
118
119            return self.rasterized.get(&key);
120        }
121
122        match self.load(handle) {
123            Svg::Loaded(tree) => {
124                if width == 0 || height == 0 {
125                    return None;
126                }
127
128                // TODO: Optimize!
129                // We currently rerasterize the SVG when its size changes. This is slow
130                // as heck. A GPU rasterizer like `pathfinder` may perform better.
131                // It would be cool to be able to smooth resize the `svg` example.
132                let mut img = tiny_skia::Pixmap::new(width, height)?;
133
134                let tree_size = tree.size().to_int_size();
135
136                let target_size = if width > height {
137                    tree_size.scale_to_width(width)
138                } else {
139                    tree_size.scale_to_height(height)
140                };
141
142                let transform = if let Some(target_size) = target_size {
143                    let tree_size = tree_size.to_size();
144                    let target_size = target_size.to_size();
145
146                    tiny_skia::Transform::from_scale(
147                        target_size.width() / tree_size.width(),
148                        target_size.height() / tree_size.height(),
149                    )
150                } else {
151                    tiny_skia::Transform::default()
152                };
153
154                // SVG rendering can panic on malformed or complex vectors.
155                // We catch panics to prevent crashes and continue gracefully.
156                let render = panic::catch_unwind(panic::AssertUnwindSafe(|| {
157                    resvg::render(tree, transform, &mut img.as_mut());
158                }));
159
160                if let Err(error) = render {
161                    log::warn!("SVG rendering for {handle:?} panicked: {error:?}");
162                }
163
164                let mut rgba = img.take();
165
166                if let Some(color) = color {
167                    rgba.chunks_exact_mut(4).for_each(|rgba| {
168                        if rgba[3] > 0 {
169                            rgba[0] = color[0];
170                            rgba[1] = color[1];
171                            rgba[2] = color[2];
172                        }
173                    });
174                }
175
176                let allocation = atlas.upload(device, encoder, belt, width, height, &rgba)?;
177
178                log::debug!("allocating {id} {width}x{height}");
179
180                let _ = self.svg_hits.insert(id);
181                let _ = self.rasterized_hits.insert(key);
182                let _ = self.rasterized.insert(key, allocation);
183                self.should_trim = true;
184
185                self.rasterized.get(&key)
186            }
187            Svg::NotFound => None,
188        }
189    }
190
191    /// Load svg and upload raster data
192    pub fn trim(&mut self, atlas: &mut Atlas) {
193        if !self.should_trim {
194            return;
195        }
196
197        let svg_hits = &self.svg_hits;
198        let rasterized_hits = &self.rasterized_hits;
199
200        self.svgs.retain(|k, _| svg_hits.contains(k));
201        self.rasterized.retain(|k, entry| {
202            let retain = rasterized_hits.contains(k);
203
204            if !retain {
205                atlas.remove(entry);
206            }
207
208            retain
209        });
210        self.svg_hits.clear();
211        self.rasterized_hits.clear();
212        self.should_trim = false;
213    }
214}
215
216impl std::fmt::Debug for Svg {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        match self {
219            Svg::Loaded(_) => write!(f, "Svg::Loaded"),
220            Svg::NotFound => write!(f, "Svg::NotFound"),
221        }
222    }
223}