Skip to main content

usvg/parser/
image.rs

1// Copyright 2018 the Resvg Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use std::sync::Arc;
5
6use svgtypes::{AspectRatio, Length};
7
8use super::svgtree::{AId, SvgNode};
9use super::{OptionLog, Options, converter};
10use 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.
16pub 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.
20pub 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).
28pub 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
39impl 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
48impl 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
117impl std::fmt::Debug for ImageHrefResolver<'_> {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.write_str("ImageHrefResolver { .. }")
120    }
121}
122
123#[derive(Clone, Copy, PartialEq, Debug)]
124enum ImageFormat {
125    PNG,
126    JPEG,
127    GIF,
128    WEBP,
129    SVG,
130}
131
132pub(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
206pub(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 = view_box.transform(parent.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        clip.root.calculate_bounding_boxes();
265
266        // Clip path should not be affected by the image viewbox transform.
267        // The final structure should look like:
268        // <g clip-path="url(#clipPath1)">
269        //     <g transform="matrix(1 0 0 1 10 20)">
270        //         <image/>
271        //     </g>
272        // </g>
273
274        let mut g2 = Group::empty();
275        std::mem::swap(&mut g.id, &mut g2.id);
276        g2.abs_transform = parent.abs_transform;
277        g2.clip_path = Some(Arc::new(clip));
278        g2.children.push(Node::Group(Box::new(g)));
279        g2.calculate_bounding_boxes();
280
281        // With `slice`, the image is scaled to *cover* the viewport and the
282        // overflow is cut away by the rectangular clip above. The bounding box
283        // of the element is therefore its `rect` (the viewport), not the
284        // larger scaled-to-cover image that `calculate_bounding_boxes` derives
285        // from the children. Without this correction an `objectBoundingBox`
286        // clip/mask/filter applied to the `<image>` would be resolved against
287        // the oversized image. See https://github.com/linebender/resvg/issues/1034
288        let bbox = rect.to_rect();
289        g2.bounding_box = bbox;
290        g2.stroke_bounding_box = bbox;
291        if let Some(abs) = bbox.transform(g2.abs_transform) {
292            g2.abs_bounding_box = abs;
293            g2.abs_stroke_bounding_box = abs;
294        }
295
296        parent.children.push(Node::Group(Box::new(g2)));
297    } else {
298        parent.children.push(Node::Group(Box::new(g)));
299    }
300
301    Some(())
302}
303
304pub(crate) fn get_href_data(href: &str, state: &converter::State) -> Option<ImageKind> {
305    if let Ok(url) = data_url::DataUrl::process(href) {
306        let (data, _) = url.decode_to_vec().ok()?;
307
308        let mime = format!(
309            "{}/{}",
310            url.mime_type().type_.as_str(),
311            url.mime_type().subtype.as_str()
312        );
313
314        (state.opt.image_href_resolver.resolve_data)(&mime, Arc::new(data), state.opt)
315    } else {
316        (state.opt.image_href_resolver.resolve_string)(href, state.opt)
317    }
318}
319
320/// Checks that file has a PNG, a GIF, a JPEG or a WebP magic bytes.
321/// Or an SVG(Z) extension.
322fn get_image_file_format(path: &std::path::Path, data: &[u8]) -> Option<ImageFormat> {
323    let ext = path.extension().and_then(|e| e.to_str())?.to_lowercase();
324    if ext == "svg" || ext == "svgz" {
325        return Some(ImageFormat::SVG);
326    }
327
328    get_image_data_format(data)
329}
330
331/// Checks that file has a PNG, a GIF, a JPEG or a WebP magic bytes.
332fn get_image_data_format(data: &[u8]) -> Option<ImageFormat> {
333    match imagesize::image_type(data).ok()? {
334        imagesize::ImageType::Gif => Some(ImageFormat::GIF),
335        imagesize::ImageType::Jpeg => Some(ImageFormat::JPEG),
336        imagesize::ImageType::Png => Some(ImageFormat::PNG),
337        imagesize::ImageType::Webp => Some(ImageFormat::WEBP),
338        _ => None,
339    }
340}
341
342/// Tries to load the `ImageData` content as an SVG image or emits a warning and returns `None`.
343pub(crate) fn load_sub_svg(data: &[u8], opt: &Options) -> Option<ImageKind> {
344    match Tree::from_data_nested(data, opt) {
345        Ok(tree) => Some(ImageKind::SVG(tree)),
346        Err(_) => {
347            log::warn!("Failed to load nested SVG image.");
348            None
349        }
350    }
351}
352
353/// Fits size into a viewbox.
354fn fit_view_box(size: Size, rect: NonZeroRect, aspect: AspectRatio) -> Size {
355    let s = rect.size();
356
357    if aspect.align == svgtypes::Align::None {
358        s
359    } else if aspect.slice {
360        size.expand_to(s)
361    } else {
362        size.scale_to(s)
363    }
364}