Skip to main content

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