Skip to main content

typf_export/
png.rs

1//! PNG export format
2//!
3//! Exports rendered text to PNG format using the `image` crate.
4
5// this_file: crates/typf-export/src/png.rs
6
7use image::{ImageBuffer, ImageEncoder, RgbaImage};
8use typf_core::{
9    error::{ExportError, Result},
10    traits::Exporter,
11    types::{BitmapData, BitmapFormat, RenderOutput},
12};
13
14/// Encode bitmap data to PNG format.
15///
16/// This is the shared implementation used by both `PngExporter` and `SvgExporter`
17/// (for embedded images). Handles all bitmap formats: RGBA8, RGB8, Gray8, Gray1.
18///
19/// Returns a valid PNG with proper IHDR, IDAT, and IEND chunks.
20pub fn encode_bitmap_to_png(bitmap: &BitmapData) -> Result<Vec<u8>> {
21    // Validate buffer size before processing
22    let expected_size = match bitmap.format {
23        BitmapFormat::Rgba8 => (bitmap.width * bitmap.height * 4) as usize,
24        BitmapFormat::Rgb8 => (bitmap.width * bitmap.height * 3) as usize,
25        BitmapFormat::Gray8 => (bitmap.width * bitmap.height) as usize,
26        BitmapFormat::Gray1 => (bitmap.width * bitmap.height).div_ceil(8) as usize,
27    };
28
29    if bitmap.data.len() < expected_size {
30        return Err(ExportError::EncodingFailed(format!(
31            "Buffer too small: expected {} bytes for {}x{} {:?}, got {}",
32            expected_size,
33            bitmap.width,
34            bitmap.height,
35            bitmap.format,
36            bitmap.data.len()
37        ))
38        .into());
39    }
40
41    // Create RGBA image buffer
42    let img: RgbaImage = match bitmap.format {
43        BitmapFormat::Rgba8 => {
44            // Direct RGBA data
45            ImageBuffer::from_raw(bitmap.width, bitmap.height, bitmap.data.clone()).ok_or_else(
46                || {
47                    ExportError::EncodingFailed(
48                        "Failed to create image buffer from RGBA data".into(),
49                    )
50                },
51            )?
52        },
53        BitmapFormat::Rgb8 => {
54            // Convert RGB to RGBA
55            let mut rgba_data = Vec::with_capacity((bitmap.width * bitmap.height * 4) as usize);
56            for chunk in bitmap.data.chunks(3) {
57                if chunk.len() < 3 {
58                    break; // Guard against malformed data
59                }
60                rgba_data.push(chunk[0]); // R
61                rgba_data.push(chunk[1]); // G
62                rgba_data.push(chunk[2]); // B
63                rgba_data.push(255); // A (fully opaque)
64            }
65            ImageBuffer::from_raw(bitmap.width, bitmap.height, rgba_data).ok_or_else(|| {
66                ExportError::EncodingFailed("Failed to create image buffer from RGB data".into())
67            })?
68        },
69        BitmapFormat::Gray8 => {
70            // Convert grayscale to RGBA
71            let mut rgba_data = Vec::with_capacity((bitmap.width * bitmap.height * 4) as usize);
72            for &gray in &bitmap.data {
73                rgba_data.push(gray); // R
74                rgba_data.push(gray); // G
75                rgba_data.push(gray); // B
76                rgba_data.push(255); // A
77            }
78            ImageBuffer::from_raw(bitmap.width, bitmap.height, rgba_data).ok_or_else(|| {
79                ExportError::EncodingFailed(
80                    "Failed to create image buffer from grayscale data".into(),
81                )
82            })?
83        },
84        BitmapFormat::Gray1 => {
85            // Convert 1-bit to RGBA
86            let mut rgba_data = Vec::with_capacity((bitmap.width * bitmap.height * 4) as usize);
87            for y in 0..bitmap.height {
88                for x in 0..bitmap.width {
89                    let byte_idx = ((y * bitmap.width + x) / 8) as usize;
90                    let bit_idx = ((y * bitmap.width + x) % 8) as usize;
91                    if byte_idx >= bitmap.data.len() {
92                        // Guard against out-of-bounds access
93                        rgba_data.extend_from_slice(&[0, 0, 0, 255]);
94                        continue;
95                    }
96                    let bit = (bitmap.data[byte_idx] >> (7 - bit_idx)) & 1;
97                    let value = if bit == 1 { 255 } else { 0 };
98                    rgba_data.push(value); // R
99                    rgba_data.push(value); // G
100                    rgba_data.push(value); // B
101                    rgba_data.push(255); // A
102                }
103            }
104            ImageBuffer::from_raw(bitmap.width, bitmap.height, rgba_data).ok_or_else(|| {
105                ExportError::EncodingFailed("Failed to create image buffer from 1-bit data".into())
106            })?
107        },
108    };
109
110    // Encode to PNG
111    let mut png_data = Vec::new();
112    let encoder = image::codecs::png::PngEncoder::new_with_quality(
113        &mut png_data,
114        image::codecs::png::CompressionType::Default,
115        image::codecs::png::FilterType::Sub,
116    );
117
118    encoder
119        .write_image(
120            img.as_raw(),
121            bitmap.width,
122            bitmap.height,
123            image::ExtendedColorType::Rgba8,
124        )
125        .map_err(|e| ExportError::EncodingFailed(format!("PNG encoding failed: {}", e)))?;
126
127    Ok(png_data)
128}
129
130/// PNG exporter for rendering results
131///
132/// Converts bitmap rendering output to PNG format.
133///
134/// # Examples
135///
136/// ```
137/// use typf_export::PngExporter;
138/// let exporter = PngExporter::new();
139/// ```
140pub struct PngExporter;
141
142impl PngExporter {
143    /// Create a new PNG exporter
144    pub fn new() -> Self {
145        Self
146    }
147
148    /// Convert bitmap data to PNG format
149    fn export_bitmap(&self, bitmap: &BitmapData) -> Result<Vec<u8>> {
150        encode_bitmap_to_png(bitmap)
151    }
152}
153
154impl Exporter for PngExporter {
155    fn name(&self) -> &'static str {
156        "png"
157    }
158
159    fn export(&self, output: &RenderOutput) -> Result<Vec<u8>> {
160        match output {
161            RenderOutput::Bitmap(bitmap) => self.export_bitmap(bitmap),
162            _ => Err(ExportError::FormatNotSupported(
163                "PNG exporter only supports bitmap output".into(),
164            )
165            .into()),
166        }
167    }
168
169    fn extension(&self) -> &'static str {
170        "png"
171    }
172
173    fn mime_type(&self) -> &'static str {
174        "image/png"
175    }
176}
177
178impl Default for PngExporter {
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_png_exporter_creation() {
190        let exporter = PngExporter::new();
191        assert_eq!(exporter.name(), "png");
192        assert_eq!(exporter.extension(), "png");
193        assert_eq!(exporter.mime_type(), "image/png");
194    }
195
196    #[test]
197    fn test_png_export_rgba() {
198        let exporter = PngExporter::new();
199
200        // Create a small 2x2 RGBA test bitmap
201        let bitmap = BitmapData {
202            width: 2,
203            height: 2,
204            format: BitmapFormat::Rgba8,
205            data: vec![
206                255, 0, 0, 255, // Red
207                0, 255, 0, 255, // Green
208                0, 0, 255, 255, // Blue
209                255, 255, 255, 255, // White
210            ],
211        };
212
213        let output = RenderOutput::Bitmap(bitmap);
214        let png_data = match exporter.export(&output) {
215            Ok(png_data) => png_data,
216            Err(e) => unreachable!("png export failed: {e}"),
217        };
218
219        // PNG should start with PNG magic bytes
220        assert_eq!(&png_data[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
221        assert!(png_data.len() > 50); // Should have reasonable size for 2x2 image
222    }
223
224    #[test]
225    fn test_png_export_grayscale() {
226        let exporter = PngExporter::new();
227
228        let bitmap = BitmapData {
229            width: 2,
230            height: 2,
231            format: BitmapFormat::Gray8,
232            data: vec![0, 128, 192, 255],
233        };
234
235        let output = RenderOutput::Bitmap(bitmap);
236        let png_data = match exporter.export(&output) {
237            Ok(png_data) => png_data,
238            Err(e) => unreachable!("png export failed: {e}"),
239        };
240
241        // Verify PNG magic bytes
242        assert_eq!(&png_data[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
243    }
244
245    #[test]
246    fn test_png_default() {
247        let exporter = PngExporter;
248        assert_eq!(exporter.name(), "png");
249    }
250}