use anyhow::{Context as _, Result, ensure};
use resvg::{tiny_skia, usvg};
pub fn load_tree_from_data(data: &[u8]) -> Result<usvg::Tree> {
let opt = usvg::Options::default();
return usvg::Tree::from_data(data, &opt).context("parsing SVG");
}
#[expect(clippy::cast_precision_loss, reason = "icon sides are exact in f32")]
pub fn render_size(tree: &usvg::Tree, target_w: u32, target_h: u32) -> Result<tiny_skia::Pixmap> {
let target_w = target_w.max(1);
let target_h = target_h.max(1);
let size = tree.size();
let (width, height) = (size.width(), size.height());
let scale = (target_w as f32 / width).min(target_h as f32 / height);
let scaled_w = width * scale;
let scaled_h = height * scale;
let tx = (target_w as f32 - scaled_w) / 2.0;
let ty = (target_h as f32 - scaled_h) / 2.0;
let transform = tiny_skia::Transform::from_scale(scale, scale).post_translate(tx, ty);
let mut pixmap = tiny_skia::Pixmap::new(target_w, target_h)
.context("allocating pixmap (size too large?)")?;
resvg::render(tree, transform, &mut pixmap.as_mut());
return Ok(pixmap);
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "round up to whole pixels"
)]
pub fn render_scale(tree: &usvg::Tree, scale: f32) -> Result<tiny_skia::Pixmap> {
let size = tree.size();
let w = (size.width() * scale).ceil().max(1.0) as u32;
let height = (size.height() * scale).ceil().max(1.0) as u32;
let mut pixmap = tiny_skia::Pixmap::new(w, height).context("allocating pixmap")?;
resvg::render(
tree,
tiny_skia::Transform::from_scale(scale, scale),
&mut pixmap.as_mut(),
);
return Ok(pixmap);
}
pub fn encode_png(pixmap: &tiny_skia::Pixmap) -> Result<Vec<u8>> {
return pixmap.encode_png().context("encoding PNG");
}
#[expect(clippy::cast_precision_loss, reason = "byte counts are exact in f64")]
pub fn rmse(left: &tiny_skia::Pixmap, right: &tiny_skia::Pixmap) -> Result<f64> {
ensure!(
left.width() == right.width() && left.height() == right.height(),
"comparing a {}x{} render against a {}x{} one",
left.width(),
left.height(),
right.width(),
right.height()
);
let (left, right) = (left.data(), right.data());
let sum: f64 = left
.iter()
.zip(right)
.map(|(x, y)| {
let diff = f64::from(*x) - f64::from(*y);
return diff * diff;
})
.sum();
return Ok((sum / left.len() as f64).sqrt() / 255.0);
}
#[cfg(test)]
mod tests {
use super::*;
fn filled(w: u32, height: u32, color: tiny_skia::Color) -> tiny_skia::Pixmap {
let mut pixmap = tiny_skia::Pixmap::new(w, height).unwrap();
pixmap.fill(color);
return pixmap;
}
#[test]
fn rmse_of_a_pixmap_against_itself_is_zero() {
let pixmap = filled(8, 8, tiny_skia::Color::from_rgba8(12, 34, 56, 255));
assert_eq!(rmse(&pixmap, &pixmap).unwrap(), 0.0);
}
#[test]
fn rmse_of_opposite_pixmaps_is_one() {
let white = filled(8, 8, tiny_skia::Color::from_rgba8(255, 255, 255, 255));
let clear = filled(8, 8, tiny_skia::Color::TRANSPARENT);
assert_eq!(rmse(&white, &clear).unwrap(), 1.0);
}
#[test]
fn rmse_rejects_mismatched_sizes() {
let square = filled(8, 8, tiny_skia::Color::TRANSPARENT);
let wide = filled(8, 4, tiny_skia::Color::TRANSPARENT);
assert!(rmse(&square, &wide).is_err());
}
}