use crate::DotmaxError;
use image::DynamicImage;
use std::path::Path;
use tracing::{debug, info};
use usvg::{TreeParsing, TreePostProc};
pub const MAX_SVG_WIDTH: u32 = 10_000;
pub const MAX_SVG_HEIGHT: u32 = 10_000;
pub fn load_svg_from_path(
path: &Path,
width: u32,
height: u32,
) -> Result<DynamicImage, DotmaxError> {
info!("Loading SVG from {:?} at {}×{}", path, width, height);
if !path.exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("SVG file not found: {}", path.display()),
)
.into());
}
let svg_data = std::fs::read(path).map_err(|e| {
DotmaxError::SvgError(format!("Failed to read SVG file {}: {e}", path.display()))
})?;
load_svg_from_bytes(&svg_data, width, height).map_err(|e| match e {
DotmaxError::SvgError(msg) => {
DotmaxError::SvgError(format!("Error loading SVG from {}: {msg}", path.display()))
}
other => other,
})
}
pub fn load_svg_from_bytes(
bytes: &[u8],
width: u32,
height: u32,
) -> Result<DynamicImage, DotmaxError> {
if width == 0 || height == 0 {
return Err(DotmaxError::InvalidImageDimensions { width, height });
}
if width > MAX_SVG_WIDTH || height > MAX_SVG_HEIGHT {
return Err(DotmaxError::InvalidImageDimensions { width, height });
}
debug!("Parsing SVG data ({} bytes)", bytes.len());
let options = usvg::Options::default();
let mut tree = usvg::Tree::from_data(bytes, &options)
.map_err(|e| DotmaxError::SvgError(format!("Failed to parse SVG: {e}")))?;
debug!(
"SVG parsed successfully, viewBox size: {}×{}",
tree.size.width(),
tree.size.height()
);
let mut fontdb = usvg::fontdb::Database::new();
fontdb.load_system_fonts();
debug!("Loaded {} font faces for text rendering", fontdb.len());
tree.postprocess(usvg::PostProcessingSteps::default(), &fontdb);
rasterize_svg_tree(&tree, width, height)
}
fn rasterize_svg_tree(
tree: &usvg::Tree,
width: u32,
height: u32,
) -> Result<DynamicImage, DotmaxError> {
use resvg::tiny_skia::{Pixmap, Transform};
debug!(
"Creating {}×{} pixel buffer for rasterization",
width, height
);
let mut pixmap = Pixmap::new(width, height).ok_or_else(|| {
DotmaxError::SvgError(format!(
"Failed to create pixmap for dimensions {width}×{height}"
))
})?;
let tree_size = tree.size;
#[allow(clippy::cast_precision_loss)]
let scale_x = width as f32 / tree_size.width();
#[allow(clippy::cast_precision_loss)]
let scale_y = height as f32 / tree_size.height();
let scale = scale_x.min(scale_y);
let transform = Transform::from_scale(scale, scale);
debug!(
"Rendering SVG with transform (scale: {:.2}, {:.2})",
scale, scale
);
resvg::render(tree, transform, &mut pixmap.as_mut());
debug!("SVG rasterization complete");
let pixmap_data = pixmap.data();
let mut brightness_sum: u64 = 0;
let pixel_count = (width * height) as usize;
for pixel in pixmap_data.chunks_exact(4) {
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::suboptimal_flops
)]
let brightness = (0.299 * f32::from(pixel[0])
+ 0.587 * f32::from(pixel[1])
+ 0.114 * f32::from(pixel[2])) as u64;
brightness_sum += brightness;
}
let avg_brightness = brightness_sum / pixel_count as u64;
let should_invert = avg_brightness < 127;
if should_invert {
debug!(
"Image is dark (avg brightness: {}), inverting for better contrast",
avg_brightness
);
let pixmap_data = pixmap.data_mut();
for pixel in pixmap_data.chunks_exact_mut(4) {
pixel[0] = 255 - pixel[0]; pixel[1] = 255 - pixel[1]; pixel[2] = 255 - pixel[2]; }
} else {
debug!(
"Image is light (avg brightness: {}), no inversion needed",
avg_brightness
);
}
let image_buffer =
image::RgbaImage::from_raw(width, height, pixmap.take()).ok_or_else(|| {
DotmaxError::SvgError("Failed to convert pixmap to image buffer".to_string())
})?;
Ok(DynamicImage::ImageRgba8(image_buffer))
}
#[cfg(all(test, feature = "svg"))]
mod tests {
use super::*;
const SIMPLE_CIRCLE_SVG: &str = r#"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="black"/>
</svg>
"#;
const GRADIENT_SVG: &str = r#"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:black;stop-opacity:1" />
<stop offset="100%" style="stop-color:white;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="100" height="100" fill="url(#grad1)" />
</svg>
"#;
const MALFORMED_SVG: &str = "<svg><notvalid>";
#[test]
fn test_load_valid_simple_svg_returns_dynamic_image() {
let result = load_svg_from_bytes(SIMPLE_CIRCLE_SVG.as_bytes(), 100, 100);
assert!(result.is_ok());
let img = result.unwrap();
assert_eq!(img.width(), 100);
assert_eq!(img.height(), 100);
}
#[test]
fn test_load_svg_with_gradient_rasterizes_correctly() {
let result = load_svg_from_bytes(GRADIENT_SVG.as_bytes(), 200, 200);
assert!(result.is_ok());
let img = result.unwrap();
assert_eq!(img.width(), 200);
assert_eq!(img.height(), 200);
}
#[test]
fn test_load_malformed_svg_returns_svg_error() {
let result = load_svg_from_bytes(MALFORMED_SVG.as_bytes(), 100, 100);
assert!(result.is_err());
match result {
Err(DotmaxError::SvgError(msg)) => {
assert!(msg.contains("parse"));
}
_ => panic!("Expected SvgError"),
}
}
#[test]
fn test_invalid_dimensions_zero_returns_error() {
let result = load_svg_from_bytes(SIMPLE_CIRCLE_SVG.as_bytes(), 0, 100);
assert!(result.is_err());
assert!(matches!(
result,
Err(DotmaxError::InvalidImageDimensions { .. })
));
let result = load_svg_from_bytes(SIMPLE_CIRCLE_SVG.as_bytes(), 100, 0);
assert!(result.is_err());
assert!(matches!(
result,
Err(DotmaxError::InvalidImageDimensions { .. })
));
}
#[test]
fn test_invalid_dimensions_exceeds_max_returns_error() {
let result = load_svg_from_bytes(SIMPLE_CIRCLE_SVG.as_bytes(), 20_000, 100);
assert!(result.is_err());
assert!(matches!(
result,
Err(DotmaxError::InvalidImageDimensions { .. })
));
let result = load_svg_from_bytes(SIMPLE_CIRCLE_SVG.as_bytes(), 100, 20_000);
assert!(result.is_err());
assert!(matches!(
result,
Err(DotmaxError::InvalidImageDimensions { .. })
));
}
#[test]
fn test_aspect_ratio_preserved_in_rasterization() {
let svg = r#"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">
<rect width="200" height="100" fill="black"/>
</svg>
"#;
let result = load_svg_from_bytes(svg.as_bytes(), 100, 100);
assert!(result.is_ok());
let img = result.unwrap();
assert_eq!(img.width(), 100);
assert_eq!(img.height(), 100);
}
#[test]
fn test_load_svg_from_bytes_same_as_file() {
let result1 = load_svg_from_bytes(SIMPLE_CIRCLE_SVG.as_bytes(), 150, 150);
let result2 = load_svg_from_bytes(SIMPLE_CIRCLE_SVG.as_bytes(), 150, 150);
assert!(result1.is_ok());
assert!(result2.is_ok());
let img1 = result1.unwrap();
let img2 = result2.unwrap();
assert_eq!(img1.width(), img2.width());
assert_eq!(img1.height(), img2.height());
}
#[test]
fn test_svg_with_paths_applies_antialiasing() {
let svg = r#"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M 10 10 L 90 10 L 90 90 L 10 90 Z" fill="black" stroke="white" stroke-width="2"/>
</svg>
"#;
let result = load_svg_from_bytes(svg.as_bytes(), 100, 100);
assert!(result.is_ok());
}
}