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")))]
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#[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.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 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}