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//! This extension installs a [`IMAGES`] extension on init that handles SVG rendering.
7//!
8//! # Crate
9//!
10#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
11#![warn(unused_extern_crates)]
12#![warn(missing_docs)]
13
14use zng_app::{APP, hn};
15use zng_ext_image::*;
16use zng_task::channel::IpcBytes;
17use zng_txt::{Txt, formatx};
18use zng_unit::{ByteLength, Px, PxDensity2d, PxDensityUnits as _, PxSize};
19use zng_var::const_var;
20
21zng_env::on_process_start!(|args| {
22    if args.yield_until_app() {
23        return;
24    }
25
26    APP.on_init(hn!(|_| {
27        tracing::trace!("register SVG extension");
28        IMAGES.extend(Box::new(SvgRenderExtension::default()));
29    }));
30});
31
32/// Image service extension that handlers SVG requests.
33#[derive(Default)]
34#[non_exhaustive]
35pub struct SvgRenderExtension {}
36impl ImagesExtension for SvgRenderExtension {
37    fn image_data(
38        &mut self,
39        max_decoded_len: zng_unit::ByteLength,
40        _key: &ImageHash,
41        data: &IpcBytes,
42        format: &ImageDataFormat,
43        options: &ImageOptions,
44    ) -> Option<ImageVar> {
45        let data = match format {
46            ImageDataFormat::FileExtension(txt) if txt == "svg" || txt == "svgz" => SvgData::Raw(data.to_vec()),
47            ImageDataFormat::MimeType(txt) if txt == "image/svg+xml" => SvgData::Raw(data.to_vec()),
48            ImageDataFormat::Unknown => SvgData::Str(svg_data_from_unknown(data)?),
49            _ => return None,
50        };
51        tracing::trace!("svg request intercepted");
52        let mut options = options.clone();
53        let downscale = options.downscale.take();
54        options.cache_mode = ImageCacheMode::Ignore;
55        let limits = ImageLimits::none().with_max_decoded_len(max_decoded_len);
56        Some(IMAGES.image_task(async move { load_render(max_decoded_len, data, downscale) }, options, Some(limits)))
57    }
58
59    fn available_formats(&self, formats: &mut Vec<ImageFormat>) {
60        let svg = ImageFormat::from_static2("SVG", "svg+xml", "svg", "", ImageFormatCapability::empty());
61        formats.push(svg);
62    }
63}
64
65enum SvgData {
66    Raw(Vec<u8>),
67    Str(String),
68}
69fn load_render(max_decoded_len: ByteLength, data: SvgData, downscale: Option<ImageDownscaleMode>) -> 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            let mut entry_sizes = vec![];
80
81            fn to_skia_size(size: PxSize) -> Option<resvg::tiny_skia::IntSize> {
82                match resvg::tiny_skia::IntSize::from_wh(size.width.0 as _, size.height.0 as _) {
83                    Some(s) => Some(s),
84                    None => {
85                        tracing::error!("cannot resize svg to zero size");
86                        None
87                    }
88                }
89            }
90            if let Some(d) = downscale {
91                let size_px = PxSize::new(Px(size.width() as _), Px(size.height() as _));
92
93                let (full_size, entries) = d.sizes(size_px, &[]);
94                size = full_size.and_then(to_skia_size).unwrap_or(size);
95
96                for entry in entries {
97                    if let Some(s) = to_skia_size(entry) {
98                        entry_sizes.push(s);
99                    }
100                }
101            }
102
103            let render = |size: resvg::tiny_skia::IntSize| -> ImageSource {
104                if size.width() as usize * size.height() as usize * 4 > max_decoded_len.bytes() {
105                    return error(formatx!("cannot render svg, would exceed max {max_decoded_len} allowed"));
106                }
107                let mut data = match IpcBytes::new_mut_blocking(size.width() as usize * size.height() as usize * 4) {
108                    Ok(b) => b,
109                    Err(e) => return error(formatx!("can't allocate bytes for {size:?} svg, {e}")),
110                };
111                let mut pixmap = match resvg::tiny_skia::PixmapMut::from_bytes(&mut data, size.width(), size.height()) {
112                    Some(p) => p,
113                    None => return error(formatx!("can't allocate pixmap for {:?} svg", size)),
114                };
115                resvg::render(&tree, resvg::tiny_skia::Transform::identity(), &mut pixmap);
116
117                let size = PxSize::new(Px(pixmap.width() as _), Px(pixmap.height() as _));
118                for rgba in data.chunks_exact_mut(4) {
119                    // rgba to bgra
120                    rgba.swap(0, 2);
121                }
122
123                ImageSource::Data(
124                    ImageHash::compute(&data),
125                    match data.finish_blocking() {
126                        Ok(b) => b,
127                        Err(e) => return error(formatx!("cannot finish ipc bytes allocation, {e}")),
128                    },
129                    ImageDataFormat::Bgra8 {
130                        size,
131                        density: Some(PxDensity2d::splat(options.dpi.ppi())),
132                        original_color_type: ColorType::RGBA8,
133                    },
134                )
135            };
136
137            let primary = render(size);
138            if entry_sizes.is_empty() {
139                primary
140            } else {
141                let entries = entry_sizes
142                    .into_iter()
143                    .map(|s| (ImageEntryKind::Reduced { synthetic: true }, render(s)))
144                    .collect();
145                ImageSource::Entries {
146                    primary: Box::new(primary),
147                    entries,
148                }
149            }
150        }
151        Err(e) => error(formatx!("{e}")),
152    }
153}
154
155fn error(error: Txt) -> ImageSource {
156    ImageSource::Image(const_var(ImageEntry::new_error(error)))
157}
158
159fn svg_data_from_unknown(data: &[u8]) -> Option<String> {
160    if data.starts_with(&[0x1f, 0x8b]) {
161        // gzip magic number
162        let data = resvg::usvg::decompress_svgz(data).ok()?;
163        uncompressed_data_is_svg(&data)
164    } else {
165        uncompressed_data_is_svg(data)
166    }
167}
168fn uncompressed_data_is_svg(data: &[u8]) -> Option<String> {
169    let s = std::str::from_utf8(data).ok()?;
170    if s.contains("http://www.w3.org/2000/svg") {
171        Some(s.to_owned())
172    } else {
173        None
174    }
175}