kas_resvg/
svg.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! SVG widget
7
8use kas::draw::{ImageFormat, ImageHandle};
9use kas::layout::{LogicalSize, PixmapScaling};
10use kas::prelude::*;
11use std::future::Future;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14use tiny_skia::{Pixmap, Transform};
15use usvg::Tree;
16
17/// Load errors
18#[derive(thiserror::Error, Debug)]
19enum LoadError {
20    #[error("IO error")]
21    Io(#[from] std::io::Error),
22    #[error("SVG error")]
23    Svg(#[from] usvg::Error),
24}
25
26fn load(data: &[u8], resources_dir: Option<&Path>) -> Result<Tree, usvg::Error> {
27    use once_cell::sync::Lazy;
28    static FONT_FAMILY: Lazy<String> = Lazy::new(|| {
29        let mut resolver = kas::text::fonts::library().resolver();
30        resolver
31            .font_family_from_generic(kas::text::fonts::GenericFamily::Serif)
32            .map(|s| s.to_string())
33            .unwrap_or_default()
34    });
35
36    // Defaults are taken from usvg::Options::default(). Notes:
37    // - adjusting for screen scale factor is purely a property of
38    //   making the canvas larger and not important here
39    // - default_size: affected by screen scale factor later
40    // - dpi: according to css-values-3, 1in = 96px
41    // - font_size: units are (logical) px per em; 16px = 12pt
42    // - TODO: add option to clone fontdb from kas::text?
43    let opts = usvg::Options {
44        resources_dir: resources_dir.map(|path| path.to_owned()),
45        dpi: 96.0,
46        font_family: FONT_FAMILY.clone(),
47        font_size: 16.0, // units: "logical pixels" per Em
48        languages: vec!["en".to_string()],
49        shape_rendering: usvg::ShapeRendering::default(),
50        text_rendering: usvg::TextRendering::default(),
51        image_rendering: usvg::ImageRendering::default(),
52        default_size: usvg::Size::from_wh(100.0, 100.0).unwrap(),
53        image_href_resolver: Default::default(),
54        font_resolver: Default::default(),
55        fontdb: Default::default(),
56        style_sheet: None,
57    };
58
59    let tree = Tree::from_data(data, &opts)?;
60
61    Ok(tree)
62}
63
64#[derive(Clone)]
65enum Source {
66    Static(&'static [u8], Option<PathBuf>),
67    Heap(Arc<[u8]>, Option<PathBuf>),
68}
69impl std::fmt::Debug for Source {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match self {
72            Source::Static(_, path) => write!(f, "Source::Static(_, {path:?}"),
73            Source::Heap(_, path) => write!(f, "Source::Heap(_, {path:?}"),
74        }
75    }
76}
77impl Source {
78    fn tree(&self) -> Result<Tree, usvg::Error> {
79        let (data, res_dir) = match self {
80            Source::Static(d, p) => (*d, p.as_ref()),
81            Source::Heap(d, p) => (&**d, p.as_ref()),
82        };
83        load(data, res_dir.map(|p| p.as_ref()))
84    }
85}
86
87#[derive(Clone, Debug, Default)]
88enum State {
89    #[default]
90    None,
91    Initial(Source),
92    Rendering(Source),
93    Ready(Source, Pixmap),
94}
95
96async fn draw(svg: Source, mut pixmap: Pixmap) -> Pixmap {
97    if let Ok(tree) = svg.tree() {
98        let w = f32::conv(pixmap.width()) / tree.size().width();
99        let h = f32::conv(pixmap.height()) / tree.size().height();
100        let transform = Transform::from_scale(w, h);
101        resvg::render(&tree, transform, &mut pixmap.as_mut());
102    }
103    pixmap
104}
105
106impl State {
107    /// Resize if required, redrawing on resize
108    ///
109    /// Returns a future to redraw. Does nothing if currently redrawing.
110    fn resize(&mut self, (w, h): (u32, u32)) -> Option<impl Future<Output = Pixmap> + use<>> {
111        let old_state = std::mem::replace(self, State::None);
112        match old_state {
113            State::None => (),
114            state @ State::Rendering(_) => *self = state,
115            State::Ready(svg, px) if (px.width(), px.height()) == (w, h) => {
116                *self = State::Ready(svg, px);
117                return None;
118            }
119            State::Initial(svg) | State::Ready(svg, _) => {
120                if let Some(px) = Pixmap::new(w, h) {
121                    *self = State::Rendering(svg.clone());
122                    return Some(draw(svg, px));
123                } else {
124                    *self = State::Initial(svg);
125                    return None;
126                }
127            }
128        }
129        None
130    }
131}
132
133#[impl_self]
134mod Svg {
135    /// An SVG image loaded from a path
136    ///
137    /// May be default constructed (result is empty).
138    #[autoimpl(Debug ignore self.inner)]
139    #[derive(Clone, Default)]
140    #[widget]
141    pub struct Svg {
142        core: widget_core!(),
143        inner: State,
144        scaling: PixmapScaling,
145        image: Option<ImageHandle>,
146    }
147
148    impl Self {
149        /// Construct from data
150        ///
151        /// Returns an error if the SVG fails to parse. If using this method
152        /// with [`include_bytes`] it is probably safe to unwrap.
153        pub fn new(data: &'static [u8]) -> Result<Self, impl std::error::Error> {
154            let mut svg = Svg::default();
155            let source = Source::Static(data, None);
156            svg.load_source(source).map(|_| svg)
157        }
158
159        /// Construct from a path
160        pub fn new_path<P: AsRef<Path>>(path: P) -> Result<Self, impl std::error::Error> {
161            let mut svg = Svg::default();
162            svg._load_path(path.as_ref())?;
163            Result::<Self, LoadError>::Ok(svg)
164        }
165
166        /// Load from `data`
167        ///
168        /// Replaces existing data and request a resize. The sizing policy is
169        /// set to [`PixmapScaling::size`] using dimensions from the SVG.
170        pub fn load(
171            &mut self,
172            cx: &mut EventState,
173            data: &'static [u8],
174            resources_dir: Option<&Path>,
175        ) -> Result<(), impl std::error::Error + use<>> {
176            let source = Source::Static(data, resources_dir.map(|p| p.to_owned()));
177            self.load_source(source).map(|_| cx.resize(self))
178        }
179
180        fn load_source(&mut self, source: Source) -> Result<(), usvg::Error> {
181            // Set scaling size. TODO: this is useless if Self::with_size is called after.
182            let size = source.tree()?.size();
183            self.scaling.size = LogicalSize(size.width(), size.height());
184
185            self.inner = match std::mem::take(&mut self.inner) {
186                State::Ready(_, px) => State::Ready(source, px),
187                _ => State::Initial(source),
188            };
189            Ok(())
190        }
191
192        /// Load from a path
193        ///
194        /// This is a wrapper around [`Self::load`].
195        pub fn load_path<P: AsRef<Path>>(
196            &mut self,
197            cx: &mut EventState,
198            path: P,
199        ) -> Result<(), impl std::error::Error + use<P>> {
200            self._load_path(path.as_ref()).map(|_| cx.resize(self))
201        }
202
203        fn _load_path(&mut self, path: &Path) -> Result<(), LoadError> {
204            let buf = std::fs::read(path)?;
205            let rd = path.parent().map(|path| path.to_owned());
206            let source = Source::Heap(buf.into(), rd);
207            Ok(self.load_source(source)?)
208        }
209
210        /// Assign size
211        ///
212        /// By default, size is derived from the loaded SVG. See also
213        /// [`Self::with_scaling`] and [`Self::set_scaling`] for more options.
214        #[inline]
215        #[must_use]
216        pub fn with_size(mut self, size: LogicalSize) -> Self {
217            self.scaling.size = size;
218            self
219        }
220
221        /// Adjust scaling
222        ///
223        /// [`PixmapScaling::size`] is set from the SVG on loading (it may also be set here).
224        /// Other scaling parameters take their default values from [`PixmapScaling`].
225        #[inline]
226        #[must_use]
227        pub fn with_scaling(mut self, f: impl FnOnce(&mut PixmapScaling)) -> Self {
228            f(&mut self.scaling);
229            self
230        }
231
232        /// Adjust scaling
233        ///
234        /// [`PixmapScaling::size`] is set from the SVG on loading (it may also be set here).
235        /// Other scaling parameters take their default values from [`PixmapScaling`].
236        #[inline]
237        pub fn set_scaling(&mut self, cx: &mut EventState, f: impl FnOnce(&mut PixmapScaling)) {
238            f(&mut self.scaling);
239            // NOTE: if only `aspect` is changed, REDRAW is enough
240            cx.resize(self);
241        }
242    }
243
244    impl Layout for Self {
245        fn rect(&self) -> Rect {
246            self.scaling.rect
247        }
248
249        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
250            self.scaling.size_rules(sizer, axis)
251        }
252
253        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
254            let align = hints.complete_default();
255            let scale_factor = cx.size_cx().scale_factor();
256            self.scaling.set_rect(rect, align, scale_factor);
257
258            let size: (u32, u32) = self.rect().size.cast();
259            if let Some(fut) = self.inner.resize(size) {
260                cx.send_spawn(self.id(), fut);
261            }
262        }
263
264        fn draw(&self, mut draw: DrawCx) {
265            if let Some(id) = self.image.as_ref().map(|h| h.id()) {
266                draw.image(self.rect(), id);
267            }
268        }
269    }
270
271    impl Tile for Self {
272        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
273            Role::Image
274        }
275    }
276
277    impl Events for Self {
278        type Data = ();
279
280        fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
281            if let Some(pixmap) = cx.try_pop::<Pixmap>() {
282                let size = (pixmap.width(), pixmap.height());
283                let ds = cx.draw_shared();
284
285                if let Some(im_size) = self.image.as_ref().and_then(|h| ds.image_size(h))
286                    && im_size != Size::conv(size)
287                    && let Some(handle) = self.image.take()
288                {
289                    ds.image_free(handle);
290                }
291
292                if self.image.is_none() {
293                    self.image = ds.image_alloc(size).ok();
294                }
295
296                if let Some(handle) = self.image.as_ref() {
297                    ds.image_upload(handle, pixmap.data(), ImageFormat::Rgba8);
298                }
299
300                cx.redraw(&self);
301                self.inner = match std::mem::take(&mut self.inner) {
302                    State::None => State::None,
303                    State::Initial(source) | State::Rendering(source) | State::Ready(source, _) => {
304                        State::Ready(source, pixmap)
305                    }
306                };
307
308                let own_size: (u32, u32) = self.rect().size.cast();
309                if size != own_size
310                    && let Some(fut) = self.inner.resize(own_size)
311                {
312                    cx.send_spawn(self.id(), fut);
313                }
314            }
315        }
316    }
317}