1use serde::{Deserialize, Serialize};
6use typf_core::{
7 error::{ExportError, Result},
8 traits::Exporter,
9 types::{Direction, RenderOutput, ShapingResult},
10};
11
12pub struct JsonExporter {
26 pretty: bool,
28}
29
30impl JsonExporter {
31 pub fn new() -> Self {
33 Self { pretty: false }
34 }
35
36 pub fn with_pretty_print() -> Self {
38 Self { pretty: true }
39 }
40
41 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, 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#[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#[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, x: 0.0,
145 y: 0.0,
146 advance: 10.0,
147 cluster: 0,
148 },
149 PositionedGlyph {
150 id: 101, 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")); }
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 assert!(json.contains('\n'));
194 assert!(json.contains(" ")); }
196
197 #[test]
198 fn test_direction_encoding() {
199 let mut shaped = create_test_shaping();
200
201 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, y: 0.5, advance: 10.5, 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}