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