iced_tiny_skia/
vector.rs

1use crate::core::svg::{Data, Handle};
2use crate::core::{Color, Rectangle, Size};
3
4use resvg::usvg;
5use rustc_hash::{FxHashMap, FxHashSet};
6use tiny_skia::Transform;
7
8use std::cell::RefCell;
9use std::collections::hash_map;
10use std::fs;
11use std::panic;
12use std::sync::Arc;
13
14#[derive(Debug)]
15pub struct Pipeline {
16    cache: RefCell<Cache>,
17}
18
19impl Pipeline {
20    pub fn new() -> Self {
21        Self {
22            cache: RefCell::new(Cache::default()),
23        }
24    }
25
26    pub fn viewport_dimensions(&self, handle: &Handle) -> Size<u32> {
27        self.cache
28            .borrow_mut()
29            .viewport_dimensions(handle)
30            .unwrap_or(Size::new(0, 0))
31    }
32
33    pub fn draw(
34        &mut self,
35        handle: &Handle,
36        color: Option<Color>,
37        bounds: Rectangle,
38        opacity: f32,
39        pixels: &mut tiny_skia::PixmapMut<'_>,
40        transform: Transform,
41        clip_mask: Option<&tiny_skia::Mask>,
42    ) {
43        if let Some(image) = self.cache.borrow_mut().draw(
44            handle,
45            color,
46            Size::new(
47                (bounds.width * transform.sx) as u32,
48                (bounds.height * transform.sy) as u32,
49            ),
50        ) {
51            pixels.draw_pixmap(
52                (bounds.x * transform.sx) as i32,
53                (bounds.y * transform.sy) as i32,
54                image,
55                &tiny_skia::PixmapPaint {
56                    opacity,
57                    ..tiny_skia::PixmapPaint::default()
58                },
59                Transform::default(),
60                clip_mask,
61            );
62        }
63    }
64
65    pub fn trim_cache(&mut self) {
66        self.cache.borrow_mut().trim();
67    }
68}
69
70#[derive(Default)]
71struct Cache {
72    trees: FxHashMap<u64, Option<resvg::usvg::Tree>>,
73    tree_hits: FxHashSet<u64>,
74    rasters: FxHashMap<RasterKey, tiny_skia::Pixmap>,
75    raster_hits: FxHashSet<RasterKey>,
76    fontdb: Option<Arc<usvg::fontdb::Database>>,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80struct RasterKey {
81    id: u64,
82    color: Option<[u8; 4]>,
83    size: Size<u32>,
84}
85
86impl Cache {
87    fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> {
88        let id = handle.id();
89
90        // TODO: Reuse `cosmic-text` font database
91        if self.fontdb.is_none() {
92            let mut fontdb = usvg::fontdb::Database::new();
93            fontdb.load_system_fonts();
94
95            self.fontdb = Some(Arc::new(fontdb));
96        }
97
98        let options = usvg::Options {
99            fontdb: self
100                .fontdb
101                .as_ref()
102                .expect("fontdb must be initialized")
103                .clone(),
104            ..usvg::Options::default()
105        };
106
107        if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) {
108            let svg = match handle.data() {
109                Data::Path(path) => fs::read_to_string(path)
110                    .ok()
111                    .and_then(|contents| usvg::Tree::from_str(&contents, &options).ok()),
112                Data::Bytes(bytes) => usvg::Tree::from_data(bytes, &options).ok(),
113            };
114
115            let _ = entry.insert(svg);
116        }
117
118        let _ = self.tree_hits.insert(id);
119        self.trees.get(&id).unwrap().as_ref()
120    }
121
122    fn viewport_dimensions(&mut self, handle: &Handle) -> Option<Size<u32>> {
123        let tree = self.load(handle)?;
124        let size = tree.size();
125
126        Some(Size::new(size.width() as u32, size.height() as u32))
127    }
128
129    fn draw(
130        &mut self,
131        handle: &Handle,
132        color: Option<Color>,
133        size: Size<u32>,
134    ) -> Option<tiny_skia::PixmapRef<'_>> {
135        if size.width == 0 || size.height == 0 {
136            return None;
137        }
138
139        let key = RasterKey {
140            id: handle.id(),
141            color: color.map(Color::into_rgba8),
142            size,
143        };
144
145        #[allow(clippy::map_entry)]
146        if !self.rasters.contains_key(&key) {
147            let tree = self.load(handle)?;
148
149            let mut image = tiny_skia::Pixmap::new(size.width, size.height)?;
150
151            let tree_size = tree.size().to_int_size();
152
153            let target_size = if size.width > size.height {
154                tree_size.scale_to_width(size.width)
155            } else {
156                tree_size.scale_to_height(size.height)
157            };
158
159            let transform = if let Some(target_size) = target_size {
160                let tree_size = tree_size.to_size();
161                let target_size = target_size.to_size();
162
163                tiny_skia::Transform::from_scale(
164                    target_size.width() / tree_size.width(),
165                    target_size.height() / tree_size.height(),
166                )
167            } else {
168                tiny_skia::Transform::default()
169            };
170
171            // SVG rendering can panic on malformed or complex vectors.
172            // We catch panics to prevent crashes and continue gracefully.
173            let render = panic::catch_unwind(panic::AssertUnwindSafe(|| {
174                resvg::render(tree, transform, &mut image.as_mut());
175            }));
176
177            if let Err(error) = render {
178                log::warn!("SVG rendering for {handle:?} panicked: {error:?}");
179            }
180
181            if let Some([r, g, b, _]) = key.color {
182                // Apply color filter
183                for pixel in bytemuck::cast_slice_mut::<u8, u32>(image.data_mut()) {
184                    *pixel = bytemuck::cast(
185                        tiny_skia::ColorU8::from_rgba(b, g, r, (*pixel >> 24) as u8).premultiply(),
186                    );
187                }
188            } else {
189                // Swap R and B channels for `softbuffer` presentation
190                for pixel in bytemuck::cast_slice_mut::<u8, u32>(image.data_mut()) {
191                    *pixel = *pixel & 0xFF00_FF00
192                        | ((0x0000_00FF & *pixel) << 16)
193                        | ((0x00FF_0000 & *pixel) >> 16);
194                }
195            }
196
197            let _ = self.rasters.insert(key, image);
198        }
199
200        let _ = self.raster_hits.insert(key);
201        self.rasters.get(&key).map(tiny_skia::Pixmap::as_ref)
202    }
203
204    fn trim(&mut self) {
205        self.trees.retain(|key, _| self.tree_hits.contains(key));
206        self.rasters.retain(|key, _| self.raster_hits.contains(key));
207
208        self.tree_hits.clear();
209        self.raster_hits.clear();
210    }
211}
212
213impl std::fmt::Debug for Cache {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        f.debug_struct("Cache")
216            .field("tree_hits", &self.tree_hits)
217            .field("rasters", &self.rasters)
218            .field("raster_hits", &self.raster_hits)
219            .finish_non_exhaustive()
220    }
221}