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