Skip to main content

typf_export/
json.rs

1//! JSON export format
2//!
3//! Exports shaping results to HarfBuzz-compatible JSON format.
4
5use serde::{Deserialize, Serialize};
6use typf_core::{
7    error::{ExportError, Result},
8    traits::Exporter,
9    types::{Direction, RenderOutput, ShapingResult},
10};
11
12/// JSON exporter for shaping results
13///
14/// Produces HarfBuzz-compatible JSON output for debugging and testing.
15///
16/// # Examples
17///
18/// ```ignore
19/// use typf_export::JsonExporter;
20///
21/// let exporter = JsonExporter::new();
22/// let json = exporter.export(&render_output)?;
23/// println!("{}", String::from_utf8_lossy(&json));
24/// ```
25pub struct JsonExporter {
26    /// Whether to pretty-print the JSON
27    pretty: bool,
28}
29
30impl JsonExporter {
31    /// Create a new JSON exporter
32    pub fn new() -> Self {
33        Self { pretty: false }
34    }
35
36    /// Create a JSON exporter with pretty-printing enabled
37    pub fn with_pretty_print() -> Self {
38        Self { pretty: true }
39    }
40
41    /// Export shaping result to HarfBuzz-compatible JSON
42    pub fn export_shaping(&self, shaped: &ShapingResult) -> Result<Vec<u8>> {
43        let output = HarfBuzzOutput {
44            glyphs: shaped
45                .glyphs
46                .iter()
47                .map(|g| HarfBuzzGlyph {
48                    glyph_id: g.id,
49                    cluster: g.cluster,
50                    x_advance: (g.advance * 64.0) as i32, // Convert to 26.6 fixed point
51                    y_advance: 0,
52                    x_offset: (g.x * 64.0) as i32,
53                    y_offset: (g.y * 64.0) as i32,
54                })
55                .collect(),
56            advance_width: shaped.advance_width,
57            advance_height: shaped.advance_height,
58            direction: match shaped.direction {
59                Direction::LeftToRight => "ltr",
60                Direction::RightToLeft => "rtl",
61                Direction::TopToBottom => "ttb",
62                Direction::BottomToTop => "btt",
63            }
64            .to_string(),
65        };
66
67        let json = if self.pretty {
68            serde_json::to_string_pretty(&output)
69        } else {
70            serde_json::to_string(&output)
71        }
72        .map_err(|e| ExportError::EncodingFailed(e.to_string()))?;
73
74        Ok(json.into_bytes())
75    }
76}
77
78impl Default for JsonExporter {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl Exporter for JsonExporter {
85    fn name(&self) -> &'static str {
86        "json"
87    }
88
89    fn export(&self, output: &RenderOutput) -> Result<Vec<u8>> {
90        match output {
91            RenderOutput::Json(json) => Ok(json.as_bytes().to_vec()),
92            _ => Err(ExportError::FormatNotSupported(
93                "JSON exporter requires JSON render output".into(),
94            )
95            .into()),
96        }
97    }
98
99    fn extension(&self) -> &'static str {
100        "json"
101    }
102
103    fn mime_type(&self) -> &'static str {
104        "application/json"
105    }
106}
107
108/// HarfBuzz-compatible JSON output structure
109#[derive(Debug, Clone, Serialize, Deserialize)]
110struct HarfBuzzOutput {
111    glyphs: Vec<HarfBuzzGlyph>,
112    advance_width: f32,
113    advance_height: f32,
114    direction: String,
115}
116
117/// HarfBuzz-compatible glyph information
118#[derive(Debug, Clone, Serialize, Deserialize)]
119struct HarfBuzzGlyph {
120    #[serde(rename = "g")]
121    glyph_id: u32,
122    #[serde(rename = "cl")]
123    cluster: u32,
124    #[serde(rename = "ax")]
125    x_advance: i32,
126    #[serde(rename = "ay")]
127    y_advance: i32,
128    #[serde(rename = "dx")]
129    x_offset: i32,
130    #[serde(rename = "dy")]
131    y_offset: i32,
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use typf_core::types::PositionedGlyph;
138
139    fn create_test_shaping() -> ShapingResult {
140        ShapingResult {
141            glyphs: vec![
142                PositionedGlyph {
143                    id: 72, // 'H'
144                    x: 0.0,
145                    y: 0.0,
146                    advance: 10.0,
147                    cluster: 0,
148                },
149                PositionedGlyph {
150                    id: 101, // 'e'
151                    x: 10.0,
152                    y: 0.0,
153                    advance: 8.0,
154                    cluster: 1,
155                },
156            ],
157            advance_width: 18.0,
158            advance_height: 16.0,
159            direction: Direction::LeftToRight,
160        }
161    }
162
163    #[test]
164    fn test_json_exporter_creation() {
165        let exporter = JsonExporter::new();
166        assert!(!exporter.pretty);
167
168        let pretty_exporter = JsonExporter::with_pretty_print();
169        assert!(pretty_exporter.pretty);
170    }
171
172    #[test]
173    fn test_export_shaping_to_json() {
174        let exporter = JsonExporter::new();
175        let shaped = create_test_shaping();
176
177        let result = exporter.export_shaping(&shaped);
178        assert!(result.is_ok());
179
180        let json = String::from_utf8(result.unwrap()).unwrap();
181        assert!(json.contains("\"g\":72"));
182        assert!(json.contains("\"cl\":0"));
183        assert!(json.contains("\"ax\":640")); // 10.0 * 64
184    }
185
186    #[test]
187    fn test_pretty_print() {
188        let exporter = JsonExporter::with_pretty_print();
189        let shaped = create_test_shaping();
190
191        let json = String::from_utf8(exporter.export_shaping(&shaped).unwrap()).unwrap();
192        // Pretty-printed JSON should have newlines
193        assert!(json.contains('\n'));
194        assert!(json.contains("  ")); // Indentation
195    }
196
197    #[test]
198    fn test_direction_encoding() {
199        let mut shaped = create_test_shaping();
200
201        // Test all directions
202        let exporter = JsonExporter::new();
203
204        shaped.direction = Direction::LeftToRight;
205        let json = String::from_utf8(exporter.export_shaping(&shaped).unwrap()).unwrap();
206        assert!(json.contains("\"ltr\""));
207
208        shaped.direction = Direction::RightToLeft;
209        let json = String::from_utf8(exporter.export_shaping(&shaped).unwrap()).unwrap();
210        assert!(json.contains("\"rtl\""));
211
212        shaped.direction = Direction::TopToBottom;
213        let json = String::from_utf8(exporter.export_shaping(&shaped).unwrap()).unwrap();
214        assert!(json.contains("\"ttb\""));
215
216        shaped.direction = Direction::BottomToTop;
217        let json = String::from_utf8(exporter.export_shaping(&shaped).unwrap()).unwrap();
218        assert!(json.contains("\"btt\""));
219    }
220
221    #[test]
222    fn test_fixed_point_conversion() {
223        let exporter = JsonExporter::new();
224        let shaped = ShapingResult {
225            glyphs: vec![PositionedGlyph {
226                id: 1,
227                x: 1.5,        // Should become 96 (1.5 * 64)
228                y: 0.5,        // Should become 32 (0.5 * 64)
229                advance: 10.5, // Should become 672 (10.5 * 64)
230                cluster: 0,
231            }],
232            advance_width: 10.5,
233            advance_height: 16.0,
234            direction: Direction::LeftToRight,
235        };
236
237        let json = String::from_utf8(exporter.export_shaping(&shaped).unwrap()).unwrap();
238        assert!(json.contains("\"ax\":672"));
239        assert!(json.contains("\"dx\":96"));
240        assert!(json.contains("\"dy\":32"));
241    }
242
243    #[test]
244    fn test_exporter_trait() {
245        let exporter = JsonExporter::new();
246        assert_eq!(exporter.name(), "json");
247        assert_eq!(exporter.extension(), "json");
248        assert_eq!(exporter.mime_type(), "application/json");
249    }
250}