use std::sync::Arc;
use svgtypes::{AspectRatio, Length};
use super::svgtree::{AId, SvgNode};
use super::{converter, OptionLog, Options};
use crate::{
ClipPath, Group, Image, ImageKind, ImageRendering, Node, NonZeroRect, Path, Size, Transform,
Tree, Visibility,
};
pub type ImageHrefDataResolverFn<'a> =
Box<dyn Fn(&str, Arc<Vec<u8>>, &Options) -> Option<ImageKind> + Send + Sync + 'a>;
pub type ImageHrefStringResolverFn<'a> =
Box<dyn Fn(&str, &Options) -> Option<ImageKind> + Send + Sync + 'a>;
pub struct ImageHrefResolver<'a> {
pub resolve_data: ImageHrefDataResolverFn<'a>,
pub resolve_string: ImageHrefStringResolverFn<'a>,
}
impl Default for ImageHrefResolver<'_> {
fn default() -> Self {
ImageHrefResolver {
resolve_data: ImageHrefResolver::default_data_resolver(),
resolve_string: ImageHrefResolver::default_string_resolver(),
}
}
}
impl ImageHrefResolver<'_> {
pub fn default_data_resolver() -> ImageHrefDataResolverFn<'static> {
Box::new(
move |mime: &str, data: Arc<Vec<u8>>, opts: &Options| match mime {
"image/jpg" | "image/jpeg" => Some(ImageKind::JPEG(data)),
"image/png" => Some(ImageKind::PNG(data)),
"image/gif" => Some(ImageKind::GIF(data)),
"image/svg+xml" => load_sub_svg(&data, opts),
"text/plain" => match get_image_data_format(&data) {
Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(data)),
Some(ImageFormat::PNG) => Some(ImageKind::PNG(data)),
Some(ImageFormat::GIF) => Some(ImageKind::GIF(data)),
_ => load_sub_svg(&data, opts),
},
_ => None,
},
)
}
pub fn default_string_resolver() -> ImageHrefStringResolverFn<'static> {
Box::new(move |href: &str, opts: &Options| {
let path = opts.get_abs_path(std::path::Path::new(href));
if path.exists() {
let data = match std::fs::read(&path) {
Ok(data) => data,
Err(_) => {
log::warn!("Failed to load '{}'. Skipped.", href);
return None;
}
};
match get_image_file_format(&path, &data) {
Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(Arc::new(data))),
Some(ImageFormat::PNG) => Some(ImageKind::PNG(Arc::new(data))),
Some(ImageFormat::GIF) => Some(ImageKind::GIF(Arc::new(data))),
Some(ImageFormat::SVG) => load_sub_svg(&data, opts),
_ => {
log::warn!("'{}' is not a PNG, JPEG, GIF or SVG(Z) image.", href);
None
}
}
} else {
log::warn!("'{}' is not a path to an image.", href);
None
}
})
}
}
impl std::fmt::Debug for ImageHrefResolver<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("ImageHrefResolver { .. }")
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum ImageFormat {
PNG,
JPEG,
GIF,
SVG,
}
pub(crate) fn convert(
node: SvgNode,
state: &converter::State,
cache: &mut converter::Cache,
parent: &mut Group,
) -> Option<()> {
let href = node
.try_attribute(AId::Href)
.log_none(|| log::warn!("Image lacks the 'xlink:href' attribute. Skipped."))?;
let kind = get_href_data(href, state)?;
let visibility: Visibility = node.find_attribute(AId::Visibility).unwrap_or_default();
let visible = visibility == Visibility::Visible;
let rendering_mode = node
.find_attribute(AId::ImageRendering)
.unwrap_or(state.opt.image_rendering);
let id = if state.parent_markers.is_empty() {
node.element_id().to_string()
} else {
String::new()
};
let actual_size = kind.actual_size()?;
let x = node.convert_user_length(AId::X, state, Length::zero());
let y = node.convert_user_length(AId::Y, state, Length::zero());
let mut width = node.convert_user_length(
AId::Width,
state,
Length::new_number(actual_size.width() as f64),
);
let mut height = node.convert_user_length(
AId::Height,
state,
Length::new_number(actual_size.height() as f64),
);
match (
node.attribute::<Length>(AId::Width),
node.attribute::<Length>(AId::Height),
) {
(Some(_), None) => {
height = actual_size.height() * (width / actual_size.width());
}
(None, Some(_)) => {
width = actual_size.width() * (height / actual_size.height());
}
_ => {}
};
let aspect: AspectRatio = node.attribute(AId::PreserveAspectRatio).unwrap_or_default();
let rect = NonZeroRect::from_xywh(x, y, width, height);
let rect = rect.log_none(|| log::warn!("Image has an invalid size. Skipped."))?;
convert_inner(
kind,
id,
visible,
rendering_mode,
aspect,
actual_size,
rect,
cache,
parent,
)
}
pub(crate) fn convert_inner(
kind: ImageKind,
id: String,
visible: bool,
rendering_mode: ImageRendering,
aspect: AspectRatio,
actual_size: Size,
rect: NonZeroRect,
cache: &mut converter::Cache,
parent: &mut Group,
) -> Option<()> {
let aligned_size = fit_view_box(actual_size, rect, aspect);
let (aligned_x, aligned_y) = crate::aligned_pos(
aspect.align,
rect.x(),
rect.y(),
rect.width() - aligned_size.width(),
rect.height() - aligned_size.height(),
);
let view_box = aligned_size.to_non_zero_rect(aligned_x, aligned_y);
let image_ts = Transform::from_row(
view_box.width() / actual_size.width(),
0.0,
0.0,
view_box.height() / actual_size.height(),
view_box.x(),
view_box.y(),
);
let abs_transform = parent.abs_transform.pre_concat(image_ts);
let abs_bounding_box = rect.transform(abs_transform)?;
let mut g = Group::empty();
g.id = id;
g.children.push(Node::Image(Box::new(Image {
id: String::new(),
visible,
size: actual_size,
rendering_mode,
kind,
abs_transform,
abs_bounding_box,
})));
g.transform = image_ts;
g.abs_transform = abs_transform;
g.calculate_bounding_boxes();
if aspect.slice {
let mut path = Path::new_simple(Arc::new(tiny_skia_path::PathBuilder::from_rect(
rect.to_rect(),
)))
.unwrap();
path.fill = Some(crate::Fill::default());
let mut clip = ClipPath::empty(cache.gen_clip_path_id());
clip.root.children.push(Node::Path(Box::new(path)));
let mut g2 = Group::empty();
std::mem::swap(&mut g.id, &mut g2.id);
g2.abs_transform = parent.abs_transform;
g2.clip_path = Some(Arc::new(clip));
g2.children.push(Node::Group(Box::new(g)));
g2.calculate_bounding_boxes();
parent.children.push(Node::Group(Box::new(g2)));
} else {
parent.children.push(Node::Group(Box::new(g)));
}
Some(())
}
pub(crate) fn get_href_data(href: &str, state: &converter::State) -> Option<ImageKind> {
if let Ok(url) = data_url::DataUrl::process(href) {
let (data, _) = url.decode_to_vec().ok()?;
let mime = format!(
"{}/{}",
url.mime_type().type_.as_str(),
url.mime_type().subtype.as_str()
);
(state.opt.image_href_resolver.resolve_data)(&mime, Arc::new(data), state.opt)
} else {
(state.opt.image_href_resolver.resolve_string)(href, state.opt)
}
}
fn get_image_file_format(path: &std::path::Path, data: &[u8]) -> Option<ImageFormat> {
let ext = path.extension().and_then(|e| e.to_str())?.to_lowercase();
if ext == "svg" || ext == "svgz" {
return Some(ImageFormat::SVG);
}
get_image_data_format(data)
}
fn get_image_data_format(data: &[u8]) -> Option<ImageFormat> {
match imagesize::image_type(data).ok()? {
imagesize::ImageType::Gif => Some(ImageFormat::GIF),
imagesize::ImageType::Jpeg => Some(ImageFormat::JPEG),
imagesize::ImageType::Png => Some(ImageFormat::PNG),
_ => None,
}
}
pub(crate) fn load_sub_svg(data: &[u8], opt: &Options) -> Option<ImageKind> {
let mut sub_opt = Options::default();
sub_opt.resources_dir = None;
sub_opt.dpi = opt.dpi;
sub_opt.font_size = opt.font_size;
sub_opt.languages = opt.languages.clone();
sub_opt.shape_rendering = opt.shape_rendering;
sub_opt.text_rendering = opt.text_rendering;
sub_opt.image_rendering = opt.image_rendering;
sub_opt.default_size = opt.default_size;
sub_opt.image_href_resolver = ImageHrefResolver {
resolve_data: Box::new(|_, _, _| None),
resolve_string: Box::new(|_, _| None),
};
#[cfg(feature = "text")]
{
sub_opt.fontdb = opt.fontdb.clone();
sub_opt.font_resolver = crate::FontResolver {
select_font: Box::new(|font, db| (opt.font_resolver.select_font)(font, db)),
select_fallback: Box::new(|c, used_fonts, db| {
(opt.font_resolver.select_fallback)(c, used_fonts, db)
}),
};
}
let tree = Tree::from_data(data, &sub_opt);
let tree = match tree {
Ok(tree) => tree,
Err(_) => {
log::warn!("Failed to load subsvg image.");
return None;
}
};
Some(ImageKind::SVG(tree))
}
fn fit_view_box(size: Size, rect: NonZeroRect, aspect: AspectRatio) -> Size {
let s = rect.size();
if aspect.align == svgtypes::Align::None {
s
} else if aspect.slice {
size.expand_to(s)
} else {
size.scale_to(s)
}
}