1use crate::png::encode_bitmap_to_png;
6use typf_core::{
7 error::{ExportError, Result},
8 traits::Exporter,
9 types::{BitmapData, RenderOutput},
10};
11
12pub struct SvgExporter {
26 embed_image: bool,
28}
29
30impl SvgExporter {
31 pub fn new() -> Self {
33 Self { embed_image: true }
34 }
35
36 pub fn with_external_images() -> Self {
38 Self { embed_image: false }
39 }
40
41 pub fn export_bitmap(&self, bitmap: &BitmapData) -> Result<Vec<u8>> {
43 let mut svg = String::new();
44
45 svg.push_str(&format!(
47 r#"<?xml version="1.0" encoding="UTF-8"?>
48<svg xmlns="http://www.w3.org/2000/svg"
49 xmlns:xlink="http://www.w3.org/1999/xlink"
50 width="{}"
51 height="{}"
52 viewBox="0 0 {} {}">
53"#,
54 bitmap.width, bitmap.height, bitmap.width, bitmap.height
55 ));
56
57 if self.embed_image {
58 let png_data = encode_bitmap_to_png(bitmap)?;
60 use base64::{engine::general_purpose::STANDARD, Engine as _};
61 let base64_data = STANDARD.encode(&png_data);
62
63 svg.push_str(&format!(
64 r#" <image width="{}" height="{}" xlink:href="data:image/png;base64,{}" />
65"#,
66 bitmap.width, bitmap.height, base64_data
67 ));
68 } else {
69 svg.push_str(&format!(
71 r#" <image width="{}" height="{}" xlink:href="output.png" />
72"#,
73 bitmap.width, bitmap.height
74 ));
75 }
76
77 svg.push_str("</svg>\n");
78
79 Ok(svg.into_bytes())
80 }
81}
82
83impl Default for SvgExporter {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl Exporter for SvgExporter {
90 fn name(&self) -> &'static str {
91 "svg"
92 }
93
94 fn export(&self, output: &RenderOutput) -> Result<Vec<u8>> {
95 match output {
96 RenderOutput::Bitmap(bitmap) => self.export_bitmap(bitmap),
97 _ => Err(ExportError::FormatNotSupported(
98 "SVG exporter only supports bitmap output".into(),
99 )
100 .into()),
101 }
102 }
103
104 fn extension(&self) -> &'static str {
105 "svg"
106 }
107
108 fn mime_type(&self) -> &'static str {
109 "image/svg+xml"
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use typf_core::types::BitmapFormat;
117
118 #[test]
119 fn test_svg_exporter_creation() {
120 let exporter = SvgExporter::new();
121 assert!(exporter.embed_image);
122 }
123
124 #[test]
125 fn test_svg_export_basic() {
126 let bitmap = BitmapData {
127 width: 10,
128 height: 10,
129 format: BitmapFormat::Rgba8,
130 data: vec![255u8; 10 * 10 * 4],
131 };
132
133 let exporter = SvgExporter::new();
134 let result = exporter.export_bitmap(&bitmap);
135 assert!(result.is_ok());
136
137 let svg = String::from_utf8(result.unwrap()).unwrap();
138 assert!(svg.contains("<svg"));
139 assert!(svg.contains("width=\"10\""));
140 assert!(svg.contains("height=\"10\""));
141 }
142
143 #[test]
144 fn test_svg_embedded_png_is_valid() {
145 let bitmap = BitmapData {
147 width: 2,
148 height: 2,
149 format: BitmapFormat::Rgba8,
150 data: vec![
151 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, ],
156 };
157
158 let exporter = SvgExporter::new();
159 let svg_bytes = exporter.export_bitmap(&bitmap).unwrap();
160 let svg = String::from_utf8(svg_bytes).unwrap();
161
162 let start = svg.find("base64,").unwrap() + 7;
164 let end = svg[start..].find('"').unwrap() + start;
165 let base64_data = &svg[start..end];
166
167 use base64::{engine::general_purpose::STANDARD, Engine};
169 let png_data = STANDARD.decode(base64_data).unwrap();
170
171 assert_eq!(
173 &png_data[0..8],
174 &[137, 80, 78, 71, 13, 10, 26, 10],
175 "PNG should start with correct magic bytes"
176 );
177
178 assert_eq!(&png_data[12..16], b"IHDR", "PNG should have IHDR chunk");
180 }
181
182 #[test]
183 fn test_svg_export_gray1_format() {
184 let bitmap = BitmapData {
186 width: 8,
187 height: 8,
188 format: BitmapFormat::Gray1,
189 data: vec![0xAA; 8], };
191
192 let exporter = SvgExporter::new();
193 let result = exporter.export_bitmap(&bitmap);
194 assert!(result.is_ok(), "Gray1 export should succeed");
195
196 let svg = String::from_utf8(result.unwrap()).unwrap();
197 assert!(svg.contains("data:image/png;base64,"));
198 }
199
200 #[test]
201 fn test_svg_export_grayscale() {
202 let bitmap = BitmapData {
203 width: 4,
204 height: 4,
205 format: BitmapFormat::Gray8,
206 data: vec![
207 0, 64, 128, 192, 255, 200, 100, 50, 0, 64, 128, 192, 255, 200, 100, 50,
208 ],
209 };
210
211 let exporter = SvgExporter::new();
212 let result = exporter.export_bitmap(&bitmap);
213 assert!(result.is_ok(), "Grayscale export should succeed");
214 }
215
216 #[test]
217 fn test_svg_export_short_buffer_fails() {
218 let bitmap = BitmapData {
220 width: 10,
221 height: 10,
222 format: BitmapFormat::Rgba8,
223 data: vec![255u8; 10], };
225
226 let exporter = SvgExporter::new();
227 let result = exporter.export_bitmap(&bitmap);
228 assert!(result.is_err(), "Short buffer should fail");
229
230 let err = result.unwrap_err();
231 let err_msg = format!("{}", err);
232 assert!(
233 err_msg.contains("Buffer too small"),
234 "Error should mention buffer size: {}",
235 err_msg
236 );
237 }
238
239 #[test]
240 fn test_svg_external_image_mode() {
241 let bitmap = BitmapData {
242 width: 10,
243 height: 10,
244 format: BitmapFormat::Rgba8,
245 data: vec![255u8; 10 * 10 * 4],
246 };
247
248 let exporter = SvgExporter::with_external_images();
249 let result = exporter.export_bitmap(&bitmap).unwrap();
250 let svg = String::from_utf8(result).unwrap();
251
252 assert!(svg.contains("xlink:href=\"output.png\""));
253 assert!(!svg.contains("base64"));
254 }
255}