use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use image::{
ImageReader, RgbaImage,
imageops::{FilterType, overlay},
};
use resvg::{
tiny_skia::{Pixmap, Transform},
usvg::{Tree, roxmltree},
};
use crate::{ImgGenRendererError, Layer, PreserveAspect, Result};
use super::{ConcreteSize, Renderer};
impl Renderer<'_> {
pub async fn render_background(
&mut self,
layer: &Layer,
size: ConcreteSize,
canvas: &mut RgbaImage,
) -> Result<()> {
if let Some(l) = layer.background.as_ref() {
let mut img = None;
if let Some(i) = &l.image {
img = Some(if maybe_builtin_svg(i) {
self.load_svg(i, size, l.preserve_aspect).await?
} else {
self.load_image(i, size, l.preserve_aspect)?
});
}
if let Some(color) = &l.color {
let mut over_layer = RgbaImage::new(size.width, size.height);
Self::colorize(color, &mut over_layer, false);
if let Some(ref mut pic) = img {
overlay(pic, &over_layer, 0, 0);
} else {
img = Some(over_layer);
}
}
if let Some(img) = img {
overlay(canvas, &img, layer.offset.x.into(), layer.offset.y.into());
}
}
Ok(())
}
pub async fn render_icon(
&mut self,
layer: &Layer,
size: ConcreteSize,
canvas: &mut RgbaImage,
) -> Result<()> {
if let Some(l) = layer.icon.as_ref() {
let mut img = if maybe_builtin_svg(&l.image) {
self.load_svg(&l.image, size, l.preserve_aspect).await?
} else {
self.load_image(&l.image, size, l.preserve_aspect)?
};
if let Some(color) = &l.color {
Self::colorize(color, &mut img, true);
}
overlay(canvas, &img, layer.offset.x.into(), layer.offset.y.into());
}
Ok(())
}
fn find_image_path<P: AsRef<Path>>(&self, name: P) -> Option<PathBuf> {
for entry in self.image_search_paths {
if entry.is_file() && entry == name.as_ref() {
return Some(entry.to_path_buf());
} else {
let path = entry.join(&name);
if path.exists() {
return Some(path);
}
}
}
None
}
fn load_image(
&self,
path: &str,
size: ConcreteSize,
preserve_aspect: PreserveAspect,
) -> Result<RgbaImage> {
let resolved_path = self.find_image_path(path).unwrap_or(PathBuf::from(path));
let mut buf = ImageReader::open(&resolved_path)
.map_err(|source| ImgGenRendererError::OpenImageFailed {
path: path.to_string(),
source,
})?
.decode()
.map_err(|source| ImgGenRendererError::DecodeImageFailed {
path: path.to_string(),
source,
})?;
let width = size.width;
let height = size.height;
let og_width = buf.width();
let og_height = buf.height();
let (new_width, new_height) = match preserve_aspect {
PreserveAspect::Off => (width, height),
PreserveAspect::On => {
if og_width > og_height {
(
width,
(height as f32 * (og_height as f32 / og_width as f32) + 0.5) as u32,
)
} else {
(
(width as f32 * (og_width as f32 / og_height as f32) + 0.5) as u32,
height,
)
}
}
PreserveAspect::Width => {
let ratio = og_height as f32 / og_width as f32;
(width, (width as f32 * ratio + 0.5) as u32)
}
PreserveAspect::Height => {
let ratio = og_width as f32 / og_height as f32;
((height as f32 * ratio + 0.5) as u32, height)
}
};
buf = buf.resize_exact(new_width, new_height, FilterType::CatmullRom);
let mut img = RgbaImage::new(width, height);
let offset_x = (width as i64 - buf.width() as i64) / 2;
let offset_y = (height as i64 - buf.height() as i64) / 2;
overlay(&mut img, &RgbaImage::from(buf), offset_x, offset_y);
Ok(img)
}
async fn prefetch_svg_fonts<'a>(
&mut self,
svg_data: &'a str,
path: &str,
) -> Result<(roxmltree::Document<'a>, HashSet<String>)> {
let (doc, families) = super::fonts::extract_fonts_from_svg(svg_data).map_err(|source| {
ImgGenRendererError::ParseSvgXmlFailed {
path: path.to_string(),
source,
}
})?;
for family in &families {
let font = crate::Font::from_family_style(family.to_string(), None);
let query = super::fonts::to_font_query(&font);
let downloaded_paths = self.fontsource_client.download_font(&query).await?;
for path in &downloaded_paths {
self.register_font_path(path)?;
}
}
Ok((doc, families))
}
async fn load_svg(
&mut self,
path: &str,
size: ConcreteSize,
preserve_aspect: PreserveAspect,
) -> Result<RgbaImage> {
let tree = {
let svg_data = match load_builtin_svg_pack(path)? {
Some(data) => data,
None => {
let svg_path = PathBuf::from(path).with_extension("svg");
let p = self.find_image_path(&svg_path).unwrap_or(svg_path);
&std::fs::read_to_string(&p).map_err(|source| {
ImgGenRendererError::ReadSvgFailed {
path: path.to_string(),
source,
}
})?
}
};
let (svg_doc, _font_families) = self.prefetch_svg_fonts(svg_data, path).await?;
Tree::from_xmltree(&svg_doc, &self.svg_options).map_err(|source| {
ImgGenRendererError::ParseSvgFailed {
path: path.to_string(),
source,
}
})?
};
let width = size.width;
let height = size.height;
let og_width = tree.size().width();
let og_height = tree.size().height();
let (scale_x, scale_y) = match preserve_aspect {
PreserveAspect::Off => {
let scale_x = width as f32 / og_width;
let scale_y = height as f32 / og_height;
(scale_x, scale_y)
}
PreserveAspect::On => {
let ratio = if og_width > og_height {
og_width / width as f32
} else {
og_height / height as f32
};
(1.0 / ratio, 1.0 / ratio)
}
PreserveAspect::Width => {
let ratio = og_width / width as f32;
(1.0 / ratio, 1.0 / ratio)
}
PreserveAspect::Height => {
let ratio = og_height / height as f32;
(1.0 / ratio, 1.0 / ratio)
}
};
let mut pixmap = Pixmap::new((og_width * scale_x) as u32, (og_height * scale_y) as u32)
.ok_or(ImgGenRendererError::SvgScaledToZeroSize {
path: path.to_string(),
})?;
resvg::render(
&tree,
Transform::from_scale(scale_x, scale_y),
&mut pixmap.as_mut(),
);
let mut img = RgbaImage::new(width, height);
let offset_x = (width as i64 - pixmap.width() as i64) / 2;
let offset_y = (height as i64 - pixmap.height() as i64) / 2;
let svg = RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec())
.ok_or(ImgGenRendererError::RasterBufferConversionFailed {
shape: "svg",
width: pixmap.width(),
height: pixmap.height(),
})?;
overlay(&mut img, &svg, offset_x, offset_y);
Ok(img)
}
}
fn maybe_builtin_svg(name: &str) -> bool {
match PathBuf::from(name).extension() {
Some(ext) => ext.eq_ignore_ascii_case("svg"),
None => name
.split_once('/')
.map(|(icon_pkg, _)| {
matches!(icon_pkg, "material" | "simple" | "octicons" | "fontawesome")
})
.unwrap_or(false),
}
}
fn load_builtin_svg_pack(name: &str) -> Result<Option<&str>> {
if let Some((icon_pkg, slug)) = name.split_once('/') {
let svg_str = match icon_pkg {
"material" => material_design_icons_pack::get_icon(slug).map(|v| v.svg),
"simple" => simple_icons_pack::get_icon(slug).map(|v| v.svg),
"octicons" => octicons_pack::get_icon(slug).map(|v| v.svg),
"fontawesome" => fontawesome_free_pack::get_icon(slug).map(|v| v.svg),
_ => None,
};
return Ok(svg_str);
}
Ok(None)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use std::collections::HashSet;
use fontsource_downloader::FontSourceClient;
use parley::{FontContext, LayoutContext};
use resvg::usvg::Options;
use swash::scale::ScaleContext;
use super::*;
#[test]
fn find_non_existent_image() {
let renderer = Renderer {
image_search_paths: &[PathBuf::from("tests")],
svg_options: Options::default(),
font_cx: FontContext::default(),
layout_cx: LayoutContext::default(),
scale_cx: ScaleContext::default(),
loaded_font_paths: HashSet::new(),
fontsource_client: &FontSourceClient::new().unwrap(),
};
assert!(renderer.find_image_path("nonexistent.png").is_none());
}
#[test]
fn non_builtin_svg() {
assert!(load_builtin_svg_pack("non-existent-svg").unwrap().is_none());
}
}