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