usvg/parser/
image.rs

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