1use 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
20pub type ImageHrefDataResolverFn<'a> =
22 Box<dyn Fn(&str, Arc<Vec<u8>>, &Options) -> Option<ImageKind> + Send + Sync + 'a>;
23
24pub type ImageHrefStringResolverFn<'a> =
26 Box<dyn Fn(&str, &Options) -> Option<ImageKind> + Send + Sync + 'a>;
27
28pub struct ImageHrefResolver<'a> {
34 pub resolve_data: ImageHrefDataResolverFn<'a>,
39
40 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 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 #[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 #[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 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 height = actual_size.height() * (width / actual_size.width());
203 }
204 (None, Some(_)) => {
205 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 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 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#[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
339fn 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
350pub(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
361fn 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}