| 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 | |