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 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#[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) }
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.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 let header_len = 3.kilobytes().0 as usize;
175
176 if buf == [0x1f, 0x8b] {
177 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 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}