Skip to main content

typf_export/
svg.rs

1//! SVG export format
2//!
3//! Exports rendered text to Scalable Vector Graphics format.
4
5use crate::png::encode_bitmap_to_png;
6use typf_core::{
7    error::{ExportError, Result},
8    traits::Exporter,
9    types::{BitmapData, RenderOutput},
10};
11
12/// SVG exporter for rendering results
13///
14/// Converts bitmap rendering output to SVG format with embedded base64 image data.
15///
16/// # Examples
17///
18/// ```ignore
19/// use typf_export::SvgExporter;
20///
21/// let exporter = SvgExporter::new();
22/// let svg_data = exporter.export(&render_output)?;
23/// std::fs::write("output.svg", svg_data)?;
24/// ```
25pub struct SvgExporter {
26    /// Whether to embed the bitmap as base64 or use data URI
27    embed_image: bool,
28}
29
30impl SvgExporter {
31    /// Create a new SVG exporter
32    pub fn new() -> Self {
33        Self { embed_image: true }
34    }
35
36    /// Create SVG exporter that references external images
37    pub fn with_external_images() -> Self {
38        Self { embed_image: false }
39    }
40
41    /// Export bitmap data to SVG
42    pub fn export_bitmap(&self, bitmap: &BitmapData) -> Result<Vec<u8>> {
43        let mut svg = String::new();
44
45        // SVG header
46        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            // Convert bitmap to base64 PNG using proper encoding
59            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            // Reference external image
70            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        // Create a small 2x2 RGBA bitmap
146        let bitmap = BitmapData {
147            width: 2,
148            height: 2,
149            format: BitmapFormat::Rgba8,
150            data: vec![
151                255, 0, 0, 255, // Red
152                0, 255, 0, 255, // Green
153                0, 0, 255, 255, // Blue
154                255, 255, 255, 255, // White
155            ],
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        // Extract base64 data from SVG
163        let start = svg.find("base64,").unwrap() + 7;
164        let end = svg[start..].find('"').unwrap() + start;
165        let base64_data = &svg[start..end];
166
167        // Decode base64 and verify PNG magic bytes
168        use base64::{engine::general_purpose::STANDARD, Engine};
169        let png_data = STANDARD.decode(base64_data).unwrap();
170
171        // PNG signature: 137 80 78 71 13 10 26 10
172        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        // Verify IHDR chunk exists (first chunk after signature)
179        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        // 8x8 bitmap = 8 bytes for Gray1 format
185        let bitmap = BitmapData {
186            width: 8,
187            height: 8,
188            format: BitmapFormat::Gray1,
189            data: vec![0xAA; 8], // Alternating pattern
190        };
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        // Create a bitmap with insufficient data
219        let bitmap = BitmapData {
220            width: 10,
221            height: 10,
222            format: BitmapFormat::Rgba8,
223            data: vec![255u8; 10], // Should be 10*10*4 = 400 bytes
224        };
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}