use crate::visualization::VisualizationError;
use std::fs;
use std::path::Path;
pub(crate) fn render_svg_to_file(
svg: &str,
output_path: &str,
png_scale: f64,
) -> Result<(), VisualizationError> {
let out_path = Path::new(output_path);
match out_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or_default()
.to_ascii_lowercase()
.as_str()
{
"png" => rasterize_svg_to_png_data(svg.as_bytes(), out_path, png_scale),
_ => fs::write(out_path, svg).map_err(VisualizationError::Io),
}
}
fn rasterize_svg_to_png_data(
svg_data: &[u8],
png_path: &Path,
scale: f64,
) -> Result<(), VisualizationError> {
if !scale.is_finite() || scale <= 0.0 {
return Err(VisualizationError::InvalidInput(format!(
"png scale must be positive and finite; got {scale}"
)));
}
let mut options = resvg::usvg::Options::default();
options.fontdb_mut().load_system_fonts();
let tree = resvg::usvg::Tree::from_data(svg_data, &options)
.map_err(|e| VisualizationError::SvgRenderFailed(e.to_string()))?;
let size = tree.size().to_int_size();
let width = scaled_extent(size.width(), scale)?;
let height = scaled_extent(size.height(), scale)?;
let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height).ok_or_else(|| {
VisualizationError::SvgRenderFailed("failed to allocate pixmap".to_string())
})?;
resvg::render(
&tree,
resvg::tiny_skia::Transform::from_scale(scale as f32, scale as f32),
&mut pixmap.as_mut(),
);
pixmap
.save_png(png_path)
.map_err(|e| VisualizationError::SvgRenderFailed(e.to_string()))?;
Ok(())
}
fn scaled_extent(value: u32, scale: f64) -> Result<u32, VisualizationError> {
let scaled = (value as f64 * scale).ceil().max(1.0);
if scaled > u32::MAX as f64 {
return Err(VisualizationError::InvalidInput(format!(
"scaled png extent {scaled} exceeds u32::MAX"
)));
}
Ok(scaled as u32)
}
pub(crate) fn escape_text(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
pub(crate) fn escape_attr(s: &str) -> String {
escape_text(s).replace('"', """)
}