Skip to main content

typf_export/
lib.rs

1//! Where rendered text leaves the building: export formats for Typf
2//!
3//! The final stage of the pipeline. Turns your carefully rendered glyphs
4//! into files, streams, or whatever format your application needs.
5
6use std::io::Write;
7use typf_core::{
8    error::{ExportError, Result},
9    traits::Exporter,
10    types::{BitmapData, BitmapFormat, RenderOutput},
11};
12
13pub mod json;
14pub mod png;
15pub mod svg;
16
17pub use json::JsonExporter;
18pub use png::{encode_bitmap_to_png, PngExporter};
19pub use svg::SvgExporter;
20
21/// Simple bitmap exporter for when you just need to see what happened
22pub struct PnmExporter {
23    /// Choose your flavor: black-and-white, grayscale, or color
24    format: PnmFormat,
25}
26
27#[derive(Debug, Clone, Copy)]
28pub enum PnmFormat {
29    /// PBM - Just black and white pixels
30    Pbm,
31    /// PGM - 256 shades of gray
32    Pgm,
33    /// PPM - Full RGB color
34    Ppm,
35}
36
37impl PnmExporter {
38    /// Creates an exporter for your chosen PNM format
39    pub fn new(format: PnmFormat) -> Self {
40        Self { format }
41    }
42
43    /// Quick way to get a color exporter
44    pub fn ppm() -> Self {
45        Self::new(PnmFormat::Ppm)
46    }
47
48    /// Quick way to get a grayscale exporter
49    pub fn pgm() -> Self {
50        Self::new(PnmFormat::Pgm)
51    }
52
53    /// Converts bitmap data into PNM's simple text format
54    fn export_bitmap(&self, bitmap: &BitmapData) -> Result<Vec<u8>> {
55        let mut output = Vec::new();
56
57        match self.format {
58            PnmFormat::Ppm => {
59                // PPM needs a simple header first
60                writeln!(&mut output, "P3")?; // Magic number for ASCII PPM
61                writeln!(&mut output, "{} {}", bitmap.width, bitmap.height)?;
62                writeln!(&mut output, "255")?; // Maximum RGB value
63
64                // Transform bitmap data into PPM's text format
65                match bitmap.format {
66                    BitmapFormat::Rgba8 => {
67                        // Strip alpha, keep just RGB
68                        for y in 0..bitmap.height {
69                            for x in 0..bitmap.width {
70                                let idx = ((y * bitmap.width + x) * 4) as usize;
71                                write!(
72                                    &mut output,
73                                    "{} {} {} ",
74                                    bitmap.data[idx],     // Red
75                                    bitmap.data[idx + 1], // Green
76                                    bitmap.data[idx + 2]  // Blue
77                                )?;
78                            }
79                            writeln!(&mut output)?; // New line after each row
80                        }
81                    },
82                    BitmapFormat::Rgb8 => {
83                        // Copy RGB values directly
84                        for y in 0..bitmap.height {
85                            for x in 0..bitmap.width {
86                                let idx = ((y * bitmap.width + x) * 3) as usize;
87                                write!(
88                                    &mut output,
89                                    "{} {} {} ",
90                                    bitmap.data[idx],
91                                    bitmap.data[idx + 1],
92                                    bitmap.data[idx + 2]
93                                )?;
94                            }
95                            writeln!(&mut output)?;
96                        }
97                    },
98                    BitmapFormat::Gray8 => {
99                        // Make gray look like color (triplet the value)
100                        for y in 0..bitmap.height {
101                            for x in 0..bitmap.width {
102                                let idx = (y * bitmap.width + x) as usize;
103                                let gray = bitmap.data[idx];
104                                write!(&mut output, "{} {} {} ", gray, gray, gray)?;
105                            }
106                            writeln!(&mut output)?;
107                        }
108                    },
109                    BitmapFormat::Gray1 => {
110                        // Expand 1-bit to full RGB
111                        for y in 0..bitmap.height {
112                            for x in 0..bitmap.width {
113                                let byte_idx = ((y * bitmap.width + x) / 8) as usize;
114                                let bit_idx = ((y * bitmap.width + x) % 8) as usize;
115                                let bit = (bitmap.data[byte_idx] >> (7 - bit_idx)) & 1;
116                                let value = if bit == 1 { 255 } else { 0 };
117                                write!(&mut output, "{} {} {} ", value, value, value)?;
118                            }
119                            writeln!(&mut output)?;
120                        }
121                    },
122                }
123            },
124            PnmFormat::Pgm => {
125                // PGM header (grayscale version of PPM)
126                writeln!(&mut output, "P2")?; // Magic number for ASCII PGM
127                writeln!(&mut output, "{} {}", bitmap.width, bitmap.height)?;
128                writeln!(&mut output, "255")?; // Maximum gray value
129
130                // Flatten everything to grayscale
131                match bitmap.format {
132                    BitmapFormat::Gray8 => {
133                        // Already grayscale, just copy
134                        for y in 0..bitmap.height {
135                            for x in 0..bitmap.width {
136                                let idx = (y * bitmap.width + x) as usize;
137                                write!(&mut output, "{} ", bitmap.data[idx])?;
138                            }
139                            writeln!(&mut output)?;
140                        }
141                    },
142                    BitmapFormat::Rgba8 => {
143                        // Convert color to grayscale using luminance
144                        for y in 0..bitmap.height {
145                            for x in 0..bitmap.width {
146                                let idx = ((y * bitmap.width + x) * 4) as usize;
147                                let r = bitmap.data[idx] as u32;
148                                let g = bitmap.data[idx + 1] as u32;
149                                let b = bitmap.data[idx + 2] as u32;
150                                // ITU-R BT.709 luminance formula
151                                let gray = ((r * 299 + g * 587 + b * 114) / 1000) as u8;
152                                write!(&mut output, "{} ", gray)?;
153                            }
154                            writeln!(&mut output)?;
155                        }
156                    },
157                    BitmapFormat::Rgb8 => {
158                        // Color to grayscale conversion
159                        for y in 0..bitmap.height {
160                            for x in 0..bitmap.width {
161                                let idx = ((y * bitmap.width + x) * 3) as usize;
162                                let r = bitmap.data[idx] as u32;
163                                let g = bitmap.data[idx + 1] as u32;
164                                let b = bitmap.data[idx + 2] as u32;
165                                let gray = ((r * 299 + g * 587 + b * 114) / 1000) as u8;
166                                write!(&mut output, "{} ", gray)?;
167                            }
168                            writeln!(&mut output)?;
169                        }
170                    },
171                    BitmapFormat::Gray1 => {
172                        // Expand 1-bit to 8-bit grayscale
173                        for y in 0..bitmap.height {
174                            for x in 0..bitmap.width {
175                                let byte_idx = ((y * bitmap.width + x) / 8) as usize;
176                                let bit_idx = ((y * bitmap.width + x) % 8) as usize;
177                                let bit = (bitmap.data[byte_idx] >> (7 - bit_idx)) & 1;
178                                let value = if bit == 1 { 255 } else { 0 };
179                                write!(&mut output, "{} ", value)?;
180                            }
181                            writeln!(&mut output)?;
182                        }
183                    },
184                }
185            },
186            PnmFormat::Pbm => {
187                // PBM header (the simplest format - just 0s and 1s)
188                writeln!(&mut output, "P1")?; // Magic number for ASCII PBM
189                writeln!(&mut output, "{} {}", bitmap.width, bitmap.height)?;
190
191                // Everything becomes black (0) or white (1)
192                match bitmap.format {
193                    BitmapFormat::Gray1 => {
194                        // Already 1-bit, just copy
195                        for y in 0..bitmap.height {
196                            for x in 0..bitmap.width {
197                                let byte_idx = ((y * bitmap.width + x) / 8) as usize;
198                                let bit_idx = ((y * bitmap.width + x) % 8) as usize;
199                                let bit = (bitmap.data[byte_idx] >> (7 - bit_idx)) & 1;
200                                write!(&mut output, "{} ", bit)?;
201                            }
202                            writeln!(&mut output)?;
203                        }
204                    },
205                    _ => {
206                        // Convert Everything to 1-bit with a simple threshold
207                        for y in 0..bitmap.height {
208                            for x in 0..bitmap.width {
209                                let gray = match bitmap.format {
210                                    BitmapFormat::Gray8 => {
211                                        bitmap.data[(y * bitmap.width + x) as usize]
212                                    },
213                                    BitmapFormat::Rgba8 => {
214                                        let idx = ((y * bitmap.width + x) * 4) as usize;
215                                        let r = bitmap.data[idx] as u32;
216                                        let g = bitmap.data[idx + 1] as u32;
217                                        let b = bitmap.data[idx + 2] as u32;
218                                        ((r * 299 + g * 587 + b * 114) / 1000) as u8
219                                    },
220                                    BitmapFormat::Rgb8 => {
221                                        let idx = ((y * bitmap.width + x) * 3) as usize;
222                                        let r = bitmap.data[idx] as u32;
223                                        let g = bitmap.data[idx + 1] as u32;
224                                        let b = bitmap.data[idx + 2] as u32;
225                                        ((r * 299 + g * 587 + b * 114) / 1000) as u8
226                                    },
227                                    _ => 0,
228                                };
229                                // 127 is a reasonable threshold
230                                let bit = if gray > 127 { 1 } else { 0 };
231                                write!(&mut output, "{} ", bit)?;
232                            }
233                            writeln!(&mut output)?;
234                        }
235                    },
236                }
237            },
238        }
239
240        Ok(output)
241    }
242}
243
244impl Exporter for PnmExporter {
245    fn name(&self) -> &'static str {
246        match self.format {
247            PnmFormat::Pbm => "pbm",
248            PnmFormat::Pgm => "pgm",
249            PnmFormat::Ppm => "ppm",
250        }
251    }
252
253    fn export(&self, output: &RenderOutput) -> Result<Vec<u8>> {
254        match output {
255            RenderOutput::Bitmap(bitmap) => self.export_bitmap(bitmap),
256            _ => Err(ExportError::FormatNotSupported(
257                "PNM exporter only supports bitmap output".into(),
258            )
259            .into()),
260        }
261    }
262
263    fn extension(&self) -> &'static str {
264        match self.format {
265            PnmFormat::Pbm => "pbm",
266            PnmFormat::Pgm => "pgm",
267            PnmFormat::Ppm => "ppm",
268        }
269    }
270
271    fn mime_type(&self) -> &'static str {
272        match self.format {
273            PnmFormat::Pbm => "image/x-portable-bitmap",
274            PnmFormat::Pgm => "image/x-portable-graymap",
275            PnmFormat::Ppm => "image/x-portable-pixmap",
276        }
277    }
278}
279
280impl Default for PnmExporter {
281    fn default() -> Self {
282        Self::ppm() // Default to color
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_ppm_export() {
292        let exporter = PnmExporter::ppm();
293
294        // Create a small test bitmap
295        let bitmap = BitmapData {
296            width: 2,
297            height: 2,
298            format: BitmapFormat::Rgba8,
299            data: vec![
300                255, 0, 0, 255, // Red pixel
301                0, 255, 0, 255, // Green pixel
302                0, 0, 255, 255, // Blue pixel
303                255, 255, 255, 255, // White pixel
304            ],
305        };
306
307        let output = RenderOutput::Bitmap(bitmap);
308        let exported = exporter.export(&output).unwrap();
309
310        let text = String::from_utf8(exported).unwrap();
311        assert!(text.starts_with("P3"));
312        assert!(text.contains("2 2")); // Dimensions
313        assert!(text.contains("255")); // Max value
314    }
315
316    #[test]
317    fn test_pgm_export() {
318        let exporter = PnmExporter::pgm();
319
320        let bitmap = BitmapData {
321            width: 2,
322            height: 1,
323            format: BitmapFormat::Gray8,
324            data: vec![128, 255],
325        };
326
327        let output = RenderOutput::Bitmap(bitmap);
328        let exported = exporter.export(&output).unwrap();
329
330        let text = String::from_utf8(exported).unwrap();
331        assert!(text.starts_with("P2"));
332        assert!(text.contains("2 1"));
333        assert!(text.contains("128"));
334        assert!(text.contains("255"));
335    }
336
337    #[test]
338    fn test_extension_and_mime() {
339        let ppm = PnmExporter::ppm();
340        assert_eq!(ppm.extension(), "ppm");
341        assert_eq!(ppm.mime_type(), "image/x-portable-pixmap");
342
343        let pgm = PnmExporter::pgm();
344        assert_eq!(pgm.extension(), "pgm");
345        assert_eq!(pgm.mime_type(), "image/x-portable-graymap");
346    }
347}