1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use std::sync::Arc;
6
7use svgtypes::Length;
8
9use super::svgtree::{AId, SvgNode};
10use super::{converter, OptionLog, Options};
11use crate::{Group, Image, ImageKind, Node, NonZeroRect, Size, Tree, ViewBox};
12
13/// A shorthand for [ImageHrefResolver]'s data function.
14#[cfg(feature = "text")]
15pub type ImageHrefDataResolverFn =
16 Box<dyn Fn(&str, Arc<Vec<u8>>, &Options, &fontdb::Database) -> Option<ImageKind> + Send + Sync>;
17
18/// A shorthand for [ImageHrefResolver]'s data function.
19#[cfg(not(feature = "text"))]
20pub type ImageHrefDataResolverFn =
21 Box<dyn Fn(&str, Arc<Vec<u8>>, &Options) -> Option<ImageKind> + Send + Sync>;
22
23/// A shorthand for [ImageHrefResolver]'s string function.
24#[cfg(feature = "text")]
25pub type ImageHrefStringResolverFn =
26 Box<dyn Fn(&str, &Options, &fontdb::Database) -> Option<ImageKind> + Send + Sync>;
27
28/// A shorthand for [ImageHrefResolver]'s string function.
29#[cfg(not(feature = "text"))]
30pub type ImageHrefStringResolverFn = Box<dyn Fn(&str, &Options) -> Option<ImageKind> + Send + Sync>;
31
32/// An `xlink:href` resolver for `<image>` elements.
33///
34/// This type can be useful if you want to have an alternative `xlink:href` handling
35/// to the default one. For example, you can forbid access to local files (which is allowed by default)
36/// or add support for resolving actual URLs (usvg doesn't do any network requests).
37pub struct ImageHrefResolver {
38 /// Resolver function that will be used when `xlink:href` contains a
39 /// [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs).
40 ///
41 /// A function would be called with mime, decoded base64 data and parsing options.
42 pub resolve_data: ImageHrefDataResolverFn,
43
44 /// Resolver function that will be used to handle an arbitrary string in `xlink:href`.
45 pub resolve_string: ImageHrefStringResolverFn,
46}
47
48impl Default for ImageHrefResolver {
49 fn default() -> Self {
50 ImageHrefResolver {
51 resolve_data: ImageHrefResolver::default_data_resolver(),
52 resolve_string: ImageHrefResolver::default_string_resolver(),
53 }
54 }
55}
56
57impl ImageHrefResolver {
58 /// Creates a default
59 /// [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs)
60 /// resolver closure.
61 ///
62 /// base64 encoded data is already decoded.
63 ///
64 /// The default implementation would try to load JPEG, PNG, GIF, SVG and SVGZ types.
65 /// Note that it will simply match the `mime` or data's magic.
66 /// The actual images would not be decoded. It's up to the renderer.
67 pub fn default_data_resolver() -> ImageHrefDataResolverFn {
68 Box::new(
69 move |mime: &str,
70 data: Arc<Vec<u8>>,
71 opts: &Options,
72 #[cfg(feature = "text")] fontdb: &fontdb::Database| match mime {
73 "image/jpg" | "image/jpeg" => Some(ImageKind::JPEG(data)),
74 "image/png" => Some(ImageKind::PNG(data)),
75 "image/gif" => Some(ImageKind::GIF(data)),
76 "image/svg+xml" => load_sub_svg(
77 &data,
78 opts,
79 #[cfg(feature = "text")]
80 fontdb,
81 ),
82 "text/plain" => match get_image_data_format(&data) {
83 Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(data)),
84 Some(ImageFormat::PNG) => Some(ImageKind::PNG(data)),
85 Some(ImageFormat::GIF) => Some(ImageKind::GIF(data)),
86 _ => load_sub_svg(
87 &data,
88 opts,
89 #[cfg(feature = "text")]
90 fontdb,
91 ),
92 },
93 _ => None,
94 },
95 )
96 }
97
98 /// Creates a default string resolver.
99 ///
100 /// The default implementation treats an input string as a file path and tries to open.
101 /// If a string is an URL or something else it would be ignored.
102 ///
103 /// Paths have to be absolute or relative to the input SVG file or relative to
104 /// [Options::resources_dir](crate::Options::resources_dir).
105 pub fn default_string_resolver() -> ImageHrefStringResolverFn {
106 Box::new(
107 move |href: &str,
108 opts: &Options,
109 #[cfg(feature = "text")] fontdb: &fontdb::Database| {
110 let path = opts.get_abs_path(std::path::Path::new(href));
111
112 if path.exists() {
113 let data = match std::fs::read(&path) {
114 Ok(data) => data,
115 Err(_) => {
116 log::warn!("Failed to load '{}'. Skipped.", href);
117 return None;
118 }
119 };
120
121 match get_image_file_format(&path, &data) {
122 Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(Arc::new(data))),
123 Some(ImageFormat::PNG) => Some(ImageKind::PNG(Arc::new(data))),
124 Some(ImageFormat::GIF) => Some(ImageKind::GIF(Arc::new(data))),
125 Some(ImageFormat::SVG) => load_sub_svg(
126 &data,
127 opts,
128 #[cfg(feature = "text")]
129 fontdb,
130 ),
131 _ => {
132 log::warn!("'{}' is not a PNG, JPEG, GIF or SVG(Z) image.", href);
133 None
134 }
135 }
136 } else {
137 log::warn!("'{}' is not a path to an image.", href);
138 None
139 }
140 },
141 )
142 }
143}
144
145impl std::fmt::Debug for ImageHrefResolver {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.write_str(data:"ImageHrefResolver { .. }")
148 }
149}
150
151#[derive(Clone, Copy, PartialEq, Debug)]
152enum ImageFormat {
153 PNG,
154 JPEG,
155 GIF,
156 SVG,
157}
158
159pub(crate) fn convert(node: SvgNode, state: &converter::State, parent: &mut Group) -> Option<()> {
160 let href = node
161 .try_attribute(AId::Href)
162 .log_none(|| log::warn!("Image lacks the 'xlink:href' attribute. Skipped."))?;
163
164 let kind = get_href_data(href, state)?;
165
166 let visibility = node.find_attribute(AId::Visibility).unwrap_or_default();
167 let rendering_mode = node
168 .find_attribute(AId::ImageRendering)
169 .unwrap_or(state.opt.image_rendering);
170
171 let actual_size = match kind {
172 ImageKind::JPEG(ref data) | ImageKind::PNG(ref data) | ImageKind::GIF(ref data) => {
173 imagesize::blob_size(data)
174 .ok()
175 .and_then(|size| Size::from_wh(size.width as f32, size.height as f32))
176 .log_none(|| log::warn!("Image has an invalid size. Skipped."))?
177 }
178 ImageKind::SVG(ref svg) => svg.size,
179 };
180
181 let x = node.convert_user_length(AId::X, state, Length::zero());
182 let y = node.convert_user_length(AId::Y, state, Length::zero());
183 let mut width = node.convert_user_length(
184 AId::Width,
185 state,
186 Length::new_number(actual_size.width() as f64),
187 );
188 let mut height = node.convert_user_length(
189 AId::Height,
190 state,
191 Length::new_number(actual_size.height() as f64),
192 );
193
194 match (
195 node.attribute::<Length>(AId::Width),
196 node.attribute::<Length>(AId::Height),
197 ) {
198 (Some(_), None) => {
199 // Only width was defined, so we need to scale height accordingly.
200 height = actual_size.height() * (width / actual_size.width());
201 }
202 (None, Some(_)) => {
203 // Only height was defined, so we need to scale width accordingly.
204 width = actual_size.width() * (height / actual_size.height());
205 }
206 _ => {}
207 };
208
209 let rect = NonZeroRect::from_xywh(x, y, width, height);
210 let rect = rect.log_none(|| log::warn!("Image has an invalid size. Skipped."))?;
211
212 let view_box = ViewBox {
213 rect,
214 aspect: node.attribute(AId::PreserveAspectRatio).unwrap_or_default(),
215 };
216
217 // Nodes generated by markers must not have an ID. Otherwise we would have duplicates.
218 let id = if state.parent_markers.is_empty() {
219 node.element_id().to_string()
220 } else {
221 String::new()
222 };
223
224 let abs_bounding_box = view_box.rect.transform(parent.abs_transform)?;
225
226 parent.children.push(Node::Image(Box::new(Image {
227 id,
228 visibility,
229 view_box,
230 rendering_mode,
231 kind,
232 abs_transform: parent.abs_transform,
233 abs_bounding_box,
234 })));
235
236 Some(())
237}
238
239pub(crate) fn get_href_data(href: &str, state: &converter::State) -> Option<ImageKind> {
240 if let Ok(url) = data_url::DataUrl::process(href) {
241 let (data, _) = url.decode_to_vec().ok()?;
242
243 let mime = format!(
244 "{}/{}",
245 url.mime_type().type_.as_str(),
246 url.mime_type().subtype.as_str()
247 );
248
249 (state.opt.image_href_resolver.resolve_data)(
250 &mime,
251 Arc::new(data),
252 state.opt,
253 #[cfg(feature = "text")]
254 state.fontdb,
255 )
256 } else {
257 (state.opt.image_href_resolver.resolve_string)(
258 href,
259 state.opt,
260 #[cfg(feature = "text")]
261 state.fontdb,
262 )
263 }
264}
265
266/// Checks that file has a PNG, a GIF or a JPEG magic bytes.
267/// Or an SVG(Z) extension.
268fn get_image_file_format(path: &std::path::Path, data: &[u8]) -> Option<ImageFormat> {
269 let ext: String = path.extension().and_then(|e: &OsStr| e.to_str())?.to_lowercase();
270 if ext == "svg" || ext == "svgz" {
271 return Some(ImageFormat::SVG);
272 }
273
274 get_image_data_format(data)
275}
276
277/// Checks that file has a PNG, a GIF or a JPEG magic bytes.
278fn get_image_data_format(data: &[u8]) -> Option<ImageFormat> {
279 match imagesize::image_type(header:data).ok()? {
280 imagesize::ImageType::Gif => Some(ImageFormat::GIF),
281 imagesize::ImageType::Jpeg => Some(ImageFormat::JPEG),
282 imagesize::ImageType::Png => Some(ImageFormat::PNG),
283 _ => None,
284 }
285}
286
287/// Tries to load the `ImageData` content as an SVG image.
288///
289/// Unlike `Tree::from_*` methods, this one will also remove all `image` elements
290/// from the loaded SVG, as required by the spec.
291pub(crate) fn load_sub_svg(
292 data: &[u8],
293 opt: &Options,
294 #[cfg(feature = "text")] fontdb: &fontdb::Database,
295) -> Option<ImageKind> {
296 let mut sub_opt = Options::default();
297 sub_opt.resources_dir = None;
298 sub_opt.dpi = opt.dpi;
299 sub_opt.font_size = opt.font_size;
300 sub_opt.languages = opt.languages.clone();
301 sub_opt.shape_rendering = opt.shape_rendering;
302 sub_opt.text_rendering = opt.text_rendering;
303 sub_opt.image_rendering = opt.image_rendering;
304 sub_opt.default_size = opt.default_size;
305
306 // The referenced SVG image cannot have any 'image' elements by itself.
307 // Not only recursive. Any. Don't know why.
308 sub_opt.image_href_resolver = ImageHrefResolver {
309 resolve_data: Box::new(|_, _, _, #[cfg(feature = "text")] _| None),
310 resolve_string: Box::new(|_, _, #[cfg(feature = "text")] _| None),
311 };
312
313 let tree = Tree::from_data(
314 data,
315 &sub_opt,
316 #[cfg(feature = "text")]
317 fontdb,
318 );
319 let tree = match tree {
320 Ok(tree) => tree,
321 Err(_) => {
322 log::warn!("Failed to load subsvg image.");
323 return None;
324 }
325 };
326
327 Some(ImageKind::SVG(tree))
328}
329