Skip to main content

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 std::io::{self, Read, Seek};
15
16use zng_app::{APP, hn};
17use zng_ext_image::*;
18use zng_task::channel::{IpcBytesMut, IpcReadBlocking, IpcReadHandle};
19use zng_txt::{Txt, formatx};
20use zng_unit::{ByteLength, ByteUnits as _, Px, PxDensity2d, PxDensityUnits as _, PxSize};
21use zng_var::const_var;
22
23zng_env::on_process_start!(|args| {
24    if args.yield_until_app() {
25        return;
26    }
27
28    APP.on_init(hn!(|_| {
29        tracing::trace!("register SVG extension");
30        IMAGES.extend(Box::new(SvgRenderExtension::default()));
31    }));
32});
33
34/// Image service extension that handlers SVG requests.
35#[derive(Default)]
36#[non_exhaustive]
37pub struct SvgRenderExtension {}
38impl ImagesExtension for SvgRenderExtension {
39    fn image_data(
40        &mut self,
41        max_decoded_len: zng_unit::ByteLength,
42        _key: &ImageHash,
43        data: &IpcReadHandle,
44        format: &ImageDataFormat,
45        options: &ImageOptions,
46    ) -> Option<ImageVar> {
47        let data = match format {
48            ImageDataFormat::FileExtension(txt) if txt == "svg" || txt == "svgz" => SvgData::Raw(data.duplicate().ok()?),
49            ImageDataFormat::MimeType(txt) if txt == "image/svg+xml" => SvgData::Raw(data.duplicate().ok()?),
50            ImageDataFormat::Unknown => SvgData::Str(svg_data_from_unknown(data)?),
51            _ => return None,
52        };
53        tracing::trace!("svg request intercepted");
54        let mut options = options.clone();
55        let downscale = options.downscale.take();
56        options.cache_mode = ImageCacheMode::Ignore;
57        let limits = ImageLimits::none().with_max_decoded_len(max_decoded_len);
58        Some(IMAGES.image_task(async move { load_render(max_decoded_len, data, downscale) }, options, Some(limits)))
59    }
60
61    fn available_formats(&self, formats: &mut Vec<ImageFormat>) {
62        let svg = ImageFormat::from_static2("SVG", "svg+xml", "svg", "", ImageFormatCapability::empty());
63        formats.push(svg);
64    }
65}
66
67enum SvgData {
68    Raw(IpcReadHandle),
69    Str(String),
70}
71fn load_render(max_decoded_len: ByteLength, data: SvgData, downscale: Option<ImageDownscaleMode>) -> ImageSource {
72    let options = resvg::usvg::Options::default();
73
74    let tree = match data {
75        SvgData::Raw(data) => match data.read_to_bytes_blocking() {
76            Ok(data) => resvg::usvg::Tree::from_data(&data, &options),
77            Err(e) => {
78                tracing::error!("cannot read svg image data, {e}");
79                Err(resvg::usvg::Error::NotAnUtf8Str) // no custom error branch
80            }
81        },
82        SvgData::Str(data) => resvg::usvg::Tree::from_str(&data, &options),
83    };
84    match tree {
85        Ok(tree) => {
86            let mut size = tree.size().to_int_size();
87            let mut entry_sizes = vec![];
88
89            fn to_skia_size(size: PxSize) -> Option<resvg::tiny_skia::IntSize> {
90                match resvg::tiny_skia::IntSize::from_wh(size.width.0 as _, size.height.0 as _) {
91                    Some(s) => Some(s),
92                    None => {
93                        tracing::error!("cannot resize svg to zero size");
94                        None
95                    }
96                }
97            }
98            if let Some(d) = downscale {
99                let size_px = PxSize::new(Px(size.width() as _), Px(size.height() as _));
100
101                let (full_size, entries) = d.sizes(size_px, &[]);
102                size = full_size.and_then(to_skia_size).unwrap_or(size);
103
104                for entry in entries {
105                    if let Some(s) = to_skia_size(entry) {
106                        entry_sizes.push(s);
107                    }
108                }
109            }
110
111            let render = |size: resvg::tiny_skia::IntSize| -> ImageSource {
112                if size.width() as u64 * size.height() as u64 * 4 > max_decoded_len.bytes() {
113                    return error(formatx!("cannot render svg, would exceed max {max_decoded_len} allowed"));
114                }
115                let mut data = match IpcBytesMut::new_blocking(size.width() as usize * size.height() as usize * 4) {
116                    Ok(b) => b,
117                    Err(e) => return error(formatx!("can't allocate bytes for {size:?} svg, {e}")),
118                };
119                let mut pixmap = match resvg::tiny_skia::PixmapMut::from_bytes(&mut data, size.width(), size.height()) {
120                    Some(p) => p,
121                    None => return error(formatx!("can't allocate pixmap for {:?} svg", size)),
122                };
123                resvg::render(&tree, resvg::tiny_skia::Transform::identity(), &mut pixmap);
124
125                let size = PxSize::new(Px(pixmap.width() as _), Px(pixmap.height() as _));
126                for rgba in data.chunks_exact_mut(4) {
127                    // rgba to bgra
128                    rgba.swap(0, 2);
129                }
130
131                ImageSource::Data(
132                    ImageHash::compute(&data),
133                    match data.finish_blocking() {
134                        Ok(b) => b,
135                        Err(e) => return error(formatx!("cannot finish ipc bytes allocation, {e}")),
136                    },
137                    ImageDataFormat::Bgra8 {
138                        size,
139                        density: Some(PxDensity2d::splat(options.dpi.ppi())),
140                        original_color_type: ColorType::RGBA8,
141                    },
142                )
143            };
144
145            let primary = render(size);
146            if entry_sizes.is_empty() {
147                primary
148            } else {
149                let entries = entry_sizes
150                    .into_iter()
151                    .map(|s| (ImageEntryKind::Reduced { synthetic: true }, render(s)))
152                    .collect();
153                ImageSource::Entries {
154                    primary: Box::new(primary),
155                    entries,
156                }
157            }
158        }
159        Err(e) => error(formatx!("{e}")),
160    }
161}
162
163fn error(error: Txt) -> ImageSource {
164    ImageSource::Image(const_var(ImageEntry::new_error(error)))
165}
166
167fn svg_data_from_unknown(data: &IpcReadHandle) -> Option<String> {
168    let mut data = data.duplicate().ok()?.read_blocking().ok()?;
169    let mut buf = [0u8; 2];
170    data.read_exact(&mut buf).ok()?;
171    data.seek(io::SeekFrom::Start(0)).ok()?;
172
173    // 3KB should allow for some comments at beginning before <svg
174    let header_len = 3.kilobytes().0 as usize;
175
176    if buf == [0x1f, 0x8b] {
177        // gzip magic number
178        // resvg::usvg::decompress_svgz(&[u8]) uses flate2::read::GzDecoder
179
180        let mut data = flate2::read::GzDecoder::new(data);
181        let mut buf = vec![];
182
183        data.by_ref().take(header_len as u64).read_to_end(&mut buf).ok()?;
184        find_open_svg(&buf)?;
185        data.read_to_end(&mut buf).ok()?;
186        String::from_utf8(buf).ok()
187    } else {
188        match data {
189            IpcReadBlocking::File(mut r) => {
190                let len = r.get_mut().metadata().ok()?.len();
191                let mut buf = String::with_capacity(usize::try_from(len).ok()?);
192                r.read_to_string(&mut buf).ok()?;
193                Some(buf)
194            }
195            IpcReadBlocking::Bytes(b) => {
196                let b = b.get_ref();
197                let header_len = b.len().min(header_len);
198                find_open_svg(&b[..header_len])?;
199                Some(str::from_utf8(b).ok()?.to_owned())
200            }
201            _ => None,
202        }
203    }
204}
205fn find_open_svg(buf: &[u8]) -> Option<usize> {
206    let len = buf.len().saturating_sub(3);
207
208    for i in 0..len {
209        if buf[i] == b'<' {
210            // ASCII lowercase via OR 0x20
211            let b1 = buf[i + 1] | 0x20;
212            let b2 = buf[i + 2] | 0x20;
213            let b3 = buf[i + 3] | 0x20;
214            if b1 == b's' && b2 == b'v' && b3 == b'g' {
215                return Some(i);
216            }
217        }
218    }
219
220    None
221}