Skip to main content

extend_docx/doc/
chp.rs

1//! Character properties (CHP): accumulated from style defaults plus CHPX sprm
2//! deltas, then emitted as OOXML `<w:rPr>` direct formatting.
3
4use super::fmt::{colorref_to_hex, escape_xml, ico_to_hex, ico_to_highlight, kul_to_val, Shading};
5use super::sprm::{self, Sprm};
6
7pub const TOGGLE_COUNT: usize = 10;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Toggle {
11    Bold = 0,
12    Italic = 1,
13    Strike = 2,
14    Outline = 3,
15    Shadow = 4,
16    SmallCaps = 5,
17    Caps = 6,
18    Vanish = 7,
19    Emboss = 8,
20    Imprint = 9,
21}
22
23fn toggle_for_opcode(opcode: u16) -> Option<Toggle> {
24    Some(match opcode {
25        0x0835 => Toggle::Bold,
26        0x0836 => Toggle::Italic,
27        0x0837 => Toggle::Strike,
28        0x0838 => Toggle::Outline,
29        0x0839 => Toggle::Shadow,
30        0x083A => Toggle::SmallCaps,
31        0x083B => Toggle::Caps,
32        0x083C => Toggle::Vanish,
33        0x0858 => Toggle::Emboss,
34        0x0854 => Toggle::Imprint,
35        _ => return None,
36    })
37}
38
39/// Per-toggle tri-state used while resolving style chains: None = the chain
40/// never mentions the property.
41#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
42pub struct ToggleSet {
43    pub values: [Option<bool>; TOGGLE_COUNT],
44}
45
46impl ToggleSet {
47    pub fn get(&self, toggle: Toggle) -> Option<bool> {
48        self.values[toggle as usize]
49    }
50
51    pub fn set(&mut self, toggle: Toggle, value: Option<bool>) {
52        self.values[toggle as usize] = value;
53    }
54
55    /// Applies the toggle sprms of a style-chain grpprl on top of this set.
56    pub fn apply_style_grpprl(&mut self, grpprl: &[u8]) {
57        for sprm in sprm::iterate(grpprl) {
58            if let Some(toggle) = toggle_for_opcode(sprm.opcode) {
59                let current = self.get(toggle).unwrap_or(false);
60                match sprm.byte() {
61                    0 => self.set(toggle, Some(false)),
62                    1 => self.set(toggle, Some(true)),
63                    0x80 => {}
64                    0x81 => self.set(toggle, Some(!current)),
65                    _ => {}
66                }
67            }
68        }
69    }
70
71    /// Overlays `other` (e.g. a character style) on top of this set.
72    pub fn overlay(&self, other: &ToggleSet) -> ToggleSet {
73        let mut merged = *self;
74        for index in 0..TOGGLE_COUNT {
75            if other.values[index].is_some() {
76                merged.values[index] = other.values[index];
77            }
78        }
79        merged
80    }
81}
82
83/// Character properties as OOXML-ready deltas; `None` everywhere means
84/// "inherit from the referenced styles".
85#[derive(Debug, Default, Clone, PartialEq)]
86pub struct Chp {
87    pub istd: Option<u16>,
88    pub toggles: [Option<bool>; TOGGLE_COUNT],
89    pub dstrike: Option<bool>,
90    pub kul: Option<u8>,
91    pub ico: Option<u8>,
92    pub cv: Option<[u8; 4]>,
93    pub hps: Option<u16>,
94    pub hps_pos: Option<i16>,
95    pub iss: Option<u8>,
96    pub ftc_ascii: Option<u16>,
97    pub ftc_fe: Option<u16>,
98    pub ftc_other: Option<u16>,
99    pub kern: Option<u16>,
100    pub dxa_space: Option<i16>,
101    pub char_scale: Option<u16>,
102    pub highlight: Option<u8>,
103    pub shd: Option<Shading>,
104    pub f_spec: bool,
105    pub deleted: bool,
106    pub f_obj: bool,
107    pub f_ole2: bool,
108    pub f_data: bool,
109    pub pic_location: Option<u32>,
110    pub symbol: Option<(u16, u16)>,
111}
112
113impl Chp {
114    /// Applies a CHPX grpprl. `base_toggles` carries the values the style sheet
115    /// produces for the current character or paragraph style, needed for
116    /// style-relative toggle operands.
117    pub fn apply_chpx(&mut self, grpprl: &[u8], base_toggles: &ToggleSet) {
118        for sprm in sprm::iterate(grpprl) {
119            self.apply_sprm(&sprm, base_toggles);
120        }
121    }
122
123    pub fn apply_sprm(&mut self, sprm: &Sprm<'_>, base_toggles: &ToggleSet) {
124        if let Some(toggle) = toggle_for_opcode(sprm.opcode) {
125            let style_value = base_toggles.get(toggle).unwrap_or(false);
126            match sprm.byte() {
127                0 => self.toggles[toggle as usize] = Some(false),
128                1 => self.toggles[toggle as usize] = Some(true),
129                0x80 => self.toggles[toggle as usize] = Some(style_value),
130                0x81 => self.toggles[toggle as usize] = Some(!style_value),
131                _ => {}
132            }
133            return;
134        }
135        match sprm.opcode {
136            0x0800 => self.deleted = matches!(sprm.byte(), 1 | 0x81),
137            0x4A30 => self.istd = Some(sprm.u16_operand()),
138            0x2A53 => self.dstrike = Some(sprm.byte() == 1),
139            0x2A3E => self.kul = Some(sprm.byte()),
140            0x2A42 => self.ico = Some(sprm.byte()),
141            0x6870 => {
142                let mut cv = [0u8; 4];
143                cv.copy_from_slice(sprm.operand.get(..4).unwrap_or(&[0, 0, 0, 0xFF]));
144                self.cv = Some(cv);
145            }
146            0x4A43 => self.hps = Some(sprm.u16_operand()),
147            0x4845 => self.hps_pos = Some(sprm.i16_operand()),
148            0x2A48 => self.iss = Some(sprm.byte()),
149            0x4A4F => self.ftc_ascii = Some(sprm.u16_operand()),
150            0x4A50 => self.ftc_fe = Some(sprm.u16_operand()),
151            0x4A51 => self.ftc_other = Some(sprm.u16_operand()),
152            0x484B => self.kern = Some(sprm.u16_operand()),
153            0x8840 => self.dxa_space = Some(sprm.i16_operand()),
154            0x852A => self.char_scale = Some(sprm.u16_operand()),
155            0x2A0C => self.highlight = Some(sprm.byte()),
156            0x4866 => self.shd = super::fmt::parse_shd80(sprm.u16_operand()),
157            0xCA71 => self.shd = super::fmt::parse_shd_operand(sprm.operand),
158            0x0855 => self.f_spec = sprm.byte() == 1,
159            0x0856 => self.f_obj = sprm.byte() == 1,
160            0x080A => self.f_ole2 = sprm.byte() == 1,
161            0x0806 => self.f_data = sprm.byte() == 1,
162            0x6A03 => self.pic_location = Some(sprm.u32_operand()),
163            0x6A09 => {
164                if sprm.operand.len() >= 4 {
165                    let ftc = u16::from_le_bytes([sprm.operand[0], sprm.operand[1]]);
166                    let xchar = u16::from_le_bytes([sprm.operand[2], sprm.operand[3]]);
167                    self.symbol = Some((ftc, xchar));
168                    self.f_spec = true;
169                }
170            }
171            _ => {}
172        }
173    }
174
175    fn toggle_xml(&self, toggle: Toggle, tag: &str, out: &mut String) {
176        match self.toggles[toggle as usize] {
177            Some(true) => out.push_str(&format!("<{tag}/>")),
178            Some(false) => out.push_str(&format!(r#"<{tag} w:val="0"/>"#)),
179            None => {}
180        }
181    }
182
183    /// Emits the `<w:rPr>` element for this run's direct formatting, or an
184    /// empty string when there is nothing to emit. `style_id` resolves a
185    /// character-style istd to its OOXML styleId; `font_name` resolves an ftc
186    /// index into the font table.
187    pub fn to_rpr_xml(
188        &self,
189        style_id: &impl Fn(u16) -> Option<String>,
190        font_name: &impl Fn(u16) -> Option<String>,
191    ) -> String {
192        let mut inner = String::new();
193
194        if let Some(istd) = self.istd {
195            // istd 10 is "Default Paragraph Font" — the implicit default.
196            if istd != 10 {
197                if let Some(id) = style_id(istd) {
198                    inner.push_str(&format!(r#"<w:rStyle w:val="{}"/>"#, escape_xml(&id)));
199                }
200            }
201        }
202
203        let ascii = self.ftc_ascii.and_then(font_name);
204        let east_asia = self.ftc_fe.and_then(font_name);
205        let other = self.ftc_other.and_then(font_name);
206        if ascii.is_some() || east_asia.is_some() || other.is_some() {
207            let mut fonts = String::from("<w:rFonts");
208            if let Some(name) = &ascii {
209                fonts.push_str(&format!(
210                    r#" w:ascii="{0}" w:hAnsi="{0}""#,
211                    escape_xml(name)
212                ));
213            }
214            if let Some(name) = &east_asia {
215                fonts.push_str(&format!(r#" w:eastAsia="{}""#, escape_xml(name)));
216            }
217            if let Some(name) = &other {
218                fonts.push_str(&format!(r#" w:cs="{}""#, escape_xml(name)));
219            }
220            fonts.push_str("/>");
221            inner.push_str(&fonts);
222        }
223
224        self.toggle_xml(Toggle::Bold, "w:b", &mut inner);
225        self.toggle_xml(Toggle::Italic, "w:i", &mut inner);
226        self.toggle_xml(Toggle::Caps, "w:caps", &mut inner);
227        self.toggle_xml(Toggle::SmallCaps, "w:smallCaps", &mut inner);
228        self.toggle_xml(Toggle::Strike, "w:strike", &mut inner);
229        if let Some(dstrike) = self.dstrike {
230            inner.push_str(if dstrike {
231                "<w:dstrike/>"
232            } else {
233                r#"<w:dstrike w:val="0"/>"#
234            });
235        }
236        self.toggle_xml(Toggle::Outline, "w:outline", &mut inner);
237        self.toggle_xml(Toggle::Shadow, "w:shadow", &mut inner);
238        self.toggle_xml(Toggle::Emboss, "w:emboss", &mut inner);
239        self.toggle_xml(Toggle::Imprint, "w:imprint", &mut inner);
240        self.toggle_xml(Toggle::Vanish, "w:vanish", &mut inner);
241
242        let color = self
243            .cv
244            .and_then(|cv| colorref_to_hex(&cv))
245            .or_else(|| self.ico.and_then(|ico| ico_to_hex(ico).map(str::to_string)));
246        if let Some(hex) = color {
247            inner.push_str(&format!(r#"<w:color w:val="{hex}"/>"#));
248        } else if self.cv.is_some() || self.ico == Some(0) {
249            inner.push_str(r#"<w:color w:val="auto"/>"#);
250        }
251
252        if let Some(dxa) = self.dxa_space {
253            inner.push_str(&format!(r#"<w:spacing w:val="{dxa}"/>"#));
254        }
255        if let Some(scale) = self.char_scale {
256            inner.push_str(&format!(r#"<w:w w:val="{scale}"/>"#));
257        }
258        if let Some(kern) = self.kern {
259            inner.push_str(&format!(r#"<w:kern w:val="{kern}"/>"#));
260        }
261        if let Some(pos) = self.hps_pos {
262            inner.push_str(&format!(r#"<w:position w:val="{pos}"/>"#));
263        }
264        if let Some(hps) = self.hps {
265            inner.push_str(&format!(r#"<w:sz w:val="{hps}"/><w:szCs w:val="{hps}"/>"#));
266        }
267        if let Some(ico) = self.highlight {
268            if let Some(name) = ico_to_highlight(ico) {
269                inner.push_str(&format!(r#"<w:highlight w:val="{name}"/>"#));
270            }
271        }
272        if let Some(kul) = self.kul {
273            inner.push_str(&format!(r#"<w:u w:val="{}"/>"#, kul_to_val(kul)));
274        }
275        if let Some(shd) = &self.shd {
276            inner.push_str(&shd.to_xml());
277        }
278        if let Some(iss) = self.iss {
279            let val = match iss {
280                1 => "superscript",
281                2 => "subscript",
282                _ => "baseline",
283            };
284            inner.push_str(&format!(r#"<w:vertAlign w:val="{val}"/>"#));
285        }
286
287        if inner.is_empty() {
288            String::new()
289        } else {
290            format!("<w:rPr>{inner}</w:rPr>")
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn applies_basic_formatting_sprms() {
301        // bold on, size 24 half-points, red color via cv.
302        let grpprl: Vec<u8> = vec![
303            0x35, 0x08, 0x01, //
304            0x43, 0x4A, 0x18, 0x00, //
305            0x70, 0x68, 0xFF, 0x00, 0x00, 0x00,
306        ];
307        let mut chp = Chp::default();
308        chp.apply_chpx(&grpprl, &ToggleSet::default());
309        assert_eq!(chp.toggles[Toggle::Bold as usize], Some(true));
310        assert_eq!(chp.hps, Some(24));
311
312        let rpr = chp.to_rpr_xml(&|_| None, &|_| None);
313        assert!(rpr.contains("<w:b/>"));
314        assert!(rpr.contains(r#"<w:sz w:val="24"/>"#));
315        assert!(rpr.contains(r#"<w:color w:val="FF0000"/>"#));
316    }
317
318    #[test]
319    fn toggle_invert_uses_style_value() {
320        let grpprl: Vec<u8> = vec![0x35, 0x08, 0x81]; // bold = invert style
321        let mut base = ToggleSet::default();
322        base.set(Toggle::Bold, Some(true));
323        let mut chp = Chp::default();
324        chp.apply_chpx(&grpprl, &base);
325        assert_eq!(chp.toggles[Toggle::Bold as usize], Some(false));
326    }
327
328    #[test]
329    fn toggle_as_style_emits_resolved_value() {
330        let grpprl: Vec<u8> = vec![0x35, 0x08, 0x80];
331        let mut chp = Chp::default();
332        chp.apply_chpx(&grpprl, &ToggleSet::default());
333        assert_eq!(chp.toggles[Toggle::Bold as usize], Some(false));
334        assert_eq!(
335            chp.to_rpr_xml(&|_| None, &|_| None),
336            r#"<w:rPr><w:b w:val="0"/></w:rPr>"#
337        );
338    }
339}