1use 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#[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 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 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#[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 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 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 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 let grpprl: Vec<u8> = vec![
303 0x35, 0x08, 0x01, 0x43, 0x4A, 0x18, 0x00, 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]; 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}