Packages

High-performance SVG to image conversion library for Elixir. Converts SVG to PNG, JPG, GIF, or WebP with support for resolution control, transparent backgrounds, and dynamic color preprocessing. Powered by Rust.

Current section

Files

Jump to
svgager native svgager_native src converter.rs
Raw

native/svgager_native/src/converter.rs

use image::{
codecs::jpeg::JpegEncoder, codecs::png::PngEncoder, ImageBuffer, ImageEncoder, ImageFormat,
Rgba,
};
use std::io::Cursor;
pub fn convert_svg_to_image(
svg_data: String,
format: String,
width: Option<u32>,
height: Option<u32>,
background_color: Option<String>,
replacements: Vec<(String, String)>,
) -> Result<Vec<u8>, String> {
// Step 1: Preprocess SVG with string replacements
let mut processed_svg = svg_data;
for (search, replace) in replacements {
processed_svg = processed_svg.replace(&search, &replace);
}
// Step 2: Parse SVG
let opt = usvg::Options::default();
let tree = usvg::Tree::from_str(&processed_svg, &opt)
.map_err(|e| format!("Failed to parse SVG: {}", e))?;
// Step 3: Determine output dimensions
let svg_size = tree.size();
let (out_width, out_height) = match (width, height) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) => {
let aspect_ratio = svg_size.height() / svg_size.width();
(w, (w as f32 * aspect_ratio) as u32)
}
(None, Some(h)) => {
let aspect_ratio = svg_size.width() / svg_size.height();
((h as f32 * aspect_ratio) as u32, h)
}
(None, None) => (svg_size.width() as u32, svg_size.height() as u32),
};
// Step 4: Create pixmap and render SVG
let mut pixmap = tiny_skia::Pixmap::new(out_width, out_height)
.ok_or_else(|| "Failed to create pixmap".to_string())?;
// Apply background color for non-PNG formats
let is_png = format.to_lowercase() == "png";
if !is_png {
let bg_color = background_color.unwrap_or_else(|| "FFFFFF".to_string());
let rgb = parse_hex_color(&bg_color)?;
pixmap.fill(tiny_skia::Color::from_rgba8(rgb.0, rgb.1, rgb.2, 255));
}
// Render SVG to pixmap
let transform = tiny_skia::Transform::from_scale(
out_width as f32 / svg_size.width(),
out_height as f32 / svg_size.height(),
);
resvg::render(&tree, transform, &mut pixmap.as_mut());
// Step 5: Encode to requested format
let image_data = pixmap.data();
encode_image(image_data, out_width, out_height, &format, is_png)
}
fn parse_hex_color(hex: &str) -> Result<(u8, u8, u8), String> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err(format!(
"Invalid hex color: must be 6 characters (RRGGBB), got {}",
hex.len()
));
}
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| format!("Invalid hex color: could not parse red component"))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| format!("Invalid hex color: could not parse green component"))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| format!("Invalid hex color: could not parse blue component"))?;
Ok((r, g, b))
}
fn encode_image(
data: &[u8],
width: u32,
height: u32,
format: &str,
has_alpha: bool,
) -> Result<Vec<u8>, String> {
let mut output = Vec::new();
let cursor = Cursor::new(&mut output);
match format.to_lowercase().as_str() {
"png" => {
let encoder = PngEncoder::new(cursor);
encoder
.write_image(data, width, height, image::ExtendedColorType::Rgba8)
.map_err(|e| format!("Failed to encode PNG: {}", e))?;
}
"jpg" | "jpeg" => {
// Convert RGBA to RGB for JPEG
let rgb_data = rgba_to_rgb(data);
let encoder = JpegEncoder::new_with_quality(cursor, 90);
encoder
.write_image(&rgb_data, width, height, image::ExtendedColorType::Rgb8)
.map_err(|e| format!("Failed to encode JPEG: {}", e))?;
}
"gif" => {
// For GIF, we need to use the image crate's DynamicImage
let img = if has_alpha {
ImageBuffer::<Rgba<u8>, Vec<u8>>::from_raw(width, height, data.to_vec())
.ok_or_else(|| "Failed to create image buffer".to_string())?
} else {
ImageBuffer::<Rgba<u8>, Vec<u8>>::from_raw(width, height, data.to_vec())
.ok_or_else(|| "Failed to create image buffer".to_string())?
};
image::DynamicImage::ImageRgba8(img)
.write_to(&mut Cursor::new(&mut output), ImageFormat::Gif)
.map_err(|e| format!("Failed to encode GIF: {}", e))?;
}
"webp" => {
// For WebP, we need to convert to DynamicImage
let img = ImageBuffer::<Rgba<u8>, Vec<u8>>::from_raw(width, height, data.to_vec())
.ok_or_else(|| "Failed to create image buffer".to_string())?;
image::DynamicImage::ImageRgba8(img)
.write_to(&mut Cursor::new(&mut output), ImageFormat::WebP)
.map_err(|e| format!("Failed to encode WebP: {}", e))?;
}
_ => return Err(format!("Unsupported format: {}", format)),
}
Ok(output)
}
fn rgba_to_rgb(rgba_data: &[u8]) -> Vec<u8> {
let mut rgb_data = Vec::with_capacity(rgba_data.len() * 3 / 4);
for chunk in rgba_data.chunks(4) {
rgb_data.push(chunk[0]); // R
rgb_data.push(chunk[1]); // G
rgb_data.push(chunk[2]); // B
// Skip alpha channel (chunk[3])
}
rgb_data
}