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