Skip to main content

i_slint_core/graphics/image/
svg.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use super::{ImageCacheKey, SharedImageBuffer, SharedPixelBuffer};
5#[cfg(not(target_arch = "wasm32"))]
6use crate::SharedString;
7use crate::lengths::PhysicalPx;
8use resvg::{tiny_skia, usvg};
9
10pub struct ParsedSVG {
11    svg_tree: usvg::Tree,
12    cache_key: ImageCacheKey,
13}
14
15impl super::OpaqueImage for ParsedSVG {
16    fn size(&self) -> crate::graphics::IntSize {
17        self.size()
18    }
19    fn cache_key(&self) -> ImageCacheKey {
20        self.cache_key.clone()
21    }
22}
23
24impl core::fmt::Debug for ParsedSVG {
25    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26        f.debug_tuple("ParsedSVG").finish()
27    }
28}
29
30impl ParsedSVG {
31    pub fn size(&self) -> crate::graphics::IntSize {
32        let size = self.svg_tree.size().to_int_size();
33        [size.width(), size.height()].into()
34    }
35
36    pub fn cache_key(&self) -> ImageCacheKey {
37        self.cache_key.clone()
38    }
39
40    /// Renders the SVG with the specified size, if no size is specified, get the size from the image
41    #[allow(clippy::unnecessary_cast)] // Coord
42    pub fn render(
43        &self,
44        size: Option<euclid::Size2D<u32, PhysicalPx>>,
45    ) -> Result<SharedImageBuffer, usvg::Error> {
46        let tree = &self.svg_tree;
47
48        let (target_size, transform) = match size {
49            Some(size) => {
50                let target_size = tiny_skia::IntSize::from_wh(size.width, size.height)
51                    .ok_or(usvg::Error::InvalidSize)?;
52                let target_size = tree.size().to_int_size().scale_to(target_size);
53                let target_size_f = target_size.to_size();
54
55                let transform = tiny_skia::Transform::from_scale(
56                    target_size_f.width() as f32 / tree.size().width() as f32,
57                    target_size_f.height() as f32 / tree.size().height() as f32,
58                );
59                (target_size, transform)
60            }
61            None => (tree.size().to_int_size(), tiny_skia::Transform::default()),
62        };
63
64        let mut buffer = SharedPixelBuffer::new(target_size.width(), target_size.height());
65        let mut skia_buffer = tiny_skia::PixmapMut::from_bytes(
66            buffer.make_mut_bytes(),
67            target_size.width(),
68            target_size.height(),
69        )
70        .ok_or(usvg::Error::InvalidSize)?;
71
72        resvg::render(tree, transform, &mut skia_buffer);
73        Ok(SharedImageBuffer::RGBA8Premultiplied(buffer))
74    }
75}
76
77/// Resolves SVG `<text>` fonts through the shared [`sharedfontique::svg`] bridge
78/// against the running `SlintContext`'s collection, so SVG text uses the same fonts
79/// as the rest of the UI.
80/// Without a context the text stays unresolved, which never happens for a displayable
81/// image since the platform is up by then.
82///
83/// Gated on `shared-parley`: without Slint's text engine there is no text layout, so
84/// SVG text resolution would be moot.
85#[cfg(feature = "shared-parley")]
86fn svg_options() -> usvg::Options<'static> {
87    use i_slint_common::sharedfontique::{self, fontique};
88
89    fn find_font(
90        families: &[fontique::QueryFamily],
91        attributes: fontique::Attributes,
92        require_char: Option<char>,
93    ) -> Option<fontique::QueryFont> {
94        crate::context::GLOBAL_CONTEXT.with(|p| {
95            let ctx = p.get()?;
96            let mut font_context = ctx.font_context().try_borrow_mut().ok()?;
97            let parley::FontContext { collection, source_cache } = &mut font_context.inner;
98            sharedfontique::svg::query_font(
99                collection,
100                source_cache,
101                families,
102                attributes,
103                require_char,
104            )
105        })
106    }
107
108    sharedfontique::svg::options(find_font)
109}
110
111#[cfg(not(feature = "shared-parley"))]
112fn svg_options() -> usvg::Options<'static> {
113    usvg::Options::default()
114}
115
116#[cfg(not(target_arch = "wasm32"))]
117pub fn load_from_path(
118    path: &SharedString,
119    cache_key: ImageCacheKey,
120) -> Result<ParsedSVG, std::io::Error> {
121    let svg_data = std::fs::read(std::path::Path::new(&path.as_str()))?;
122
123    usvg::Tree::from_data(&svg_data, &svg_options())
124        .map(|svg| ParsedSVG { svg_tree: svg, cache_key })
125        .map_err(std::io::Error::other)
126}
127
128pub fn load_from_data(slice: &[u8], cache_key: ImageCacheKey) -> Result<ParsedSVG, usvg::Error> {
129    usvg::Tree::from_data(slice, &svg_options()).map(|svg| ParsedSVG { svg_tree: svg, cache_key })
130}