use crate::png::encode_bitmap_to_png;
use typf_core::{
error::{ExportError, Result},
traits::Exporter,
types::{BitmapData, RenderOutput},
};
pub struct SvgExporter {
embed_image: bool,
}
impl SvgExporter {
pub fn new() -> Self {
Self { embed_image: true }
}
pub fn with_external_images() -> Self {
Self { embed_image: false }
}
pub fn export_bitmap(&self, bitmap: &BitmapData) -> Result<Vec<u8>> {
let mut svg = String::new();
svg.push_str(&format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="{}"
height="{}"
viewBox="0 0 {} {}">
"#,
bitmap.width, bitmap.height, bitmap.width, bitmap.height
));
if self.embed_image {
let png_data = encode_bitmap_to_png(bitmap)?;
use base64::{engine::general_purpose::STANDARD, Engine as _};
let base64_data = STANDARD.encode(&png_data);
svg.push_str(&format!(
r#" <image width="{}" height="{}" xlink:href="data:image/png;base64,{}" />
"#,
bitmap.width, bitmap.height, base64_data
));
} else {
svg.push_str(&format!(
r#" <image width="{}" height="{}" xlink:href="output.png" />
"#,
bitmap.width, bitmap.height
));
}
svg.push_str("</svg>\n");
Ok(svg.into_bytes())
}
}
impl Default for SvgExporter {
fn default() -> Self {
Self::new()
}
}
impl Exporter for SvgExporter {
fn name(&self) -> &'static str {
"svg"
}
fn export(&self, output: &RenderOutput) -> Result<Vec<u8>> {
match output {
RenderOutput::Bitmap(bitmap) => self.export_bitmap(bitmap),
_ => Err(ExportError::FormatNotSupported(
"SVG exporter only supports bitmap output".into(),
)
.into()),
}
}
fn extension(&self) -> &'static str {
"svg"
}
fn mime_type(&self) -> &'static str {
"image/svg+xml"
}
}
#[cfg(test)]
mod tests {
use super::*;
use typf_core::types::BitmapFormat;
#[test]
fn test_svg_exporter_creation() {
let exporter = SvgExporter::new();
assert!(exporter.embed_image);
}
#[test]
fn test_svg_export_basic() {
let bitmap = BitmapData {
width: 10,
height: 10,
format: BitmapFormat::Rgba8,
data: vec![255u8; 10 * 10 * 4],
};
let exporter = SvgExporter::new();
let result = exporter.export_bitmap(&bitmap);
assert!(result.is_ok());
let svg = String::from_utf8(result.unwrap()).unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("width=\"10\""));
assert!(svg.contains("height=\"10\""));
}
#[test]
fn test_svg_embedded_png_is_valid() {
let bitmap = BitmapData {
width: 2,
height: 2,
format: BitmapFormat::Rgba8,
data: vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, ],
};
let exporter = SvgExporter::new();
let svg_bytes = exporter.export_bitmap(&bitmap).unwrap();
let svg = String::from_utf8(svg_bytes).unwrap();
let start = svg.find("base64,").unwrap() + 7;
let end = svg[start..].find('"').unwrap() + start;
let base64_data = &svg[start..end];
use base64::{engine::general_purpose::STANDARD, Engine};
let png_data = STANDARD.decode(base64_data).unwrap();
assert_eq!(
&png_data[0..8],
&[137, 80, 78, 71, 13, 10, 26, 10],
"PNG should start with correct magic bytes"
);
assert_eq!(&png_data[12..16], b"IHDR", "PNG should have IHDR chunk");
}
#[test]
fn test_svg_export_gray1_format() {
let bitmap = BitmapData {
width: 8,
height: 8,
format: BitmapFormat::Gray1,
data: vec![0xAA; 8], };
let exporter = SvgExporter::new();
let result = exporter.export_bitmap(&bitmap);
assert!(result.is_ok(), "Gray1 export should succeed");
let svg = String::from_utf8(result.unwrap()).unwrap();
assert!(svg.contains("data:image/png;base64,"));
}
#[test]
fn test_svg_export_grayscale() {
let bitmap = BitmapData {
width: 4,
height: 4,
format: BitmapFormat::Gray8,
data: vec![
0, 64, 128, 192, 255, 200, 100, 50, 0, 64, 128, 192, 255, 200, 100, 50,
],
};
let exporter = SvgExporter::new();
let result = exporter.export_bitmap(&bitmap);
assert!(result.is_ok(), "Grayscale export should succeed");
}
#[test]
fn test_svg_export_short_buffer_fails() {
let bitmap = BitmapData {
width: 10,
height: 10,
format: BitmapFormat::Rgba8,
data: vec![255u8; 10], };
let exporter = SvgExporter::new();
let result = exporter.export_bitmap(&bitmap);
assert!(result.is_err(), "Short buffer should fail");
let err = result.unwrap_err();
let err_msg = format!("{}", err);
assert!(
err_msg.contains("Buffer too small"),
"Error should mention buffer size: {}",
err_msg
);
}
#[test]
fn test_svg_external_image_mode() {
let bitmap = BitmapData {
width: 10,
height: 10,
format: BitmapFormat::Rgba8,
data: vec![255u8; 10 * 10 * 4],
};
let exporter = SvgExporter::with_external_images();
let result = exporter.export_bitmap(&bitmap).unwrap();
let svg = String::from_utf8(result).unwrap();
assert!(svg.contains("xlink:href=\"output.png\""));
assert!(!svg.contains("base64"));
}
}