1use std::collections::{HashMap, HashSet};
7
8use crate::package::{OoxmlPackage, OoxmlPart};
9
10use super::chp::Chp;
11use super::fib::fcidx;
12use super::fkp;
13use super::fmt::{escape_xml, escape_xml_into};
14use super::fonts::FontTable;
15use super::images::{extract_image, InlineImage};
16use super::lists::ListTables;
17use super::pap::Pap;
18use super::sep::Sep;
19use super::stsh::StyleSheet;
20use super::tap::Tap;
21use super::textboxes::{parse_text_boxes, TextBoxSpec, TextBoxStory};
22use super::DocFile;
23
24const W_NS: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
25const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
26const WP_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
27const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
28const PIC_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/picture";
29
30const REL_TYPE_HEADER: &str =
31 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
32const REL_TYPE_FOOTER: &str =
33 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
34const REL_TYPE_IMAGE: &str =
35 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
36const REL_TYPE_STYLES: &str =
37 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
38const REL_TYPE_NUMBERING: &str =
39 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
40const REL_TYPE_SETTINGS: &str =
41 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
42const REL_TYPE_FOOTNOTES: &str =
43 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
44const REL_TYPE_ENDNOTES: &str =
45 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
46
47#[derive(Default)]
48struct RelSet {
49 rels: Vec<(String, String, String)>, }
51
52impl RelSet {
53 fn add(&mut self, rel_type: &str, target: &str) -> String {
54 let id = format!("rId{}", self.rels.len() + 1);
55 self.rels
56 .push((id.clone(), rel_type.to_string(), target.to_string()));
57 id
58 }
59
60 fn to_xml(&self) -> String {
61 let mut xml = String::from(
62 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#,
63 );
64 for (id, rel_type, target) in &self.rels {
65 xml.push_str(&format!(
66 r#"<Relationship Id="{id}" Type="{rel_type}" Target="{}"/>"#,
67 escape_xml(target)
68 ));
69 }
70 xml.push_str("</Relationships>");
71 xml
72 }
73}
74
75#[derive(Debug, Clone)]
76struct CharRun {
77 cp_start: u32,
78 cp_end: u32,
79 grpprl: Vec<u8>,
80}
81
82struct ParaOut {
84 pap: Pap,
85 runs_xml: String,
86 mark_rpr: String,
89 mark_char: u16,
90 effective_itap: u32,
91 is_row_end_level: u32, cp_end: u32,
93 sect_index: Option<usize>, }
95
96#[derive(Default, Clone, Copy, PartialEq)]
99enum FieldState {
100 #[default]
101 None,
102 Instruction,
103 ReplacedInstruction,
104 Result,
105}
106
107enum NoteContext {
108 Body,
110 Footnote,
112 Endnote,
113}
114
115pub struct DocBuilder<'a> {
116 doc: &'a DocFile,
117 styles: StyleSheet,
118 fonts: FontTable,
119 lists: ListTables,
120 char_runs: Vec<CharRun>,
121 char_run_max_ends: Vec<u32>,
122 papx_bins: Vec<(u32, u32, u32)>,
123 papx_pages: HashMap<u32, Vec<fkp::PapxRun>>,
124 sections: Vec<(u32, Sep)>, section_ends: HashSet<u32>,
126 footnote_refs: Vec<u32>,
127 endnote_refs: Vec<u32>,
128 footnote_ranges: Vec<(u32, u32)>,
129 endnote_ranges: Vec<(u32, u32)>,
130 comments: Vec<super::comments::Comment>,
131 revision_authors: Vec<String>,
132 field_instructions: HashMap<u32, String>,
133 phonetic_results: HashMap<u32, String>,
134 spanning_links: Vec<super::fields::LinkRange>,
135 bookmarks: std::collections::BTreeMap<u32, String>,
136 text_boxes_by_anchor: HashMap<u32, Vec<TextBoxSpec>>,
137 images_by_anchor: HashMap<u32, Vec<super::images::FloatingImage>>,
138 warnings: Vec<String>,
139 f_facing_pages: bool,
140 default_tab: u16,
141 parts: HashMap<String, String>,
143 media: HashMap<String, Vec<u8>>,
144 media_extensions: HashSet<&'static str>,
145 image_count: usize,
146 header_count: usize,
147 footer_count: usize,
148}
149
150pub fn parse_doc(bytes: &[u8]) -> Result<OoxmlPackage, String> {
151 let doc = DocFile::open(bytes)?;
152 let mut builder = DocBuilder::new(&doc);
153 builder.build()
154}
155
156impl<'a> DocBuilder<'a> {
157 fn new(doc: &'a DocFile) -> DocBuilder<'a> {
158 let styles = doc
159 .fib
160 .table_slice(&doc.table, fcidx::STSHF)
161 .map(StyleSheet::parse)
162 .unwrap_or_else(StyleSheet::empty);
163 let fonts = doc
164 .fib
165 .table_slice(&doc.table, fcidx::STTBF_FFN)
166 .map(FontTable::parse)
167 .unwrap_or_else(FontTable::empty);
168 let lists = ListTables::parse(
169 doc.fib
170 .fc_lcb(fcidx::PLF_LST)
171 .filter(|(_, size)| *size != 0)
172 .and_then(|(offset, _)| doc.table.get(offset as usize..)),
173 doc.fib.table_slice(&doc.table, fcidx::PLF_LFO),
174 );
175
176 let field_text = super::fields::field_text(doc);
177 let mut builder = DocBuilder {
178 doc,
179 styles,
180 fonts,
181 lists,
182 char_runs: Vec::new(),
183 char_run_max_ends: Vec::new(),
184 papx_bins: Vec::new(),
185 papx_pages: HashMap::new(),
186 sections: Vec::new(),
187 section_ends: HashSet::new(),
188 footnote_refs: Vec::new(),
189 endnote_refs: Vec::new(),
190 footnote_ranges: Vec::new(),
191 endnote_ranges: Vec::new(),
192 comments: super::comments::parse(doc),
193 revision_authors: super::revisions::authors(
194 doc.fib.table_slice(&doc.table, fcidx::STTBF_RMARK).unwrap_or_default(),
195 ),
196 field_instructions: field_text.instructions,
197 phonetic_results: field_text.phonetic_results,
198 spanning_links: super::fields::spanning_links(doc),
199 bookmarks: super::fields::bookmarks(doc),
200 text_boxes_by_anchor: HashMap::new(),
201 images_by_anchor: HashMap::new(),
202 warnings: Vec::new(),
203 f_facing_pages: false,
204 default_tab: 720,
205 parts: HashMap::new(),
206 media: HashMap::new(),
207 media_extensions: HashSet::new(),
208 image_count: 0,
209 header_count: 0,
210 footer_count: 0,
211 };
212 builder.load_char_runs();
213 builder.load_papx_bins();
214 builder.load_sections();
215 builder.load_notes();
216 builder.load_dop();
217 builder.load_text_boxes();
218 for image in super::images::floating_images(doc) {
219 builder
220 .images_by_anchor
221 .entry(image.anchor.anchor_cp)
222 .or_default()
223 .push(image);
224 }
225 builder
226 }
227
228 fn load_char_runs(&mut self) {
231 let Some(plc) = self
232 .doc
233 .fib
234 .table_slice(&self.doc.table, fcidx::PLCF_BTE_CHPX)
235 else {
236 return;
237 };
238 let bins = fkp::parse_bin_table(plc);
239 let mut fkp_cache: HashMap<u32, Vec<fkp::ChpxRun>> = HashMap::new();
240
241 for piece in &self.doc.pieces.pieces {
242 let char_width = if piece.compressed { 1u32 } else { 2u32 };
243 let piece_fc_start = piece.fc;
244 let piece_fc_end = piece.fc + (piece.cp_end - piece.cp_start) * char_width;
245 let prm_grpprl = self.prm_grpprl(piece.prm);
246
247 for (bin_fc_start, bin_fc_end, page) in &bins {
248 if *bin_fc_end <= piece_fc_start || *bin_fc_start >= piece_fc_end {
249 continue;
250 }
251 let runs = fkp_cache
252 .entry(*page)
253 .or_insert_with(|| fkp::parse_chpx_fkp(&self.doc.word, *page));
254 for run in runs.iter() {
255 let fc_start = run.fc_start.max(piece_fc_start);
256 let fc_end = run.fc_end.min(piece_fc_end);
257 if fc_start >= fc_end {
258 continue;
259 }
260 let cp_start = piece.cp_start + (fc_start - piece_fc_start) / char_width;
261 let cp_end = piece.cp_start + (fc_end - piece_fc_start).div_ceil(char_width);
262 let mut grpprl = run.grpprl.clone();
263 if let Some(extra) = &prm_grpprl {
264 grpprl.extend_from_slice(extra);
265 }
266 self.char_runs.push(CharRun {
267 cp_start,
268 cp_end,
269 grpprl,
270 });
271 }
272 }
273 }
274 self.char_runs.sort_by_key(|run| run.cp_start);
275 self.char_run_max_ends = self
276 .char_runs
277 .iter()
278 .scan(0, |end, run| {
279 *end = (*end).max(run.cp_end);
280 Some(*end)
281 })
282 .collect();
283 }
284
285 fn char_run_range(&self, start: u32, end: u32) -> std::ops::Range<usize> {
286 let first = self
287 .char_run_max_ends
288 .partition_point(|max_end| *max_end <= start);
289 let last = self.char_runs.partition_point(|run| run.cp_start < end);
290 first.min(last)..last
291 }
292
293 fn prm_grpprl(&self, prm: u16) -> Option<Vec<u8>> {
294 if prm == 0 {
295 return None;
296 }
297 if prm & 1 == 1 {
298 let index = (prm >> 1) as usize;
299 return self.doc.pieces.grpprls.get(index).cloned();
300 }
301 let opcode: u16 = match (prm >> 1) & 0x7F {
302 0x05 => 0x2461,
303 0x07 => 0x2405,
304 0x08 => 0x2406,
305 0x09 => 0x2407,
306 0x0C => 0x260A,
307 0x18 => 0x2416,
308 0x19 => 0x2417,
309 0x33 => 0x2431,
310 0x41 => 0x0800,
311 0x42 => 0x0801,
312 0x43 => 0x0802,
313 0x47 => 0x0806,
314 0x4B => 0x080A,
315 0x4D => 0x2A0C,
316 0x4E => 0x0858,
317 0x55..=0x5C => 0x0835 + ((prm >> 1) & 0x7F) - 0x55,
318 0x5E => 0x2A3E,
319 0x62 => 0x2A42,
320 0x68 => 0x2A48,
321 0x73 => 0x2A53,
322 0x74 => 0x0854,
323 0x75 => 0x0855,
324 0x76 => 0x0856,
325 0x78 => 0x2640,
326 _ => return None,
327 };
328 let [low, high] = opcode.to_le_bytes();
329 Some(vec![low, high, (prm >> 8) as u8])
330 }
331
332 fn load_papx_bins(&mut self) {
333 if let Some(plc) = self
334 .doc
335 .fib
336 .table_slice(&self.doc.table, fcidx::PLCF_BTE_PAPX)
337 {
338 self.papx_bins = fkp::parse_bin_table(plc);
339 }
340 }
341
342 fn papx_for_fc(&mut self, fc: u32) -> Option<fkp::PapxRun> {
343 let page = self
344 .papx_bins
345 .iter()
346 .find(|(start, end, _)| fc >= *start && fc < *end)
347 .map(|(_, _, page)| *page)?;
348 let runs = self
349 .papx_pages
350 .entry(page)
351 .or_insert_with(|| fkp::parse_papx_fkp(&self.doc.word, page));
352 runs.iter()
353 .find(|run| fc >= run.fc_start && fc < run.fc_end)
354 .cloned()
355 }
356
357 fn resolve_huge_papx(&self, grpprl: &[u8]) -> Vec<u8> {
361 let Some(huge) = super::sprm::iterate(grpprl).find(|sprm| sprm.opcode == 0x6646) else {
362 return grpprl.to_vec();
363 };
364 let offset = huge.u32_operand() as usize;
365 let Some(cb) = self
366 .doc
367 .data
368 .get(offset..offset + 2)
369 .map(|b| u16::from_le_bytes([b[0], b[1]]) as usize)
370 else {
371 return grpprl.to_vec();
372 };
373 let Some(huge_grpprl) = self.doc.data.get(offset + 2..offset + 2 + cb) else {
374 return grpprl.to_vec();
375 };
376 let mut combined = grpprl.to_vec();
377 combined.extend_from_slice(huge_grpprl);
378 combined
379 }
380
381 fn load_sections(&mut self) {
382 let Some(plc) = self.doc.fib.table_slice(&self.doc.table, fcidx::PLCF_SED) else {
383 self.sections.push((self.doc.fib.ccp.text, Sep::default()));
384 return;
385 };
386 const SED_SIZE: usize = 12;
387 if plc.len() < 4 + SED_SIZE {
388 self.sections.push((self.doc.fib.ccp.text, Sep::default()));
389 return;
390 }
391 let count = (plc.len() - 4) / (4 + SED_SIZE);
392 let read_u32 = |offset: usize| {
393 plc.get(offset..offset + 4)
394 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
395 .unwrap_or(0)
396 };
397 for index in 0..count {
398 let cp_end = read_u32((index + 1) * 4);
399 let sed_offset = (count + 1) * 4 + index * SED_SIZE;
400 let fc_sepx = read_u32(sed_offset + 2);
401 let mut sep = Sep::default();
402 if fc_sepx != 0xFFFF_FFFF {
403 let base = fc_sepx as usize;
404 if let Some(cb) = self
405 .doc
406 .word
407 .get(base..base + 2)
408 .map(|b| u16::from_le_bytes([b[0], b[1]]) as usize)
409 {
410 if let Some(grpprl) = self.doc.word.get(base + 2..base + 2 + cb) {
411 sep.apply_sepx(grpprl);
412 }
413 }
414 }
415 self.sections.push((cp_end, sep));
416 }
417 for (cp_end, _) in &self.sections {
418 self.section_ends.insert(*cp_end);
419 }
420 }
421
422 fn load_notes(&mut self) {
423 let parse_ref_plc = |plc: Option<&[u8]>, data_size: usize| -> Vec<u32> {
425 let Some(plc) = plc else { return Vec::new() };
426 if plc.len() < 4 {
427 return Vec::new();
428 }
429 let count = (plc.len() - 4) / (4 + data_size);
430 (0..count)
431 .filter_map(|index| {
432 plc.get(index * 4..index * 4 + 4)
433 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
434 })
435 .collect()
436 };
437 let parse_txt_plc = |plc: Option<&[u8]>| -> Vec<u32> {
439 let Some(plc) = plc else { return Vec::new() };
440 plc.chunks_exact(4)
441 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
442 .collect()
443 };
444
445 self.footnote_refs = parse_ref_plc(
446 self.doc
447 .fib
448 .table_slice(&self.doc.table, fcidx::PLCFFND_REF),
449 2,
450 );
451 self.endnote_refs = parse_ref_plc(
452 self.doc
453 .fib
454 .table_slice(&self.doc.table, fcidx::PLCFEND_REF),
455 2,
456 );
457
458 let ftn_base = self.doc.fib.ccp.ftn_start();
459 let ftn_cps = parse_txt_plc(
460 self.doc
461 .fib
462 .table_slice(&self.doc.table, fcidx::PLCFFND_TXT),
463 );
464 for window in ftn_cps.windows(2).take(self.footnote_refs.len()) {
465 self.footnote_ranges
466 .push((ftn_base + window[0], ftn_base + window[1]));
467 }
468 let edn_base = self.doc.fib.ccp.edn_start();
469 let edn_cps = parse_txt_plc(
470 self.doc
471 .fib
472 .table_slice(&self.doc.table, fcidx::PLCFEND_TXT),
473 );
474 for window in edn_cps.windows(2).take(self.endnote_refs.len()) {
475 self.endnote_ranges
476 .push((edn_base + window[0], edn_base + window[1]));
477 }
478 }
479
480 fn load_dop(&mut self) {
481 if let Some(dop) = self.doc.fib.table_slice(&self.doc.table, fcidx::DOP) {
482 if let Some(&flags) = dop.first() {
483 self.f_facing_pages = flags & 0x01 != 0;
484 }
485 if let Some(tab) = dop.get(8..10).map(|b| u16::from_le_bytes([b[0], b[1]])) {
486 if (36..=2880).contains(&tab) {
487 self.default_tab = tab;
488 }
489 }
490 }
491 }
492
493 fn load_text_boxes(&mut self) {
494 for story in [TextBoxStory::Main, TextBoxStory::Header] {
495 let parsed = parse_text_boxes(self.doc, story);
496 self.warnings.extend(parsed.warnings);
497 for spec in parsed.specs {
498 self.text_boxes_by_anchor
499 .entry(spec.anchor_cp)
500 .or_default()
501 .push(spec);
502 }
503 }
504 for specs in self.text_boxes_by_anchor.values_mut() {
505 specs.sort_by_key(|spec| spec.shape_id);
506 }
507 }
508
509 fn build_paragraphs(
514 &mut self,
515 cp_start: u32,
516 cp_end: u32,
517 context: &NoteContext,
518 ) -> Vec<ParaOut> {
519 let text = self.doc.text(cp_start, cp_end);
520 let mut paragraphs = Vec::new();
521 let mut fields = Vec::new();
522 let mut para_start = cp_start;
523 for (index, &unit) in text.iter().enumerate() {
524 let cp = cp_start + index as u32;
525 let is_end = unit == 0x0D
526 || unit == 0x07
527 || (unit == 0x0C && self.section_ends.contains(&(cp + 1)));
528 if is_end {
529 paragraphs.push(self.build_paragraph(
530 para_start,
531 cp + 1,
532 &text[(para_start - cp_start) as usize..=index],
533 unit,
534 context,
535 &mut fields,
536 ));
537 para_start = cp + 1;
538 }
539 }
540 if para_start < cp_end {
541 paragraphs.push(self.build_paragraph(
542 para_start,
543 cp_end,
544 &text[(para_start - cp_start) as usize..],
545 0x0D,
546 context,
547 &mut fields,
548 ));
549 }
550 paragraphs
551 }
552
553 fn build_paragraph(
554 &mut self,
555 cp_start: u32,
556 cp_end: u32,
557 text: &[u16],
558 mark_char: u16,
559 context: &NoteContext,
560 fields: &mut Vec<FieldState>,
561 ) -> ParaOut {
562 let mark_cp = cp_end.saturating_sub(1);
564 let mut pap = Pap::default();
565 if let Some(fc) = self.doc.pieces.cp_to_fc(mark_cp) {
566 if let Some(papx) = self.papx_for_fc(fc) {
567 pap.istd = papx.istd;
568 pap.apply_papx(&self.resolve_huge_papx(&papx.grpprl));
569 }
570 }
571 if let Some(piece) = self.doc.pieces.piece_for(mark_cp) {
572 if let Some(extra) = self.prm_grpprl(piece.prm) {
573 pap.apply_papx(&extra);
574 }
575 }
576
577 let effective_itap = if pap.itap == 0 && pap.f_in_table {
578 1
579 } else {
580 pap.itap
581 };
582 let is_row_end_level = if pap.f_ttp {
583 1
584 } else if pap.f_inner_ttp {
585 effective_itap.max(2)
586 } else {
587 0
588 };
589
590 let mut runs_xml = self.build_runs(
591 cp_start,
592 mark_cp,
593 &text[..text.len().saturating_sub(1)],
594 pap.istd,
595 context,
596 fields,
597 );
598 if let Some(markers) = self.bookmarks.get(&mark_cp) {
599 runs_xml.push_str(markers);
600 }
601
602 let mark_rpr = {
604 let grpprl = self.char_runs[self.char_run_range(mark_cp, cp_end)]
605 .iter()
606 .find(|run| mark_cp >= run.cp_start && mark_cp < run.cp_end)
607 .map(|run| run.grpprl.as_slice())
608 .unwrap_or_default();
609 let char_istd = super::sprm::iterate(&grpprl)
610 .find(|sprm| sprm.opcode == 0x4A30)
611 .map(|sprm| sprm.u16_operand());
612 let base_toggles = self.styles.run_toggle_base(pap.istd, char_istd);
613 let mut chp = Chp::default();
614 chp.apply_chpx(&grpprl, &base_toggles);
615 chp.to_rpr_xml(&|istd| self.styles.style_id(istd), &|ftc| {
616 self.fonts.name(ftc)
617 })
618 };
619
620 ParaOut {
621 pap,
622 runs_xml,
623 mark_rpr,
624 mark_char,
625 effective_itap,
626 is_row_end_level,
627 cp_end,
628 sect_index: None,
629 }
630 }
631
632 fn build_runs(
635 &mut self,
636 cp_start: u32,
637 cp_end: u32,
638 text: &[u16],
639 para_istd: u16,
640 context: &NoteContext,
641 fields: &mut Vec<FieldState>,
642 ) -> String {
643 let mut xml = String::new();
644 if cp_start >= cp_end {
645 return xml;
646 }
647
648 let range = self.char_run_range(cp_start, cp_end);
650 let mut intervals: Vec<(u32, u32, Option<usize>)> = self.char_runs[range.clone()]
651 .iter()
652 .enumerate()
653 .filter(|(_, run)| run.cp_end > cp_start && run.cp_start < cp_end)
654 .map(|(index, run)| {
655 (
656 run.cp_start.max(cp_start),
657 run.cp_end.min(cp_end),
658 Some(range.start + index),
659 )
660 })
661 .collect();
662 if intervals.is_empty() {
663 intervals.push((cp_start, cp_end, None));
664 }
665 intervals.sort_by_key(|(start, _, _)| *start);
666 let mut covered = cp_start;
668 let mut complete: Vec<(u32, u32, Option<usize>)> = Vec::new();
669 for (start, end, grpprl) in intervals {
670 if start > covered {
671 complete.push((covered, start, None));
672 }
673 let clamped_start = start.max(covered);
674 if clamped_start < end {
675 complete.push((clamped_start, end, grpprl));
676 covered = end;
677 }
678 }
679 if covered < cp_end {
680 complete.push((covered, cp_end, None));
681 }
682
683 let mut linked = Vec::new();
684 for (start, end, grpprl) in complete {
685 if self.spanning_links.is_empty() {
686 linked.push((start, end, grpprl, None));
687 continue;
688 }
689 let mut boundaries = vec![start, end];
690 for link in &self.spanning_links {
691 boundaries.extend(
692 [link.start, link.end]
693 .into_iter()
694 .filter(|cp| *cp > start && *cp < end),
695 );
696 }
697 boundaries.sort_unstable();
698 boundaries.dedup();
699 for pair in boundaries.windows(2) {
700 let instruction = self
701 .spanning_links
702 .iter()
703 .find(|link| pair[0] >= link.start && pair[1] <= link.end)
704 .map(|link| link.instruction.clone());
705 linked.push((pair[0], pair[1], grpprl, instruction));
706 }
707 }
708 for (start, end, run_index, instruction) in linked {
709 let grpprl = run_index
710 .map(|index| self.char_runs[index].grpprl.as_slice())
711 .unwrap_or_default();
712 if let Some(instruction) = &instruction {
713 xml.push_str(&format!(
714 r#"<w:fldSimple w:instr="{}">"#,
715 escape_xml(instruction)
716 ));
717 }
718 let char_istd = super::sprm::iterate(&grpprl)
719 .find(|sprm| sprm.opcode == 0x4A30)
720 .map(|sprm| sprm.u16_operand());
721 let base_toggles = self.styles.run_toggle_base(para_istd, char_istd);
722 let mut chp = Chp::default();
723 chp.apply_chpx(&grpprl, &base_toggles);
724 let rpr = chp.to_rpr_xml(&|istd| self.styles.style_id(istd), &|ftc| {
725 self.fonts.name(ftc)
726 });
727 let (insertion, deletion) = super::revisions::marks(grpprl, chp.deleted);
728 if let Some(mark) = &insertion {
729 xml.push_str(&mark.open("ins", start, &self.revision_authors));
730 }
731 if let Some(mark) = deletion {
732 xml.push_str(&mark.open("del", start, &self.revision_authors));
733 let mut deleted = String::new();
734 let mut deleted_fields = fields.clone();
735 self.emit_run_content(
736 &mut deleted,
737 start,
738 &text[(start - cp_start) as usize..(end - cp_start) as usize],
739 &chp,
740 &rpr,
741 &mut deleted_fields,
742 context,
743 );
744 let deleted = deleted
745 .replace("<w:t ", "<w:delText ")
746 .replace("</w:t>", "</w:delText>");
747 xml.push_str(&deleted);
748 xml.push_str("</w:del>");
749 } else {
750 self.emit_run_content(
751 &mut xml,
752 start,
753 &text[(start - cp_start) as usize..(end - cp_start) as usize],
754 &chp,
755 &rpr,
756 fields,
757 context,
758 );
759 }
760 if insertion.is_some() {
761 xml.push_str("</w:ins>");
762 }
763 if instruction.is_some() {
764 xml.push_str("</w:fldSimple>");
765 }
766 }
767 xml
768 }
769
770 #[allow(clippy::too_many_arguments)]
771 fn emit_run_content(
772 &mut self,
773 xml: &mut String,
774 cp_start: u32,
775 text: &[u16],
776 chp: &Chp,
777 rpr: &str,
778 fields: &mut Vec<FieldState>,
779 context: &NoteContext,
780 ) {
781 let mut buffer = String::new();
782
783 let flush = |buffer: &mut String, xml: &mut String, state: FieldState, rpr: &str| {
784 if buffer.is_empty() {
785 return;
786 }
787 xml.push_str("<w:r>");
788 xml.push_str(rpr);
789 let instruction = state == FieldState::Instruction;
790 xml.push_str(if instruction {
791 r#"<w:instrText xml:space="preserve">"#
792 } else {
793 r#"<w:t xml:space="preserve">"#
794 });
795 escape_xml_into(buffer, xml);
796 xml.push_str(if instruction {
797 "</w:instrText></w:r>"
798 } else {
799 "</w:t></w:r>"
800 });
801 buffer.clear();
802 };
803 let single = |xml: &mut String, rpr: &str, inner: &str| {
804 xml.push_str("<w:r>");
805 xml.push_str(rpr);
806 xml.push_str(inner);
807 xml.push_str("</w:r>");
808 };
809
810 for (index, &unit) in text.iter().enumerate() {
811 let cp = cp_start + index as u32;
812 if let Some(markers) = self.bookmarks.get(&cp) {
813 flush(
814 &mut buffer,
815 xml,
816 fields.last().copied().unwrap_or_default(),
817 rpr,
818 );
819 xml.push_str(markers);
820 }
821 if fields.last() == Some(&FieldState::ReplacedInstruction)
822 && !matches!(unit, 0x13 | 0x14 | 0x15)
823 {
824 continue;
825 }
826 match unit {
827 0x05 => {
828 flush(
829 &mut buffer,
830 xml,
831 fields.last().copied().unwrap_or_default(),
832 rpr,
833 );
834 if let Some(index) = self
835 .comments
836 .iter()
837 .position(|comment| comment.reference_cp == cp)
838 {
839 single(
840 xml,
841 rpr,
842 &format!(r#"<w:commentReference w:id="{index}"/>"#),
843 );
844 }
845 }
846 0x09 => {
847 flush(
848 &mut buffer,
849 xml,
850 fields.last().copied().unwrap_or_default(),
851 rpr,
852 );
853 single(xml, rpr, "<w:tab/>");
854 }
855 0x0B => {
856 flush(
857 &mut buffer,
858 xml,
859 fields.last().copied().unwrap_or_default(),
860 rpr,
861 );
862 single(xml, rpr, "<w:br/>");
863 }
864 0x0C => {
865 flush(
866 &mut buffer,
867 xml,
868 fields.last().copied().unwrap_or_default(),
869 rpr,
870 );
871 single(xml, rpr, r#"<w:br w:type="page"/>"#);
872 }
873 0x0E => {
874 flush(
875 &mut buffer,
876 xml,
877 fields.last().copied().unwrap_or_default(),
878 rpr,
879 );
880 single(xml, rpr, r#"<w:br w:type="column"/>"#);
881 }
882 0x08 => {
883 flush(
884 &mut buffer,
885 xml,
886 fields.last().copied().unwrap_or_default(),
887 rpr,
888 );
889 if let Some(images) = self.images_by_anchor.remove(&cp) {
890 for image in images {
891 let drawing = self.register_floating_image(image);
892 single(xml, rpr, &drawing);
893 }
894 }
895 if let Some(specs) = self.text_boxes_by_anchor.remove(&cp) {
896 for spec in specs {
897 let shape = self.build_text_box_shape(&spec);
898 single(xml, rpr, &shape);
899 }
900 }
901 }
902 0x13 => {
903 flush(
904 &mut buffer,
905 xml,
906 fields.last().copied().unwrap_or_default(),
907 rpr,
908 );
909 single(xml, rpr, r#"<w:fldChar w:fldCharType="begin"/>"#);
910 if let Some(instruction) = self.field_instructions.get(&cp) {
911 single(
912 xml,
913 rpr,
914 &format!(
915 r#"<w:instrText xml:space="preserve">{}</w:instrText>"#,
916 escape_xml(instruction)
917 ),
918 );
919 fields.push(FieldState::ReplacedInstruction);
920 } else {
921 fields.push(FieldState::Instruction);
922 }
923 }
924 0x14 => {
925 flush(
926 &mut buffer,
927 xml,
928 fields.last().copied().unwrap_or_default(),
929 rpr,
930 );
931 single(xml, rpr, r#"<w:fldChar w:fldCharType="separate"/>"#);
932 if let Some(state) = fields.last_mut() {
933 *state = FieldState::Result;
934 }
935 }
936 0x15 => {
937 flush(
938 &mut buffer,
939 xml,
940 fields.last().copied().unwrap_or_default(),
941 rpr,
942 );
943 if fields.last() == Some(&FieldState::Instruction) {
944 if let Some(text) = self.phonetic_results.get(&cp) {
945 single(xml, rpr, r#"<w:fldChar w:fldCharType="separate"/>"#);
946 single(
947 xml,
948 rpr,
949 &format!(r#"<w:t xml:space="preserve">{}</w:t>"#, escape_xml(text)),
950 );
951 }
952 }
953 single(xml, rpr, r#"<w:fldChar w:fldCharType="end"/>"#);
954 fields.pop();
955 }
956 0x1E => buffer.push('\u{2011}'),
957 0x1F => buffer.push('\u{00AD}'),
958 0x01 if chp.f_spec => {
959 flush(
960 &mut buffer,
961 xml,
962 fields.last().copied().unwrap_or_default(),
963 rpr,
964 );
965 if !chp.f_obj && !chp.f_ole2 && !chp.f_data {
966 if let Some(location) = chp.pic_location {
967 if let Some(image) = extract_image(&self.doc.data, location) {
968 let drawing = self.register_image(image);
969 single(xml, rpr, &drawing);
970 }
971 }
972 }
973 }
974 0x02 if chp.f_spec => {
975 flush(
976 &mut buffer,
977 xml,
978 fields.last().copied().unwrap_or_default(),
979 rpr,
980 );
981 match context {
982 NoteContext::Body => {
983 if let Some(position) =
984 self.footnote_refs.iter().position(|&ref_cp| ref_cp == cp)
985 {
986 single(
987 xml,
988 rpr,
989 &format!(r#"<w:footnoteReference w:id="{}"/>"#, position + 1),
990 );
991 } else if let Some(position) =
992 self.endnote_refs.iter().position(|&ref_cp| ref_cp == cp)
993 {
994 single(
995 xml,
996 rpr,
997 &format!(r#"<w:endnoteReference w:id="{}"/>"#, position + 1),
998 );
999 }
1000 }
1001 NoteContext::Footnote => single(xml, rpr, "<w:footnoteRef/>"),
1002 NoteContext::Endnote => single(xml, rpr, "<w:endnoteRef/>"),
1003 }
1004 }
1005 0x28 if chp.f_spec => {
1006 flush(
1007 &mut buffer,
1008 xml,
1009 fields.last().copied().unwrap_or_default(),
1010 rpr,
1011 );
1012 if let Some((ftc, xchar)) = chp.symbol {
1013 let font = self.fonts.name(ftc).unwrap_or_else(|| "Symbol".to_string());
1014 single(
1015 xml,
1016 rpr,
1017 &format!(
1018 r#"<w:sym w:font="{}" w:char="{xchar:04X}"/>"#,
1019 escape_xml(&font)
1020 ),
1021 );
1022 }
1023 }
1024 0x00..=0x1F => {} 0xD800..=0xDBFF
1026 if text
1027 .get(index + 1)
1028 .is_some_and(|u| (0xDC00..=0xDFFF).contains(u)) =>
1029 {
1030 buffer.push_str(&String::from_utf16_lossy(&text[index..index + 2]));
1031 }
1032 0xDC00..=0xDFFF if index > 0 && (0xD800..=0xDBFF).contains(&text[index - 1]) => {}
1033 _ => buffer.push_str(&String::from_utf16_lossy(&[unit])),
1034 }
1035 }
1036 flush(
1037 &mut buffer,
1038 xml,
1039 fields.last().copied().unwrap_or_default(),
1040 rpr,
1041 );
1042 }
1043
1044 fn build_text_box_shape(&mut self, spec: &TextBoxSpec) -> String {
1045 let paragraphs =
1046 self.build_paragraphs(spec.text_cp_start, spec.text_cp_end, &NoteContext::Body);
1047 let body = self.assemble_blocks(¶graphs);
1048 let horizontal_relative = match spec.horizontal_relative {
1049 0 => "margin",
1050 1 => "page",
1051 2 => "column",
1052 _ => "page",
1053 };
1054 let vertical_relative = match spec.vertical_relative {
1055 0 => "margin",
1056 1 => "page",
1057 2 => "paragraph",
1058 _ => "page",
1059 };
1060 let wrap_type = match spec.wrap {
1061 0 | 2 => "square",
1062 1 => "topAndBottom",
1063 3 => "none",
1064 4 => "tight",
1065 5 => "through",
1066 _ => "square",
1067 };
1068 let wrap_side = match spec.wrap_side {
1069 1 => "left",
1070 2 => "right",
1071 3 => "largest",
1072 _ => "both",
1073 };
1074 let z_index = if spec.wrap == 3 && spec.behind_text {
1075 -1
1076 } else {
1077 0
1078 };
1079 let left = format_twips_as_points(i64::from(spec.left_twips));
1080 let top = format_twips_as_points(i64::from(spec.top_twips));
1081 let width = format_twips_as_points(i64::from(spec.width_twips));
1082 let height = format_twips_as_points(i64::from(spec.height_twips));
1083 format!(
1084 r##"<w:pict xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word"><v:shape id="_x0000_s{shape_id}" type="#_x0000_t202" filled="f" stroked="f" style="position:absolute;left:{left}pt;top:{top}pt;width:{width}pt;height:{height}pt;mso-position-horizontal:absolute;mso-position-horizontal-relative:{horizontal_relative};mso-position-vertical:absolute;mso-position-vertical-relative:{vertical_relative};mso-wrap-style:{wrap_type};z-index:{z_index}"><w10:wrap type="{wrap_type}" side="{wrap_side}"/><v:textbox inset="0pt,0pt,0pt,0pt"><w:txbxContent>{body}</w:txbxContent></v:textbox></v:shape></w:pict>"##,
1085 shape_id = spec.shape_id,
1086 )
1087 }
1088
1089 fn register_image(&mut self, image: InlineImage) -> String {
1090 self.image_count += 1;
1091 let index = self.image_count;
1092 let file_name = format!("media/image{index}.{}", image.extension);
1093 let part_name = format!("word/{file_name}");
1094 self.media.insert(part_name, image.bytes);
1095 self.media_extensions.insert(image.extension);
1096
1097 let placeholder = format!("__IMG_REL_{index}__");
1100 format!(
1101 r#"<w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="{cx}" cy="{cy}"/><wp:docPr id="{index}" name="Picture {index}"/><a:graphic xmlns:a="{A_NS}"><a:graphicData uri="{PIC_NS}"><pic:pic xmlns:pic="{PIC_NS}"><pic:nvPicPr><pic:cNvPr id="{index}" name="Picture {index}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="{placeholder}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="{cx}" cy="{cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing>"#,
1102 cx = image.width_emu.max(1),
1103 cy = image.height_emu.max(1),
1104 )
1105 }
1106
1107 fn register_floating_image(&mut self, image: super::images::FloatingImage) -> String {
1108 let anchor = image.anchor;
1109 let mut drawing = self.register_image(image.image);
1110 if image.tiled {
1111 drawing = drawing.replace("<a:stretch><a:fillRect/></a:stretch>", "<a:tile/>");
1112 }
1113 let horizontal = match anchor.horizontal_relative {
1114 0 => "margin",
1115 1 => "page",
1116 2 => "column",
1117 _ => "character",
1118 };
1119 let vertical = match anchor.vertical_relative {
1120 0 => "margin",
1121 1 => "page",
1122 2 => "paragraph",
1123 _ => "line",
1124 };
1125 let position = format!(
1126 r#"<wp:anchor distT="0" distB="0" distL="0" distR="0" simplePos="0" relativeHeight="{}" behindDoc="{}" locked="0" layoutInCell="1" allowOverlap="1"><wp:simplePos x="0" y="0"/><wp:positionH relativeFrom="{horizontal}"><wp:posOffset>{}</wp:posOffset></wp:positionH><wp:positionV relativeFrom="{vertical}"><wp:posOffset>{}</wp:posOffset></wp:positionV>"#,
1127 anchor.shape_id,
1128 u8::from(anchor.behind_text),
1129 i64::from(anchor.left) * 635,
1130 i64::from(anchor.top) * 635
1131 );
1132 let side = match anchor.wrap_side {
1133 1 => "left",
1134 2 => "right",
1135 3 => "largest",
1136 _ => "bothSides",
1137 };
1138 let wrap = match anchor.wrap {
1139 1 => "<wp:wrapTopAndBottom/>".to_string(),
1140 3 => "<wp:wrapNone/>".to_string(),
1141 _ => format!(r#"<wp:wrapSquare wrapText="{side}"/>"#),
1142 };
1143 drawing
1144 .replace(
1145 r#"<wp:inline distT="0" distB="0" distL="0" distR="0">"#,
1146 &position,
1147 )
1148 .replace("<wp:docPr", &format!("{wrap}<wp:docPr"))
1149 .replace("</wp:inline>", "</wp:anchor>")
1150 }
1151
1152 fn finalize_story_images(&mut self, xml: Option<String>, part: &str) -> Option<String> {
1153 let xml = xml?;
1154 let mut rels = RelSet::default();
1155 let xml = self.finalize_image_rels(xml, &mut rels);
1156 if !rels.rels.is_empty() {
1157 self.parts
1158 .insert(format!("word/_rels/{part}.rels"), rels.to_xml());
1159 }
1160 Some(xml)
1161 }
1162
1163 fn finalize_image_rels(&self, xml: String, rels: &mut RelSet) -> String {
1166 let mut result = xml;
1167 for index in 1..=self.image_count {
1168 let placeholder = format!("__IMG_REL_{index}__");
1169 if result.contains(&placeholder) {
1170 let target = self
1172 .media
1173 .keys()
1174 .find(|name| name.starts_with(&format!("word/media/image{index}.")))
1175 .map(|name| name.trim_start_matches("word/").to_string())
1176 .unwrap_or_else(|| format!("media/image{index}.png"));
1177 let rel_id = rels.add(REL_TYPE_IMAGE, &target);
1178 result = result.replace(&placeholder, &rel_id);
1179 }
1180 }
1181 result
1182 }
1183
1184 fn assemble_blocks(&self, paragraphs: &[ParaOut]) -> String {
1187 let mut xml = String::new();
1188 let mut index = 0;
1189 while index < paragraphs.len() {
1190 let para = ¶graphs[index];
1191 if para.effective_itap >= 1 {
1192 let (table_xml, consumed, pending_sections) =
1193 self.assemble_table(¶graphs[index..], 1);
1194 xml.push_str(&table_xml);
1195 for sect_index in pending_sections {
1196 xml.push_str(&format!(
1197 "<w:p><w:pPr>{}</w:pPr></w:p>",
1198 self.sect_pr_xml(sect_index)
1199 ));
1200 }
1201 index += consumed;
1202 } else {
1203 self.append_paragraph_xml(para, &mut xml);
1204 index += 1;
1205 }
1206 }
1207 xml
1208 }
1209
1210 fn append_paragraph_xml(&self, para: &ParaOut, xml: &mut String) {
1211 let mut ppr_inner = para
1212 .pap
1213 .to_ppr_inner_xml(&|istd| self.styles.style_id(istd));
1214 ppr_inner.push_str(¶.mark_rpr);
1215 if let Some(sect_index) = para.sect_index {
1216 ppr_inner.push_str(&self.sect_pr_xml(sect_index));
1217 }
1218 xml.push_str("<w:p>");
1219 if !ppr_inner.is_empty() {
1220 xml.push_str("<w:pPr>");
1221 xml.push_str(&ppr_inner);
1222 xml.push_str("</w:pPr>");
1223 }
1224 xml.push_str(¶.runs_xml);
1225 xml.push_str("</w:p>");
1226 }
1227
1228 fn assemble_table(&self, paragraphs: &[ParaOut], level: u32) -> (String, usize, Vec<usize>) {
1231 struct Row<'p> {
1232 cells: Vec<Vec<&'p ParaOut>>,
1233 tap: Tap,
1234 }
1235 let mut rows: Vec<Row<'_>> = Vec::new();
1236 let mut pending_sections = Vec::new();
1237 let mut index = 0;
1238
1239 'rows: while index < paragraphs.len() && paragraphs[index].effective_itap >= level {
1240 let mut cells: Vec<Vec<&ParaOut>> = Vec::new();
1241 let mut current: Vec<&ParaOut> = Vec::new();
1242 loop {
1243 if index >= paragraphs.len() {
1244 if !current.is_empty() {
1245 cells.push(std::mem::take(&mut current));
1246 }
1247 if !cells.is_empty() {
1248 rows.push(Row {
1249 cells,
1250 tap: Tap::default(),
1251 });
1252 }
1253 break 'rows;
1254 }
1255 let para = ¶graphs[index];
1256 if para.effective_itap < level {
1257 if !current.is_empty() {
1258 cells.push(std::mem::take(&mut current));
1259 }
1260 if !cells.is_empty() {
1261 rows.push(Row {
1262 cells,
1263 tap: Tap::default(),
1264 });
1265 }
1266 break 'rows;
1267 }
1268 if let Some(sect_index) = para.sect_index {
1269 pending_sections.push(sect_index);
1270 }
1271 if para.is_row_end_level != 0 && para.is_row_end_level == level {
1272 let tap = Tap::from_grpprl(¶.pap.table_grpprl);
1273 if !current.is_empty() {
1274 cells.push(std::mem::take(&mut current));
1275 }
1276 index += 1;
1277 rows.push(Row { cells, tap });
1278 break;
1279 }
1280 current.push(para);
1281 index += 1;
1282 if para.effective_itap == level
1283 && (para.mark_char == 0x07 || para.pap.f_inner_table_cell)
1284 && para.is_row_end_level == 0
1285 {
1286 cells.push(std::mem::take(&mut current));
1287 }
1288 }
1289 }
1290
1291 if rows.is_empty() {
1292 return (String::new(), 1.max(index), pending_sections);
1294 }
1295
1296 let mut boundaries: Vec<i32> = Vec::new();
1298 for row in &rows {
1299 for cell in &row.tap.cells {
1300 boundaries.push(cell.boundary_left);
1301 boundaries.push(cell.boundary_right);
1302 }
1303 }
1304 boundaries.sort_unstable();
1305 boundaries.dedup();
1306
1307 let first_tap = &rows[0].tap;
1308 let mut xml = String::from("<w:tbl>");
1309 xml.push_str("<w:tblPr>");
1310 let jc = match first_tap.jc {
1311 1 => Some("center"),
1312 2 => Some("right"),
1313 _ => None,
1314 };
1315 xml.push_str(r#"<w:tblW w:w="0" w:type="auto"/>"#);
1316 if let Some(jc) = jc {
1317 xml.push_str(&format!(r#"<w:jc w:val="{jc}"/>"#));
1318 }
1319 let ind = first_tap.row_left_edge() + first_tap.dxa_gap_half as i32;
1320 if ind != 0 {
1321 xml.push_str(&format!(r#"<w:tblInd w:w="{ind}" w:type="dxa"/>"#));
1322 }
1323 let border_tags = [
1324 "w:top",
1325 "w:left",
1326 "w:bottom",
1327 "w:right",
1328 "w:insideH",
1329 "w:insideV",
1330 ];
1331 if first_tap.table_borders.iter().any(Option::is_some) {
1332 xml.push_str("<w:tblBorders>");
1333 for (tag, border) in border_tags.iter().zip(first_tap.table_borders.iter()) {
1334 if let Some(border) = border {
1335 xml.push_str(&border.to_xml(tag));
1336 }
1337 }
1338 xml.push_str("</w:tblBorders>");
1339 }
1340 let table_cell_padding = first_tap.default_cell_padding_twips();
1341 let [top, left, bottom, right] = table_cell_padding;
1342 xml.push_str(&format!(
1343 r#"<w:tblCellMar><w:top w:w="{top}" w:type="dxa"/><w:left w:w="{left}" w:type="dxa"/><w:bottom w:w="{bottom}" w:type="dxa"/><w:right w:w="{right}" w:type="dxa"/></w:tblCellMar>"#
1344 ));
1345 xml.push_str(r#"<w:tblLayout w:type="fixed"/>"#);
1346 xml.push_str("</w:tblPr>");
1347
1348 if boundaries.len() >= 2 {
1349 xml.push_str("<w:tblGrid>");
1350 for window in boundaries.windows(2) {
1351 xml.push_str(&format!(r#"<w:gridCol w:w="{}"/>"#, window[1] - window[0]));
1352 }
1353 xml.push_str("</w:tblGrid>");
1354 }
1355
1356 for row in &rows {
1357 xml.push_str("<w:tr>");
1358 let mut tr_pr = String::new();
1359 if row.tap.dya_row_height != 0 {
1360 let (value, rule) = if row.tap.dya_row_height < 0 {
1361 (-(row.tap.dya_row_height as i32), "exact")
1362 } else {
1363 (row.tap.dya_row_height as i32, "atLeast")
1364 };
1365 tr_pr.push_str(&format!(
1366 r#"<w:trHeight w:val="{value}" w:hRule="{rule}"/>"#
1367 ));
1368 }
1369 if row.tap.f_cant_split {
1370 tr_pr.push_str("<w:cantSplit/>");
1371 }
1372 if row.tap.f_table_header {
1373 tr_pr.push_str("<w:tblHeader/>");
1374 }
1375 if !tr_pr.is_empty() {
1376 xml.push_str(&format!("<w:trPr>{tr_pr}</w:trPr>"));
1377 }
1378
1379 let cell_count = row.tap.cells.len().max(row.cells.len());
1380 let mut cell_index = 0usize;
1381 while cell_index < cell_count {
1382 let tc = row.tap.cells.get(cell_index);
1383 if tc
1385 .map(|tc| tc.f_merged && !tc.f_first_merged)
1386 .unwrap_or(false)
1387 {
1388 cell_index += 1;
1389 continue;
1390 }
1391 let mut span_right = tc.map(|tc| tc.boundary_right).unwrap_or(0);
1392 let mut merged_end = cell_index + 1;
1393 if tc.map(|tc| tc.f_first_merged).unwrap_or(false) {
1394 while merged_end < row.tap.cells.len() && row.tap.cells[merged_end].f_merged {
1395 span_right = row.tap.cells[merged_end].boundary_right;
1396 merged_end += 1;
1397 }
1398 }
1399
1400 xml.push_str("<w:tc>");
1401 let mut tc_pr = String::new();
1402 if let Some(tc) = tc {
1403 let width = span_right - tc.boundary_left;
1404 tc_pr.push_str(&format!(r#"<w:tcW w:w="{}" w:type="dxa"/>"#, width.max(0)));
1405 let grid_span = boundaries
1406 .iter()
1407 .filter(|b| **b > tc.boundary_left && **b < span_right)
1408 .count()
1409 + 1;
1410 if grid_span > 1 {
1411 xml_push_grid_span(&mut tc_pr, grid_span);
1412 }
1413 if tc.f_vert_restart {
1414 tc_pr.push_str(r#"<w:vMerge w:val="restart"/>"#);
1415 } else if tc.f_vert_merge {
1416 tc_pr.push_str("<w:vMerge/>");
1417 }
1418 let cell_borders: Vec<(&str, &Option<super::fmt::Border>)> = vec![
1419 ("w:top", &tc.brc_top),
1420 ("w:left", &tc.brc_left),
1421 ("w:bottom", &tc.brc_bottom),
1422 ("w:right", &tc.brc_right),
1423 ];
1424 if cell_borders.iter().any(|(_, border)| border.is_some()) {
1425 tc_pr.push_str("<w:tcBorders>");
1426 for (tag, border) in cell_borders {
1427 if let Some(border) = border {
1428 tc_pr.push_str(&border.to_xml(tag));
1429 }
1430 }
1431 tc_pr.push_str("</w:tcBorders>");
1432 }
1433 if let Some(shd) = &tc.shd {
1434 if shd.is_visible() {
1435 tc_pr.push_str(&shd.to_xml());
1436 }
1437 }
1438 }
1439 let cell_padding = row.tap.cell_padding_twips(cell_index);
1440 if cell_padding != table_cell_padding {
1441 let [top, left, bottom, right] = cell_padding;
1442 tc_pr.push_str(&format!(
1443 r#"<w:tcMar><w:top w:w="{top}" w:type="dxa"/><w:left w:w="{left}" w:type="dxa"/><w:bottom w:w="{bottom}" w:type="dxa"/><w:right w:w="{right}" w:type="dxa"/></w:tcMar>"#
1444 ));
1445 }
1446 if let Some(tc) = tc {
1447 if tc.f_vertical {
1448 tc_pr.push_str(&format!(
1449 r#"<w:textDirection w:val="{}"/>"#,
1450 if tc.f_backward { "btLr" } else { "tbRl" }
1451 ));
1452 }
1453 match tc.vert_align {
1454 1 => tc_pr.push_str(r#"<w:vAlign w:val="center"/>"#),
1455 2 => tc_pr.push_str(r#"<w:vAlign w:val="bottom"/>"#),
1456 _ => {}
1457 }
1458 }
1459 if !tc_pr.is_empty() {
1460 xml.push_str(&format!("<w:tcPr>{tc_pr}</w:tcPr>"));
1461 }
1462
1463 let content: String = match row.cells.get(cell_index) {
1464 Some(cell_paras) if !cell_paras.is_empty() => {
1465 let owned: Vec<&ParaOut> = cell_paras.to_vec();
1466 self.assemble_cell(&owned)
1467 }
1468 _ => "<w:p/>".to_string(),
1469 };
1470 xml.push_str(&content);
1471 xml.push_str("</w:tc>");
1472 cell_index = merged_end.max(cell_index + 1);
1473 }
1474 xml.push_str("</w:tr>");
1475 }
1476 xml.push_str("</w:tbl>");
1477 (xml, index, pending_sections)
1478 }
1479
1480 fn assemble_cell(&self, cell_paras: &[&ParaOut]) -> String {
1481 let mut xml = String::new();
1482 let mut index = 0;
1483 while index < cell_paras.len() {
1484 let para = cell_paras[index];
1485 let level = para.effective_itap;
1486 if level >= 2 {
1487 let start = index;
1490 while index < cell_paras.len() && cell_paras[index].effective_itap >= 2 {
1491 index += 1;
1492 }
1493 let owned: Vec<ParaOut> = cell_paras[start..index]
1494 .iter()
1495 .map(|para| ParaOut {
1496 pap: para.pap.clone(),
1497 runs_xml: para.runs_xml.clone(),
1498 mark_rpr: para.mark_rpr.clone(),
1499 mark_char: para.mark_char,
1500 effective_itap: para.effective_itap - 1,
1501 is_row_end_level: para.is_row_end_level.saturating_sub(1),
1502 cp_end: para.cp_end,
1503 sect_index: para.sect_index,
1504 })
1505 .collect();
1506 let (table_xml, _, _) = self.assemble_table(&owned, 1);
1507 xml.push_str(&table_xml);
1508 } else {
1509 self.append_paragraph_xml(para, &mut xml);
1510 index += 1;
1511 }
1512 }
1513 if xml.is_empty() {
1514 xml.push_str("<w:p/>");
1515 }
1516 xml
1517 }
1518
1519 fn sect_pr_xml(&self, sect_index: usize) -> String {
1522 let sep = self
1523 .sections
1524 .get(sect_index)
1525 .map(|(_, sep)| sep.clone())
1526 .unwrap_or_default();
1527 let mut inner = String::new();
1528 inner.push_str(
1530 self.parts
1531 .get(&format!("__sect_refs_{sect_index}"))
1532 .map(String::as_str)
1533 .unwrap_or(""),
1534 );
1535 inner.push_str(&sep.to_sect_pr_inner_xml());
1536 format!("<w:sectPr>{inner}</w:sectPr>")
1537 }
1538
1539 fn build_headers_footers(&mut self) -> Vec<(String, String)> {
1542 let mut rels_to_add: Vec<(String, String)> = Vec::new();
1543 let Some(plc) = self.doc.fib.table_slice(&self.doc.table, fcidx::PLCF_HDD) else {
1544 return rels_to_add;
1545 };
1546 if self.doc.fib.ccp.hdd == 0 {
1547 return rels_to_add;
1548 }
1549 let cps: Vec<u32> = plc
1550 .chunks_exact(4)
1551 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
1552 .collect();
1553 if cps.len() < 2 {
1554 return rels_to_add;
1555 }
1556 let hdd_base = self.doc.fib.ccp.hdd_start();
1557 let story_count = cps.len() - 1;
1558
1559 const STORY_KINDS: [(usize, &str, bool); 6] = [
1562 (0, "even", true),
1563 (1, "default", true),
1564 (2, "even", false),
1565 (3, "default", false),
1566 (4, "first", true),
1567 (5, "first", false),
1568 ];
1569
1570 let mut previous_refs: [Option<(String, String, bool)>; 6] = Default::default();
1572
1573 for sect_index in 0..self.sections.len() {
1574 let mut refs_xml = String::new();
1575 for (kind_index, ref_type, is_header) in STORY_KINDS {
1576 let story_index = 6 + sect_index * 6 + kind_index;
1577 let story_present = story_index + 1 < story_count + 1
1578 && story_index + 1 < cps.len()
1579 && cps[story_index + 1] > cps[story_index];
1580
1581 let entry: Option<(String, String, bool)> = if story_present {
1582 let cp_start = hdd_base + cps[story_index];
1583 let cp_end = hdd_base + cps[story_index + 1];
1584 let part_name = if is_header {
1585 self.header_count += 1;
1586 format!("word/header{}.xml", self.header_count)
1587 } else {
1588 self.footer_count += 1;
1589 format!("word/footer{}.xml", self.footer_count)
1590 };
1591 let paragraphs = self.build_paragraphs(cp_start, cp_end, &NoteContext::Body);
1592 let body = self.assemble_blocks(¶graphs);
1593 let root_tag = if is_header { "w:hdr" } else { "w:ftr" };
1594 let mut part_rels = RelSet::default();
1595 let body = self.finalize_image_rels(body, &mut part_rels);
1596 let part_xml = format!(
1597 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><{root_tag} xmlns:w="{W_NS}" xmlns:r="{R_NS}" xmlns:wp="{WP_NS}">{body}</{root_tag}>"#
1598 );
1599 self.parts.insert(part_name.clone(), part_xml);
1600 if !part_rels.rels.is_empty() {
1601 let rel_part =
1602 format!("word/_rels/{}.rels", part_name.trim_start_matches("word/"));
1603 self.parts.insert(rel_part, part_rels.to_xml());
1604 }
1605 Some((part_name, ref_type.to_string(), is_header))
1606 } else {
1607 previous_refs[kind_index].clone()
1608 };
1609
1610 if let Some((part_name, ref_type, is_header)) = &entry {
1611 let target = part_name.trim_start_matches("word/");
1612 let rel_id = format!("rIdHdrFtr{}_{}", sect_index, kind_index);
1613 rels_to_add.push((
1614 rel_id.clone(),
1615 format!(
1616 r#"<Relationship Id="{rel_id}" Type="{}" Target="{target}"/>"#,
1617 if *is_header {
1618 REL_TYPE_HEADER
1619 } else {
1620 REL_TYPE_FOOTER
1621 }
1622 ),
1623 ));
1624 let tag = if *is_header {
1625 "w:headerReference"
1626 } else {
1627 "w:footerReference"
1628 };
1629 refs_xml.push_str(&format!(r#"<{tag} w:type="{ref_type}" r:id="{rel_id}"/>"#));
1630 }
1631 previous_refs[kind_index] = entry;
1632 }
1633 self.parts
1634 .insert(format!("__sect_refs_{sect_index}"), refs_xml);
1635 }
1636 rels_to_add
1637 }
1638
1639 fn build_comments_part(&mut self) -> Option<String> {
1642 if self.comments.is_empty() {
1643 return None;
1644 }
1645 let mut xml =
1646 format!(r#"<w:comments xmlns:w="{W_NS}" xmlns:r="{R_NS}" xmlns:wp="{WP_NS}">"#);
1647 for index in 0..self.comments.len() {
1648 let comment = &self.comments[index];
1649 let mut attrs = format!(
1650 r#"w:id="{index}" w:author="{}" w:initials="{}""#,
1651 escape_xml(&comment.author),
1652 escape_xml(&comment.initials)
1653 );
1654 if let Some(date) = &comment.date {
1655 attrs.push_str(&format!(r#" w:date="{date}""#));
1656 }
1657 let (start, end) = (comment.text_start, comment.text_end);
1658 let paragraphs = self.build_paragraphs(start, end, &NoteContext::Body);
1659 let body = self.assemble_blocks(¶graphs);
1660 xml.push_str(&format!("<w:comment {attrs}>{body}</w:comment>"));
1661 }
1662 xml.push_str("</w:comments>");
1663 Some(xml)
1664 }
1665
1666 fn build_notes_part(
1667 &mut self,
1668 ranges: &[(u32, u32)],
1669 root: &str,
1670 note_tag: &str,
1671 ref_tag: &str,
1672 context: NoteContext,
1673 ) -> Option<String> {
1674 if ranges.is_empty() {
1675 return None;
1676 }
1677 let mut xml = format!(
1678 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><{root} xmlns:w="{W_NS}" xmlns:r="{R_NS}" xmlns:wp="{WP_NS}">"#
1679 );
1680 xml.push_str(&format!(
1681 r#"<{note_tag} w:type="separator" w:id="-1"><w:p><w:r><w:separator/></w:r></w:p></{note_tag}><{note_tag} w:type="continuationSeparator" w:id="0"><w:p><w:r><w:continuationSeparator/></w:r></w:p></{note_tag}>"#
1682 ));
1683 let owned_ranges = ranges.to_vec();
1684 for (index, (cp_start, cp_end)) in owned_ranges.into_iter().enumerate() {
1685 let paragraphs = self.build_paragraphs(cp_start, cp_end, &context);
1686 let body = self.assemble_blocks(¶graphs);
1687 xml.push_str(&format!(
1688 r#"<{note_tag} w:id="{}">{body}</{note_tag}>"#,
1689 index + 1
1690 ));
1691 }
1692 let _ = ref_tag;
1693 xml.push_str(&format!("</{root}>"));
1694 Some(xml)
1695 }
1696
1697 fn build(&mut self) -> Result<OoxmlPackage, String> {
1700 let ccp_text = self.doc.fib.ccp.text;
1701
1702 let header_rels = self.build_headers_footers();
1704
1705 let mut paragraphs = self.build_paragraphs(0, ccp_text, &NoteContext::Body);
1707 let section_count = self.sections.len();
1708 for para in paragraphs.iter_mut() {
1709 if let Some(position) = self
1710 .sections
1711 .iter()
1712 .position(|(cp_end, _)| *cp_end == para.cp_end)
1713 {
1714 if position + 1 < section_count {
1715 para.sect_index = Some(position);
1716 }
1717 }
1718 }
1719 let body_blocks = self.assemble_blocks(¶graphs);
1720 let final_sect = self.sect_pr_xml(section_count.saturating_sub(1));
1721
1722 let mut document_rels = RelSet::default();
1723 let body_blocks = self.finalize_image_rels(body_blocks, &mut document_rels);
1724
1725 let document_xml = format!(
1726 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="{W_NS}" xmlns:r="{R_NS}" xmlns:wp="{WP_NS}" xmlns:a="{A_NS}" xmlns:pic="{PIC_NS}"><w:body>{body_blocks}{final_sect}</w:body></w:document>"#
1727 );
1728
1729 let footnote_ranges = self.footnote_ranges.clone();
1731 let footnotes_xml = self.build_notes_part(
1732 &footnote_ranges,
1733 "w:footnotes",
1734 "w:footnote",
1735 "w:footnoteRef",
1736 NoteContext::Footnote,
1737 );
1738 let endnote_ranges = self.endnote_ranges.clone();
1739 let endnotes_xml = self.build_notes_part(
1740 &endnote_ranges,
1741 "w:endnotes",
1742 "w:endnote",
1743 "w:endnoteRef",
1744 NoteContext::Endnote,
1745 );
1746 let comments_xml = self.build_comments_part();
1747 let footnotes_xml = self.finalize_story_images(footnotes_xml, "footnotes.xml");
1748 let endnotes_xml = self.finalize_story_images(endnotes_xml, "endnotes.xml");
1749 let comments_xml = self.finalize_story_images(comments_xml, "comments.xml");
1750
1751 document_rels.add(REL_TYPE_STYLES, "styles.xml");
1753 document_rels.add(REL_TYPE_SETTINGS, "settings.xml");
1754 if comments_xml.is_some() {
1755 document_rels.add(
1756 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments",
1757 "comments.xml",
1758 );
1759 }
1760 let numbering_xml = self.lists.to_numbering_xml(&self.fonts);
1761 if numbering_xml.is_some() {
1762 document_rels.add(REL_TYPE_NUMBERING, "numbering.xml");
1763 }
1764 if footnotes_xml.is_some() {
1765 document_rels.add(REL_TYPE_FOOTNOTES, "footnotes.xml");
1766 }
1767 if endnotes_xml.is_some() {
1768 document_rels.add(REL_TYPE_ENDNOTES, "endnotes.xml");
1769 }
1770 let mut document_rels_xml = document_rels.to_xml();
1771 if !header_rels.is_empty() {
1773 let mut spliced = String::new();
1774 for (_, rel_xml) in &header_rels {
1775 spliced.push_str(rel_xml);
1776 }
1777 document_rels_xml = document_rels_xml
1778 .replace("</Relationships>", &format!("{spliced}</Relationships>"));
1779 }
1780
1781 let settings_xml = format!(
1782 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:settings xmlns:w="{W_NS}">{}<w:defaultTabStop w:val="{}"/></w:settings>"#,
1783 if self.f_facing_pages {
1784 "<w:evenAndOddHeaders/>"
1785 } else {
1786 ""
1787 },
1788 self.default_tab
1789 );
1790
1791 let styles_xml = self.styles.to_styles_xml(&self.fonts, 20);
1792
1793 let mut content_types = String::from(
1795 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>"#,
1796 );
1797 for extension in &self.media_extensions {
1798 let mime = match *extension {
1799 "png" => "image/png",
1800 "jpeg" => "image/jpeg",
1801 "bmp" => "image/bmp",
1802 "tiff" => "image/tiff",
1803 "emf" => "image/x-emf",
1804 "wmf" => "image/x-wmf",
1805 "pict" => "image/x-pict",
1806 _ => continue,
1807 };
1808 content_types.push_str(&format!(
1809 r#"<Default Extension="{extension}" ContentType="{mime}"/>"#
1810 ));
1811 }
1812 let mut add_override = |part: &str, content_type: &str| {
1813 content_types.push_str(&format!(
1814 r#"<Override PartName="/{part}" ContentType="{content_type}"/>"#
1815 ));
1816 };
1817 add_override(
1818 "word/document.xml",
1819 "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
1820 );
1821 add_override(
1822 "word/styles.xml",
1823 "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
1824 );
1825 add_override(
1826 "word/settings.xml",
1827 "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
1828 );
1829 if numbering_xml.is_some() {
1830 add_override(
1831 "word/numbering.xml",
1832 "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
1833 );
1834 }
1835 if footnotes_xml.is_some() {
1836 add_override(
1837 "word/footnotes.xml",
1838 "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
1839 );
1840 }
1841 if endnotes_xml.is_some() {
1842 add_override(
1843 "word/endnotes.xml",
1844 "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
1845 );
1846 }
1847 if comments_xml.is_some() {
1848 add_override(
1849 "word/comments.xml",
1850 "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml",
1851 );
1852 }
1853 for index in 1..=self.header_count {
1854 add_override(
1855 &format!("word/header{index}.xml"),
1856 "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
1857 );
1858 }
1859 for index in 1..=self.footer_count {
1860 add_override(
1861 &format!("word/footer{index}.xml"),
1862 "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
1863 );
1864 }
1865 content_types.push_str("</Types>");
1866
1867 let root_rels = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
1868
1869 let mut parts = HashMap::new();
1871 let mut insert = |name: &str, content: String| {
1872 parts.insert(
1873 name.to_string(),
1874 OoxmlPart {
1875 name: name.to_string(),
1876 content,
1877 },
1878 );
1879 };
1880 insert("[Content_Types].xml", content_types);
1881 insert("_rels/.rels", root_rels.to_string());
1882 insert("word/document.xml", document_xml);
1883 insert("word/_rels/document.xml.rels", document_rels_xml);
1884 insert("word/styles.xml", styles_xml);
1885 insert("word/settings.xml", settings_xml);
1886 if let Some(numbering) = numbering_xml {
1887 insert("word/numbering.xml", numbering);
1888 }
1889 if let Some(footnotes) = footnotes_xml {
1890 insert("word/footnotes.xml", footnotes);
1891 }
1892 if let Some(endnotes) = endnotes_xml {
1893 insert("word/endnotes.xml", endnotes);
1894 }
1895 if let Some(comments) = comments_xml {
1896 insert("word/comments.xml", comments);
1897 }
1898 for (name, content) in self.parts.drain() {
1899 if name.starts_with("__sect_refs_") {
1900 continue;
1901 }
1902 parts.insert(name.clone(), OoxmlPart { name, content });
1903 }
1904
1905 let mut unanchored: Vec<(u32, u32)> = self
1906 .text_boxes_by_anchor
1907 .drain()
1908 .flat_map(|(cp, specs)| specs.into_iter().map(move |spec| (cp, spec.shape_id)))
1909 .collect();
1910 unanchored.sort_unstable();
1911 for (cp, shape_id) in unanchored {
1912 self.warnings.push(format!(
1913 "Legacy DOC textbox shape {shape_id} could not be attached at CP {cp}"
1914 ));
1915 }
1916
1917 Ok(OoxmlPackage {
1918 parts,
1919 binary_assets: self.media.drain().collect(),
1920 warnings: std::mem::take(&mut self.warnings),
1921 })
1922 }
1923}
1924
1925fn format_twips_as_points(twips: i64) -> String {
1926 let sign = if twips < 0 { "-" } else { "" };
1927 let magnitude = twips.unsigned_abs();
1928 let whole = magnitude / 20;
1929 let remainder = magnitude % 20;
1930 if remainder == 0 {
1931 format!("{sign}{whole}")
1932 } else {
1933 let hundredths = remainder * 5;
1934 format!("{sign}{whole}.{hundredths:02}")
1935 }
1936}
1937
1938fn xml_push_grid_span(tc_pr: &mut String, grid_span: usize) {
1939 tc_pr.push_str(&format!(r#"<w:gridSpan w:val="{grid_span}"/>"#));
1940}