mod debug;
mod ellipse;
pub(super) mod fonts;
mod images;
mod mask;
mod polygon;
mod rectangle;
mod typography;
use std::{collections::HashSet, path::PathBuf};
use fontsource_downloader::FontSourceClient;
use image::{ImageBuffer, Pixel, RgbaImage, imageops::overlay};
use parley::{FontContext, LayoutContext};
use resvg::{
tiny_skia::{FillRule, Paint, Path, Pixmap, Stroke, Transform},
usvg::Options,
};
use swash::scale::ScaleContext;
use crate::{
Border, ColorKind, ImgGenRendererError, Layer, LayerOffset, Result, Size,
validators::{HEIGHT, WIDTH},
};
pub struct Renderer<'a> {
svg_options: Options<'a>,
font_cx: FontContext,
layout_cx: LayoutContext<typography::TextBrush>,
scale_cx: ScaleContext,
loaded_font_paths: HashSet<PathBuf>,
fontsource_client: &'a FontSourceClient,
external_resource_paths: &'a [PathBuf],
}
impl<'a> Renderer<'a> {
pub fn new(
options: Options<'a>,
fontsource_client: &'a FontSourceClient,
external_resource_paths: &'a [PathBuf],
) -> Self {
Renderer {
svg_options: options,
font_cx: FontContext::new(),
layout_cx: LayoutContext::new(),
scale_cx: ScaleContext::new(),
loaded_font_paths: HashSet::new(),
fontsource_client,
external_resource_paths,
}
}
fn colorize_masked(
color: &ColorKind,
img: &mut ImageBuffer<image::Rgba<u8>, Vec<u8>>,
mask: Option<&[u8]>,
only_visible: bool,
) {
let (w, _h) = (img.width(), img.height());
for (x, y, pixel) in img.enumerate_pixels_mut() {
let old_color = pixel.0;
if only_visible && old_color[3] == 0 {
continue;
}
let idx = (y * w + x) as usize;
if let Some(mask) = mask {
let mask_alpha = mask[idx];
if mask_alpha == 0 {
continue;
}
let (r, g, b, a) = color.get_color_tuple_at(x, y);
let final_alpha = ((mask_alpha as u16 * a as u16) / 255) as u8;
pixel.blend(&image::Rgba([r, g, b, final_alpha]));
} else {
let tup = color.get_color_tuple_at(x, y);
pixel.0 = [tup.0, tup.1, tup.2, tup.3];
}
}
}
fn colorize(
color: &ColorKind,
img: &mut ImageBuffer<image::Rgba<u8>, Vec<u8>>,
only_visible: bool,
) {
Self::colorize_masked(color, img, None, only_visible)
}
fn render_shape(
path: Path,
layer_color: &ColorKind,
shape_size: ConcreteSize,
layer_offset: &LayerOffset,
border: &Option<Border>,
canvas: &mut RgbaImage,
) -> Result<()> {
let mut pixmap = Pixmap::new(shape_size.width, shape_size.height).ok_or(
ImgGenRendererError::PixmapAllocationFailed {
shape: "shape",
width: shape_size.width,
height: shape_size.height,
},
)?;
let mut paint = Paint::default();
paint.set_color_rgba8(255, 255, 255, 255);
pixmap.fill_path(
&path,
&paint,
FillRule::EvenOdd,
Transform::identity(),
None,
);
let mut body = RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec())
.ok_or(ImgGenRendererError::RasterBufferConversionFailed {
shape: "shape",
width: pixmap.width(),
height: pixmap.height(),
})?;
Self::colorize(layer_color, &mut body, true);
overlay(canvas, &body, layer_offset.x.into(), layer_offset.y.into());
if let Some(b) = border {
let stroke = Stroke {
width: b.width.get() as f32,
..Default::default()
};
pixmap = Pixmap::new(shape_size.width, shape_size.height).ok_or(
ImgGenRendererError::PixmapAllocationFailed {
shape: "shape border",
width: shape_size.width,
height: shape_size.height,
},
)?;
pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
let mut border =
RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec())
.ok_or(ImgGenRendererError::RasterBufferConversionFailed {
shape: "shape border",
width: pixmap.width(),
height: pixmap.height(),
})?;
Self::colorize(&b.color, &mut border, true);
overlay(
canvas,
&border,
layer_offset.x.into(),
layer_offset.y.into(),
);
}
Ok(())
}
pub async fn render_layer(&mut self, layer: &Layer, canvas: &mut RgbaImage) -> Result<()> {
let layout_size = ConcreteSize::for_canvas(canvas);
let size = ConcreteSize::for_layer(layer.size, layout_size);
if let Some(layer_mask) = layer.mask.as_ref() {
let mut layer_canvas = RgbaImage::new(canvas.width(), canvas.height());
self.render_layer_unmasked(layer, size, &mut layer_canvas)
.await?;
self.apply_layer_mask(layer, layer_mask, &mut layer_canvas)
.await?;
overlay(canvas, &layer_canvas, 0, 0);
Ok(())
} else {
self.render_layer_unmasked(layer, size, canvas).await
}
}
pub(super) async fn render_layer_unmasked(
&mut self,
layer: &Layer,
size: ConcreteSize,
canvas: &mut RgbaImage,
) -> Result<()> {
self.render_background(layer, size, canvas).await?;
self.render_rectangle(layer, size, canvas)?;
self.render_ellipse(layer, size, canvas)?;
self.render_polygon(layer, size, canvas)?;
self.render_icon(layer, size, canvas).await?;
self.render_typography(layer, size, canvas).await?;
Ok(())
}
pub(super) fn find_ext_resource_path<P: AsRef<std::path::Path>>(
&self,
name: P,
) -> Option<PathBuf> {
for entry in self.external_resource_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
}
}
#[derive(Copy, Clone)]
pub struct ConcreteSize {
width: u32,
height: u32,
}
impl ConcreteSize {
pub(self) fn for_layer(layer_size: Option<Size>, layout_size: Self) -> Self {
match layer_size {
None => layout_size,
Some(s) => Self {
width: s.width.map_or(layout_size.width, |value| value.get()),
height: s.height.map_or(layout_size.height, |value| value.get()),
},
}
}
pub(self) fn for_canvas(canvas: &RgbaImage) -> Self {
Self {
width: canvas.width().max(1),
height: canvas.height().max(1),
}
}
}
impl Default for ConcreteSize {
fn default() -> Self {
Self {
width: WIDTH.get(),
height: HEIGHT.get(),
}
}
}
impl From<&Size> for ConcreteSize {
fn from(value: &Size) -> Self {
Self {
width: value.width.unwrap_or(WIDTH).get(),
height: value.height.unwrap_or(HEIGHT).get(),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::Renderer;
use std::{collections::HashSet, path::PathBuf};
use fontsource_downloader::FontSourceClient;
use parley::{FontContext, LayoutContext};
use resvg::usvg::Options;
use swash::scale::ScaleContext;
#[test]
fn find_non_existent_image() {
let renderer = Renderer {
external_resource_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_ext_resource_path("nonexistent.png").is_none());
}
}