Skip to main content

rpptx_layout/
font.rs

1//! Theme typeface resolution.
2
3use oxml_drawing::theme::CT_FontCollection;
4
5use crate::ResolveCtx;
6
7impl ResolveCtx<'_> {
8    /// Resolves a DrawingML theme typeface token to a concrete font face.
9    ///
10    /// A supplied ISO 15924 script selects the first matching supplemental
11    /// face in document order. Ordinary faces and unknown tokens pass through
12    /// unchanged.
13    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}