#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
pub enum FontStyle {
Regular,
Bold,
Italic,
BoldItalic,
}
impl FontStyle {
pub fn xml_element(&self) -> &'static str {
match self {
FontStyle::Regular => "regular",
FontStyle::Bold => "bold",
FontStyle::Italic => "italic",
FontStyle::BoldItalic => "boldItalic",
}
}
pub fn is_bold(&self) -> bool {
matches!(self, FontStyle::Bold | FontStyle::BoldItalic)
}
pub fn is_italic(&self) -> bool {
matches!(self, FontStyle::Italic | FontStyle::BoldItalic)
}
}
#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)]
pub enum FontCharset {
#[default]
Ansi,
Symbol,
ShiftJis,
Hangul,
Gb2312,
ChineseBig5,
Greek,
Turkish,
Hebrew,
Arabic,
Baltic,
Russian,
Thai,
EastEurope,
}
impl FontCharset {
pub fn code(&self) -> u8 {
match self {
FontCharset::Ansi => 0x00,
FontCharset::Symbol => 0x02,
FontCharset::ShiftJis => 0x80,
FontCharset::Hangul => 0x81,
FontCharset::Gb2312 => 0x86,
FontCharset::ChineseBig5 => 0x88,
FontCharset::Greek => 0xA1,
FontCharset::Turkish => 0xA2,
FontCharset::Hebrew => 0xB1,
FontCharset::Arabic => 0xB2,
FontCharset::Baltic => 0xBA,
FontCharset::Russian => 0xCC,
FontCharset::Thai => 0xDE,
FontCharset::EastEurope => 0xEE,
}
}
}
#[derive(Clone, Debug)]
pub struct EmbeddedFont {
pub typeface: String,
pub style: FontStyle,
pub charset: FontCharset,
pub panose: Option<String>,
pub pitch_family: u8,
pub data: Vec<u8>,
pub relationship_id: String,
}
impl EmbeddedFont {
pub fn new(typeface: &str, style: FontStyle, data: Vec<u8>, rel_id: &str) -> Self {
Self {
typeface: typeface.to_string(),
style,
charset: FontCharset::default(),
panose: None,
pitch_family: 0x22, data,
relationship_id: rel_id.to_string(),
}
}
pub fn charset(mut self, charset: FontCharset) -> Self {
self.charset = charset;
self
}
pub fn panose(mut self, panose: &str) -> Self {
self.panose = Some(panose.to_string());
self
}
pub fn pitch_family(mut self, pf: u8) -> Self {
self.pitch_family = pf;
self
}
pub fn data_size(&self) -> usize {
self.data.len()
}
pub fn content_type() -> &'static str {
"application/x-fontdata"
}
pub fn part_name(&self) -> String {
format!(
"ppt/fonts/{}-{}.fntdata",
self.typeface.replace(' ', ""),
self.style.xml_element()
)
}
}
#[derive(Clone, Debug, Default)]
pub struct EmbeddedFontList {
fonts: Vec<EmbeddedFont>,
}
impl EmbeddedFontList {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, font: EmbeddedFont) {
self.fonts.push(font);
}
pub fn fonts(&self) -> &[EmbeddedFont] {
&self.fonts
}
pub fn len(&self) -> usize {
self.fonts.len()
}
pub fn is_empty(&self) -> bool {
self.fonts.is_empty()
}
pub fn total_size(&self) -> usize {
self.fonts.iter().map(|f| f.data_size()).sum()
}
pub fn find_by_typeface(&self, typeface: &str) -> Vec<&EmbeddedFont> {
self.fonts.iter().filter(|f| f.typeface == typeface).collect()
}
pub fn to_xml(&self) -> String {
if self.fonts.is_empty() {
return String::new();
}
let mut xml = String::from("<p:embeddedFontLst>");
let mut seen_typefaces: Vec<String> = Vec::new();
for font in &self.fonts {
if !seen_typefaces.contains(&font.typeface) {
seen_typefaces.push(font.typeface.clone());
}
}
for typeface in &seen_typefaces {
let variants: Vec<&EmbeddedFont> = self.fonts
.iter()
.filter(|f| &f.typeface == typeface)
.collect();
xml.push_str("<p:embeddedFont>");
if let Some(first) = variants.first() {
let panose_attr = first.panose.as_ref()
.map(|p| format!(r#" panose="{}""#, p))
.unwrap_or_default();
xml.push_str(&format!(
r#"<p:font typeface="{}" charset="{}" pitchFamily="{}"{}/>"#,
xml_escape(typeface),
first.charset.code(),
first.pitch_family,
panose_attr,
));
}
for font in &variants {
xml.push_str(&format!(
r#"<p:{} r:id="{}"/>"#,
font.style.xml_element(),
font.relationship_id,
));
}
xml.push_str("</p:embeddedFont>");
}
xml.push_str("</p:embeddedFontLst>");
xml
}
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_font_style_xml() {
assert_eq!(FontStyle::Regular.xml_element(), "regular");
assert_eq!(FontStyle::Bold.xml_element(), "bold");
assert_eq!(FontStyle::Italic.xml_element(), "italic");
assert_eq!(FontStyle::BoldItalic.xml_element(), "boldItalic");
}
#[test]
fn test_font_style_flags() {
assert!(!FontStyle::Regular.is_bold());
assert!(!FontStyle::Regular.is_italic());
assert!(FontStyle::Bold.is_bold());
assert!(!FontStyle::Bold.is_italic());
assert!(!FontStyle::Italic.is_bold());
assert!(FontStyle::Italic.is_italic());
assert!(FontStyle::BoldItalic.is_bold());
assert!(FontStyle::BoldItalic.is_italic());
}
#[test]
fn test_font_charset_default() {
assert_eq!(FontCharset::default(), FontCharset::Ansi);
assert_eq!(FontCharset::Ansi.code(), 0x00);
}
#[test]
fn test_font_charset_codes() {
assert_eq!(FontCharset::Symbol.code(), 0x02);
assert_eq!(FontCharset::ShiftJis.code(), 0x80);
assert_eq!(FontCharset::Arabic.code(), 0xB2);
assert_eq!(FontCharset::Russian.code(), 0xCC);
}
#[test]
fn test_embedded_font_new() {
let font = EmbeddedFont::new("Arial", FontStyle::Regular, vec![1, 2, 3], "rId10");
assert_eq!(font.typeface, "Arial");
assert_eq!(font.style, FontStyle::Regular);
assert_eq!(font.data_size(), 3);
assert_eq!(font.relationship_id, "rId10");
}
#[test]
fn test_embedded_font_builder() {
let font = EmbeddedFont::new("Calibri", FontStyle::Bold, vec![0; 100], "rId1")
.charset(FontCharset::Russian)
.panose("020F0502020204030204")
.pitch_family(0x34);
assert_eq!(font.charset, FontCharset::Russian);
assert_eq!(font.panose.as_deref(), Some("020F0502020204030204"));
assert_eq!(font.pitch_family, 0x34);
}
#[test]
fn test_embedded_font_part_name() {
let font = EmbeddedFont::new("Times New Roman", FontStyle::BoldItalic, vec![], "rId1");
assert_eq!(font.part_name(), "ppt/fonts/TimesNewRoman-boldItalic.fntdata");
}
#[test]
fn test_embedded_font_content_type() {
assert_eq!(EmbeddedFont::content_type(), "application/x-fontdata");
}
#[test]
fn test_embedded_font_list_new() {
let list = EmbeddedFontList::new();
assert!(list.is_empty());
assert_eq!(list.len(), 0);
assert_eq!(list.total_size(), 0);
}
#[test]
fn test_embedded_font_list_add() {
let mut list = EmbeddedFontList::new();
list.add(EmbeddedFont::new("Arial", FontStyle::Regular, vec![0; 50], "rId1"));
list.add(EmbeddedFont::new("Arial", FontStyle::Bold, vec![0; 60], "rId2"));
assert_eq!(list.len(), 2);
assert_eq!(list.total_size(), 110);
}
#[test]
fn test_embedded_font_list_find() {
let mut list = EmbeddedFontList::new();
list.add(EmbeddedFont::new("Arial", FontStyle::Regular, vec![], "rId1"));
list.add(EmbeddedFont::new("Calibri", FontStyle::Regular, vec![], "rId2"));
list.add(EmbeddedFont::new("Arial", FontStyle::Bold, vec![], "rId3"));
assert_eq!(list.find_by_typeface("Arial").len(), 2);
assert_eq!(list.find_by_typeface("Calibri").len(), 1);
assert_eq!(list.find_by_typeface("Missing").len(), 0);
}
#[test]
fn test_embedded_font_list_xml_empty() {
let list = EmbeddedFontList::new();
assert_eq!(list.to_xml(), "");
}
#[test]
fn test_embedded_font_list_xml() {
let mut list = EmbeddedFontList::new();
list.add(EmbeddedFont::new("Arial", FontStyle::Regular, vec![0; 10], "rId10"));
list.add(EmbeddedFont::new("Arial", FontStyle::Bold, vec![0; 10], "rId11"));
let xml = list.to_xml();
assert!(xml.contains("<p:embeddedFontLst>"));
assert!(xml.contains("</p:embeddedFontLst>"));
assert!(xml.contains(r#"typeface="Arial""#));
assert!(xml.contains(r#"r:id="rId10""#));
assert!(xml.contains(r#"r:id="rId11""#));
assert!(xml.contains("<p:regular"));
assert!(xml.contains("<p:bold"));
assert_eq!(xml.matches("<p:embeddedFont>").count(), 1);
}
#[test]
fn test_embedded_font_list_xml_multiple_typefaces() {
let mut list = EmbeddedFontList::new();
list.add(EmbeddedFont::new("Arial", FontStyle::Regular, vec![], "rId1"));
list.add(EmbeddedFont::new("Calibri", FontStyle::Regular, vec![], "rId2"));
let xml = list.to_xml();
assert_eq!(xml.matches("<p:embeddedFont>").count(), 2);
assert!(xml.contains("Arial"));
assert!(xml.contains("Calibri"));
}
#[test]
fn test_embedded_font_list_xml_with_panose() {
let mut list = EmbeddedFontList::new();
list.add(
EmbeddedFont::new("Calibri", FontStyle::Regular, vec![], "rId1")
.panose("020F0502020204030204"),
);
let xml = list.to_xml();
assert!(xml.contains(r#"panose="020F0502020204030204""#));
}
}