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 | |
5 | use std::sync::Arc; |
6 | |
7 | use svgtypes::Length; |
8 | |
9 | use super::svgtree::{AId, SvgNode}; |
10 | use super::{converter, OptionLog, Options}; |
11 | use crate::{Group, Image, ImageKind, Node, NonZeroRect, Size, Tree, ViewBox}; |
12 | |
13 | /// A shorthand for [ImageHrefResolver]'s data function. |
14 | #[cfg (feature = "text" )] |
15 | pub 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" ))] |
20 | pub 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" )] |
25 | pub 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" ))] |
30 | pub 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). |
37 | pub 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 | |
48 | impl 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 | |
57 | impl 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 | |
145 | impl 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)] |
152 | enum ImageFormat { |
153 | PNG, |
154 | JPEG, |
155 | GIF, |
156 | SVG, |
157 | } |
158 | |
159 | pub(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 | |
239 | pub(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. |
268 | fn 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. |
278 | fn 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. |
291 | pub(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 | |