1use oxml_drawing::theme::CT_FontCollection;
4
5use crate::ResolveCtx;
6
7impl ResolveCtx<'_> {
8 pub fn resolve_typeface(&self, typeface: &str, script: Option<&str>) -> String {
14 let scheme = &self.theme.theme_elements.font_scheme;
15 let (collection, base) = match typeface {
16 "+mj-lt" => (&scheme.major_font, &scheme.major_font.latin.typeface),
17 "+mn-lt" => (&scheme.minor_font, &scheme.minor_font.latin.typeface),
18 "+mj-ea" => (&scheme.major_font, &scheme.major_font.east_asian.typeface),
19 "+mn-ea" => (&scheme.minor_font, &scheme.minor_font.east_asian.typeface),
20 "+mj-cs" => (
21 &scheme.major_font,
22 &scheme.major_font.complex_script.typeface,
23 ),
24 "+mn-cs" => (
25 &scheme.minor_font,
26 &scheme.minor_font.complex_script.typeface,
27 ),
28 _ => return typeface.to_owned(),
29 };
30
31 supplemental_typeface(collection, script)
32 .unwrap_or(base)
33 .clone()
34 }
35}
36
37fn supplemental_typeface<'a>(
38 collection: &'a CT_FontCollection,
39 script: Option<&str>,
40) -> Option<&'a String> {
41 let script = script?;
42 collection
43 .supplemental_fonts
44 .iter()
45 .find(|font| font.script == script)
46 .map(|font| &font.typeface)
47}
48
49#[cfg(test)]
50mod tests {
51 use oxml_drawing::color::ColorMap;
52 use oxml_drawing::text::CT_TextListStyle;
53 use oxml_drawing::theme::CT_OfficeStyleSheet;
54 use rpptx_oxml::slide_parts::{CT_Slide, CT_SlideLayout, CT_SlideMaster};
55
56 use crate::ResolveCtx;
57
58 const P_NS: &str = "http://schemas.openxmlformats.org/presentationml/2006/main";
59
60 #[test]
61 fn minor_latin_theme_token_resolves_to_minor_latin_typeface() {
62 let fixture = Fixture::new();
63
64 assert_eq!(fixture.context().resolve_typeface("+mn-lt", None), "Aptos");
65 }
66
67 #[test]
68 fn major_and_minor_tokens_select_each_base_face() {
69 let fixture = Fixture::new();
70 let context = fixture.context();
71
72 for (token, expected) in [
73 ("+mj-lt", "Aptos Display"),
74 ("+mn-lt", "Aptos"),
75 ("+mj-ea", ""),
76 ("+mn-ea", ""),
77 ("+mj-cs", ""),
78 ("+mn-cs", ""),
79 ] {
80 assert_eq!(context.resolve_typeface(token, None), expected);
81 }
82 }
83
84 #[test]
85 fn supplemental_script_face_overrides_the_base() {
86 let fixture = Fixture::new();
87 let context = fixture.context();
88
89 assert_eq!(
90 context.resolve_typeface("+mj-lt", Some("Jpan")),
91 "游ゴシック Light"
92 );
93 assert_eq!(
94 context.resolve_typeface("+mn-ea", Some("Jpan")),
95 "游ゴシック"
96 );
97 }
98
99 #[test]
100 fn missing_script_override_falls_back_to_the_base() {
101 let fixture = Fixture::new();
102 let context = fixture.context();
103
104 assert_eq!(
105 context.resolve_typeface("+mj-lt", Some("Zzzz")),
106 "Aptos Display"
107 );
108 assert_eq!(context.resolve_typeface("+mn-cs", None), "");
109 }
110
111 #[test]
112 fn ordinary_and_unknown_typefaces_pass_through() {
113 let fixture = Fixture::new();
114 let context = fixture.context();
115
116 assert_eq!(context.resolve_typeface("Arial", Some("Jpan")), "Arial");
117 assert_eq!(context.resolve_typeface("+mn-zz", Some("Jpan")), "+mn-zz");
118 assert_eq!(context.resolve_typeface("+future", None), "+future");
119 }
120
121 #[test]
122 fn duplicate_supplemental_scripts_use_document_order() {
123 let mut fixture = Fixture::new();
124 let fonts = &mut fixture
125 .theme
126 .theme_elements
127 .font_scheme
128 .minor_font
129 .supplemental_fonts;
130 let mut first = fonts[0].clone();
131 first.script = "Test".to_owned();
132 first.typeface = "First Face".to_owned();
133 let mut second = fonts[1].clone();
134 second.script = "Test".to_owned();
135 second.typeface = "Second Face".to_owned();
136 fonts.insert(0, second);
137 fonts.insert(0, first);
138
139 assert_eq!(
140 fixture.context().resolve_typeface("+mn-lt", Some("Test")),
141 "First Face"
142 );
143 }
144
145 struct Fixture {
146 theme: CT_OfficeStyleSheet,
147 master: CT_SlideMaster,
148 layout: CT_SlideLayout,
149 slide: CT_Slide,
150 default_text_style: CT_TextListStyle,
151 }
152
153 impl Fixture {
154 fn new() -> Self {
155 Self {
156 theme: CT_OfficeStyleSheet::office_default(),
157 master: CT_SlideMaster::from_xml(master_xml().as_bytes()).unwrap(),
158 layout: CT_SlideLayout::from_xml(layout_xml().as_bytes()).unwrap(),
159 slide: CT_Slide::from_xml(slide_xml().as_bytes()).unwrap(),
160 default_text_style: CT_TextListStyle::default(),
161 }
162 }
163
164 fn context(&self) -> ResolveCtx<'_> {
165 ResolveCtx::new(
166 &self.theme,
167 ColorMap::default(),
168 &self.master,
169 &self.layout,
170 &self.slide,
171 &self.default_text_style,
172 )
173 }
174 }
175
176 fn slide_xml() -> String {
177 format!(
178 "<p:sld xmlns:p=\"{P_NS}\"><p:cSld>{}</p:cSld></p:sld>",
179 shape_tree()
180 )
181 }
182
183 fn layout_xml() -> String {
184 format!(
185 "<p:sldLayout xmlns:p=\"{P_NS}\"><p:cSld>{}</p:cSld></p:sldLayout>",
186 shape_tree()
187 )
188 }
189
190 fn master_xml() -> String {
191 format!(
192 "<p:sldMaster xmlns:p=\"{P_NS}\"><p:cSld>{}</p:cSld><p:clrMap bg1=\"lt1\" tx1=\"dk1\" bg2=\"lt2\" tx2=\"dk2\" accent1=\"accent1\" accent2=\"accent2\" accent3=\"accent3\" accent4=\"accent4\" accent5=\"accent5\" accent6=\"accent6\" hlink=\"hlink\" folHlink=\"folHlink\"/></p:sldMaster>",
193 shape_tree()
194 )
195 }
196
197 fn shape_tree() -> &'static str {
198 "<p:spTree><p:nvGrpSpPr/><p:grpSpPr/></p:spTree>"
199 }
200}