1#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
9pub enum FontStyle {
10 Regular,
11 Bold,
12 Italic,
13 BoldItalic,
14}
15
16impl FontStyle {
17 pub fn xml_element(&self) -> &'static str {
18 match self {
19 FontStyle::Regular => "regular",
20 FontStyle::Bold => "bold",
21 FontStyle::Italic => "italic",
22 FontStyle::BoldItalic => "boldItalic",
23 }
24 }
25
26 pub fn is_bold(&self) -> bool {
27 matches!(self, FontStyle::Bold | FontStyle::BoldItalic)
28 }
29
30 pub fn is_italic(&self) -> bool {
31 matches!(self, FontStyle::Italic | FontStyle::BoldItalic)
32 }
33}
34
35#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)]
37pub enum FontCharset {
38 #[default]
39 Ansi,
40 Symbol,
41 ShiftJis,
42 Hangul,
43 Gb2312,
44 ChineseBig5,
45 Greek,
46 Turkish,
47 Hebrew,
48 Arabic,
49 Baltic,
50 Russian,
51 Thai,
52 EastEurope,
53}
54
55impl FontCharset {
56 pub fn code(&self) -> u8 {
57 match self {
58 FontCharset::Ansi => 0x00,
59 FontCharset::Symbol => 0x02,
60 FontCharset::ShiftJis => 0x80,
61 FontCharset::Hangul => 0x81,
62 FontCharset::Gb2312 => 0x86,
63 FontCharset::ChineseBig5 => 0x88,
64 FontCharset::Greek => 0xA1,
65 FontCharset::Turkish => 0xA2,
66 FontCharset::Hebrew => 0xB1,
67 FontCharset::Arabic => 0xB2,
68 FontCharset::Baltic => 0xBA,
69 FontCharset::Russian => 0xCC,
70 FontCharset::Thai => 0xDE,
71 FontCharset::EastEurope => 0xEE,
72 }
73 }
74}
75
76#[derive(Clone, Debug)]
78pub struct EmbeddedFont {
79 pub typeface: String,
80 pub style: FontStyle,
81 pub charset: FontCharset,
82 pub panose: Option<String>,
83 pub pitch_family: u8,
84 pub data: Vec<u8>,
85 pub relationship_id: String,
86}
87
88impl EmbeddedFont {
89 pub fn new(typeface: &str, style: FontStyle, data: Vec<u8>, rel_id: &str) -> Self {
91 Self {
92 typeface: typeface.to_string(),
93 style,
94 charset: FontCharset::default(),
95 panose: None,
96 pitch_family: 0x22, data,
98 relationship_id: rel_id.to_string(),
99 }
100 }
101
102 pub fn charset(mut self, charset: FontCharset) -> Self {
103 self.charset = charset;
104 self
105 }
106
107 pub fn panose(mut self, panose: &str) -> Self {
108 self.panose = Some(panose.to_string());
109 self
110 }
111
112 pub fn pitch_family(mut self, pf: u8) -> Self {
113 self.pitch_family = pf;
114 self
115 }
116
117 pub fn data_size(&self) -> usize {
119 self.data.len()
120 }
121
122 pub fn content_type() -> &'static str {
124 "application/x-fontdata"
125 }
126
127 pub fn part_name(&self) -> String {
129 format!(
130 "ppt/fonts/{}-{}.fntdata",
131 self.typeface.replace(' ', ""),
132 self.style.xml_element()
133 )
134 }
135
136 pub fn rel_target(&self) -> String {
138 format!(
139 "fonts/{}-{}.fntdata",
140 self.typeface.replace(' ', ""),
141 self.style.xml_element()
142 )
143 }
144}
145
146#[derive(Clone, Debug, Default)]
148pub struct EmbeddedFontList {
149 fonts: Vec<EmbeddedFont>,
150}
151
152impl EmbeddedFontList {
153 pub fn new() -> Self {
154 Self::default()
155 }
156
157 pub fn add(&mut self, font: EmbeddedFont) {
159 self.fonts.push(font);
160 }
161
162 pub fn fonts(&self) -> &[EmbeddedFont] {
164 &self.fonts
165 }
166
167 pub fn len(&self) -> usize {
169 self.fonts.len()
170 }
171
172 pub fn is_empty(&self) -> bool {
173 self.fonts.is_empty()
174 }
175
176 pub fn assign_relationship_ids(&mut self, start: usize) {
179 for (i, font) in self.fonts.iter_mut().enumerate() {
180 if font.relationship_id.is_empty() {
181 font.relationship_id = format!("rId{}", start + i);
182 }
183 }
184 }
185
186 pub fn total_size(&self) -> usize {
188 self.fonts.iter().map(|f| f.data_size()).sum()
189 }
190
191 pub fn find_by_typeface(&self, typeface: &str) -> Vec<&EmbeddedFont> {
193 self.fonts.iter().filter(|f| f.typeface == typeface).collect()
194 }
195
196 pub fn to_xml(&self) -> String {
198 if self.fonts.is_empty() {
199 return String::new();
200 }
201
202 let mut xml = String::from("<p:embeddedFontLst>");
203
204 let mut seen_typefaces: Vec<String> = Vec::new();
206 for font in &self.fonts {
207 if !seen_typefaces.contains(&font.typeface) {
208 seen_typefaces.push(font.typeface.clone());
209 }
210 }
211
212 for typeface in &seen_typefaces {
213 let variants: Vec<&EmbeddedFont> = self.fonts
214 .iter()
215 .filter(|f| &f.typeface == typeface)
216 .collect();
217
218 xml.push_str("<p:embeddedFont>");
219
220 if let Some(first) = variants.first() {
222 let panose_attr = first.panose.as_ref()
223 .map(|p| format!(r#" panose="{}""#, p))
224 .unwrap_or_default();
225 xml.push_str(&format!(
226 r#"<p:font typeface="{}" charset="{}" pitchFamily="{}"{}/>"#,
227 xml_escape(typeface),
228 first.charset.code(),
229 first.pitch_family,
230 panose_attr,
231 ));
232 }
233
234 for font in &variants {
236 xml.push_str(&format!(
237 r#"<p:{} r:id="{}"/>"#,
238 font.style.xml_element(),
239 font.relationship_id,
240 ));
241 }
242
243 xml.push_str("</p:embeddedFont>");
244 }
245
246 xml.push_str("</p:embeddedFontLst>");
247 xml
248 }
249}
250
251fn xml_escape(s: &str) -> String {
252 s.replace('&', "&")
253 .replace('<', "<")
254 .replace('>', ">")
255 .replace('"', """)
256 .replace('\'', "'")
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_font_style_xml() {
265 assert_eq!(FontStyle::Regular.xml_element(), "regular");
266 assert_eq!(FontStyle::Bold.xml_element(), "bold");
267 assert_eq!(FontStyle::Italic.xml_element(), "italic");
268 assert_eq!(FontStyle::BoldItalic.xml_element(), "boldItalic");
269 }
270
271 #[test]
272 fn test_font_style_flags() {
273 assert!(!FontStyle::Regular.is_bold());
274 assert!(!FontStyle::Regular.is_italic());
275 assert!(FontStyle::Bold.is_bold());
276 assert!(!FontStyle::Bold.is_italic());
277 assert!(!FontStyle::Italic.is_bold());
278 assert!(FontStyle::Italic.is_italic());
279 assert!(FontStyle::BoldItalic.is_bold());
280 assert!(FontStyle::BoldItalic.is_italic());
281 }
282
283 #[test]
284 fn test_font_charset_default() {
285 assert_eq!(FontCharset::default(), FontCharset::Ansi);
286 assert_eq!(FontCharset::Ansi.code(), 0x00);
287 }
288
289 #[test]
290 fn test_font_charset_codes() {
291 assert_eq!(FontCharset::Symbol.code(), 0x02);
292 assert_eq!(FontCharset::ShiftJis.code(), 0x80);
293 assert_eq!(FontCharset::Arabic.code(), 0xB2);
294 assert_eq!(FontCharset::Russian.code(), 0xCC);
295 }
296
297 #[test]
298 fn test_embedded_font_new() {
299 let font = EmbeddedFont::new("Arial", FontStyle::Regular, vec![1, 2, 3], "rId10");
300 assert_eq!(font.typeface, "Arial");
301 assert_eq!(font.style, FontStyle::Regular);
302 assert_eq!(font.data_size(), 3);
303 assert_eq!(font.relationship_id, "rId10");
304 }
305
306 #[test]
307 fn test_embedded_font_builder() {
308 let font = EmbeddedFont::new("Calibri", FontStyle::Bold, vec![0; 100], "rId1")
309 .charset(FontCharset::Russian)
310 .panose("020F0502020204030204")
311 .pitch_family(0x34);
312 assert_eq!(font.charset, FontCharset::Russian);
313 assert_eq!(font.panose.as_deref(), Some("020F0502020204030204"));
314 assert_eq!(font.pitch_family, 0x34);
315 }
316
317 #[test]
318 fn test_embedded_font_part_name() {
319 let font = EmbeddedFont::new("Times New Roman", FontStyle::BoldItalic, vec![], "rId1");
320 assert_eq!(font.part_name(), "ppt/fonts/TimesNewRoman-boldItalic.fntdata");
321 }
322
323 #[test]
324 fn test_embedded_font_content_type() {
325 assert_eq!(EmbeddedFont::content_type(), "application/x-fontdata");
326 }
327
328 #[test]
329 fn test_embedded_font_list_new() {
330 let list = EmbeddedFontList::new();
331 assert!(list.is_empty());
332 assert_eq!(list.len(), 0);
333 assert_eq!(list.total_size(), 0);
334 }
335
336 #[test]
337 fn test_embedded_font_list_add() {
338 let mut list = EmbeddedFontList::new();
339 list.add(EmbeddedFont::new("Arial", FontStyle::Regular, vec![0; 50], "rId1"));
340 list.add(EmbeddedFont::new("Arial", FontStyle::Bold, vec![0; 60], "rId2"));
341 assert_eq!(list.len(), 2);
342 assert_eq!(list.total_size(), 110);
343 }
344
345 #[test]
346 fn test_embedded_font_list_find() {
347 let mut list = EmbeddedFontList::new();
348 list.add(EmbeddedFont::new("Arial", FontStyle::Regular, vec![], "rId1"));
349 list.add(EmbeddedFont::new("Calibri", FontStyle::Regular, vec![], "rId2"));
350 list.add(EmbeddedFont::new("Arial", FontStyle::Bold, vec![], "rId3"));
351 assert_eq!(list.find_by_typeface("Arial").len(), 2);
352 assert_eq!(list.find_by_typeface("Calibri").len(), 1);
353 assert_eq!(list.find_by_typeface("Missing").len(), 0);
354 }
355
356 #[test]
357 fn test_embedded_font_list_xml_empty() {
358 let list = EmbeddedFontList::new();
359 assert_eq!(list.to_xml(), "");
360 }
361
362 #[test]
363 fn test_embedded_font_list_xml() {
364 let mut list = EmbeddedFontList::new();
365 list.add(EmbeddedFont::new("Arial", FontStyle::Regular, vec![0; 10], "rId10"));
366 list.add(EmbeddedFont::new("Arial", FontStyle::Bold, vec![0; 10], "rId11"));
367 let xml = list.to_xml();
368 assert!(xml.contains("<p:embeddedFontLst>"));
369 assert!(xml.contains("</p:embeddedFontLst>"));
370 assert!(xml.contains(r#"typeface="Arial""#));
371 assert!(xml.contains(r#"r:id="rId10""#));
372 assert!(xml.contains(r#"r:id="rId11""#));
373 assert!(xml.contains("<p:regular"));
374 assert!(xml.contains("<p:bold"));
375 assert_eq!(xml.matches("<p:embeddedFont>").count(), 1);
377 }
378
379 #[test]
380 fn test_embedded_font_list_xml_multiple_typefaces() {
381 let mut list = EmbeddedFontList::new();
382 list.add(EmbeddedFont::new("Arial", FontStyle::Regular, vec![], "rId1"));
383 list.add(EmbeddedFont::new("Calibri", FontStyle::Regular, vec![], "rId2"));
384 let xml = list.to_xml();
385 assert_eq!(xml.matches("<p:embeddedFont>").count(), 2);
386 assert!(xml.contains("Arial"));
387 assert!(xml.contains("Calibri"));
388 }
389
390 #[test]
391 fn test_embedded_font_list_xml_with_panose() {
392 let mut list = EmbeddedFontList::new();
393 list.add(
394 EmbeddedFont::new("Calibri", FontStyle::Regular, vec![], "rId1")
395 .panose("020F0502020204030204"),
396 );
397 let xml = list.to_xml();
398 assert!(xml.contains(r#"panose="020F0502020204030204""#));
399 }
400}