Skip to main content

ai_usvg/parser/
image.rs

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