zng_ext_svg/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! SVG image support.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12use zng_app::AppExtension;
13use zng_ext_image::*;
14use zng_task::channel::IpcBytes;
15use zng_txt::{Txt, formatx};
16use zng_unit::{Px, PxDensity2d, PxDensityUnits as _, PxSize};
17
18/// Application extension that installs SVG handling.
19///
20/// This extension installs the [`SvgRenderCache`] in [`IMAGES`] on init.
21#[derive(Default)]
22#[non_exhaustive]
23pub struct SvgManager {}
24
25impl AppExtension for SvgManager {
26    fn init(&mut self) {
27        IMAGES.install_proxy(Box::new(SvgRenderCache::default()));
28    }
29}
30
31/// Image cache proxy that handlers SVG requests.
32#[derive(Default)]
33#[non_exhaustive]
34pub struct SvgRenderCache {}
35impl ImageCacheProxy for SvgRenderCache {
36    fn data(
37        &mut self,
38        key: &ImageHash,
39        data: &[u8],
40        format: &ImageDataFormat,
41        mode: ImageCacheMode,
42        downscale: Option<ImageDownscale>,
43        mask: Option<ImageMaskMode>,
44        is_loaded: bool,
45    ) -> Option<ImageVar> {
46        let data = match format {
47            ImageDataFormat::FileExtension(txt) if txt == "svg" || txt == "svgz" => SvgData::Raw(data.to_vec()),
48            ImageDataFormat::MimeType(txt) if txt == "image/svg+xml" => SvgData::Raw(data.to_vec()),
49            ImageDataFormat::Unknown => SvgData::Str(svg_data_from_unknown(data)?),
50            _ => return None,
51        };
52        let key = if is_loaded {
53            None // already cached, return image is internal
54        } else {
55            Some(*key)
56        };
57        Some(IMAGES.image_task(async move { load(data, downscale) }, mode, key, None, None, mask))
58    }
59
60    fn is_data_proxy(&self) -> bool {
61        true
62    }
63}
64
65enum SvgData {
66    Raw(Vec<u8>),
67    Str(String),
68}
69fn load(data: SvgData, downscale: Option<ImageDownscale>) -> ImageSource {
70    let options = resvg::usvg::Options::default();
71
72    let tree = match data {
73        SvgData::Raw(data) => resvg::usvg::Tree::from_data(&data, &options),
74        SvgData::Str(data) => resvg::usvg::Tree::from_str(&data, &options),
75    };
76    match tree {
77        Ok(tree) => {
78            let mut size = tree.size().to_int_size();
79            if let Some(d) = downscale {
80                let s = d.resize_dimensions(PxSize::new(Px(size.width() as _), Px(size.height() as _)));
81                match resvg::tiny_skia::IntSize::from_wh(s.width.0 as _, s.height.0 as _) {
82                    Some(s) => size = s,
83                    None => tracing::error!("cannot resize svg to zero size"),
84                }
85            }
86            let mut pixmap = match resvg::tiny_skia::Pixmap::new(size.width(), size.height()) {
87                Some(p) => p,
88                None => return error(formatx!("can't allocate pixmap for {:?} svg", size)),
89            };
90            resvg::render(&tree, resvg::tiny_skia::Transform::identity(), &mut pixmap.as_mut());
91            let size = PxSize::new(Px(pixmap.width() as _), Px(pixmap.height() as _));
92
93            let mut data = pixmap.take();
94            for pixel in data.chunks_exact_mut(4) {
95                pixel.swap(0, 2);
96            }
97
98            ImageSource::Data(
99                ImageHash::compute(&data),
100                IpcBytes::from_vec_blocking(data).expect("cannot allocate IpcBytes"),
101                ImageDataFormat::Bgra8 {
102                    size,
103                    density: Some(PxDensity2d::splat(options.dpi.ppi())),
104                },
105            )
106        }
107        Err(e) => error(formatx!("{e}")),
108    }
109}
110
111fn error(error: Txt) -> ImageSource {
112    ImageSource::Image(IMAGES.dummy(Some(error)))
113}
114
115fn svg_data_from_unknown(data: &[u8]) -> Option<String> {
116    if data.starts_with(&[0x1f, 0x8b]) {
117        // gzip magic number
118        let data = resvg::usvg::decompress_svgz(data).ok()?;
119        uncompressed_data_is_svg(&data)
120    } else {
121        uncompressed_data_is_svg(data)
122    }
123}
124fn uncompressed_data_is_svg(data: &[u8]) -> Option<String> {
125    let s = std::str::from_utf8(data).ok()?;
126    if s.contains("http://www.w3.org/2000/svg") {
127        Some(s.to_owned())
128    } else {
129        None
130    }
131}