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