Skip to main content

docx_rs/documents/
mod.rs

1use std::{
2    collections::{HashMap, HashSet},
3    str::FromStr,
4};
5
6mod bookmark_id;
7mod build_xml;
8mod comments;
9mod comments_extended;
10mod content_types;
11mod custom_item;
12mod custom_item_property;
13mod custom_item_rels;
14mod doc_props;
15mod document;
16mod document_rels;
17mod document_tree;
18mod elements;
19mod font_table;
20mod footer;
21mod footer_id;
22mod footer_rels;
23mod footnote_id;
24mod footnotes;
25mod header;
26mod header_id;
27mod header_rels;
28mod history_id;
29mod hyperlink_id;
30mod image_collector;
31mod numberings;
32mod paragraph_id;
33mod paragraph_property_change_id;
34mod pic_id;
35mod preset_styles;
36mod rels;
37mod settings;
38mod styles;
39mod taskpanes;
40mod taskpanes_rels;
41mod theme;
42mod toc_key;
43mod web_settings;
44mod webextension;
45mod xml_docx;
46
47pub use build_xml::BuildXML;
48pub(crate) use history_id::HistoryId;
49pub(crate) use hyperlink_id::*;
50pub(crate) use paragraph_id::*;
51pub(crate) use paragraph_property_change_id::ParagraphPropertyChangeId;
52pub(crate) use pic_id::*;
53
54pub use bookmark_id::*;
55pub use comments::*;
56pub use comments_extended::*;
57pub use content_types::*;
58pub use custom_item::*;
59pub use custom_item_property::*;
60pub use custom_item_rels::*;
61pub use doc_props::*;
62pub use document::*;
63pub use document_rels::*;
64pub use elements::*;
65pub use font_table::*;
66pub use footer::*;
67pub use footer_id::*;
68pub use footer_rels::*;
69pub use footnotes::*;
70pub use header::*;
71pub use header_id::*;
72pub use header_rels::*;
73pub use numberings::*;
74pub use rels::*;
75pub use settings::*;
76pub use styles::*;
77pub use taskpanes::*;
78pub use taskpanes_rels::*;
79pub use theme::*;
80pub use toc_key::*;
81pub use web_settings::*;
82pub use webextension::*;
83pub use xml_docx::*;
84
85use base64::Engine;
86use serde::{ser, Serialize};
87
88use self::image_collector::{
89    collect_document_footnotes, collect_document_part, collect_footer_part, collect_header_part,
90    MediaRegistry,
91};
92
93#[derive(Debug, Clone)]
94pub struct Image(pub Vec<u8>);
95
96/// Decoded preview bytes for an entry in `Docx.images`.
97///
98/// For raster originals (PNG / JPEG / BMP / GIF / TIFF) decoded via the
99/// `image` crate, the contents are PNG bytes. Unsupported formats such as
100/// EMF are surfaced with an empty preview so downstream consumers can
101/// provide their own conversion. The struct is named `Png` for backwards
102/// compatibility.
103#[derive(Debug, Clone)]
104pub struct Png(pub Vec<u8>);
105
106pub type ImageIdAndPath = (String, String);
107pub type ImageIdAndBuf = (String, Vec<u8>);
108
109const PNG_SIGNATURE: &[u8; 8] = &[137, 80, 78, 71, 13, 10, 26, 10];
110
111fn is_emf(path: &str, buf: &[u8]) -> bool {
112    if path.to_ascii_lowercase().ends_with(".emf") {
113        return true;
114    }
115    // EMF files start with EMR_HEADER, whose first 4 bytes are the
116    // record type 0x00000001 (little-endian) and bytes 40..44 hold
117    // the signature " EMF" (0x464D4520).
118    buf.len() >= 44
119        && buf[0..4] == [0x01, 0x00, 0x00, 0x00]
120        && buf[40..44] == [0x20, 0x45, 0x4D, 0x46]
121}
122
123impl ser::Serialize for Image {
124    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
125    where
126        S: ser::Serializer,
127    {
128        let base64 = base64::engine::general_purpose::STANDARD.encode(&self.0);
129        serializer.collect_str(&base64)
130    }
131}
132
133impl ser::Serialize for Png {
134    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
135    where
136        S: ser::Serializer,
137    {
138        let base64 = base64::engine::general_purpose::STANDARD.encode(&self.0);
139        serializer.collect_str(&base64)
140    }
141}
142
143#[derive(Debug, Clone, Serialize)]
144#[serde(rename_all = "camelCase")]
145pub struct Docx {
146    pub content_type: ContentTypes,
147    pub rels: Rels,
148    pub document_rels: DocumentRels,
149    pub doc_props: DocProps,
150    pub styles: Styles,
151    pub document: Document,
152    pub comments: Comments,
153    pub numberings: Numberings,
154    pub settings: Settings,
155    pub font_table: FontTable,
156    pub media: Vec<(String, Vec<u8>)>,
157    pub comments_extended: CommentsExtended,
158    pub web_settings: WebSettings,
159    pub taskpanes: Option<Taskpanes>,
160    pub taskpanes_rels: TaskpanesRels,
161    pub web_extensions: Vec<WebExtension>,
162    pub custom_items: Vec<CustomItem>,
163    pub custom_item_props: Vec<CustomItemProperty>,
164    pub custom_item_rels: Vec<CustomItemRels>,
165    // reader only
166    pub themes: Vec<Theme>,
167    /// Reader-only collection of images embedded in `word/media/`.
168    ///
169    /// Each tuple is `(rId, media path, original bytes, preview bytes)`.
170    /// The preview is PNG for raster originals decoded via the `image`
171    /// crate. Unsupported formats such as EMF keep an empty preview for
172    /// downstream consumers to populate. See [`Png`] for details.
173    pub images: Vec<(String, String, Image, Png)>,
174    // reader only
175    pub hyperlinks: Vec<(String, String, String)>,
176    pub footnotes: Footnotes,
177}
178
179/// Package metadata derived while normalizing a document for output.
180pub(crate) struct PackageMetadata {
181    pub(crate) media: Vec<ImageIdAndBuf>,
182    pub(crate) header_rels: Vec<HeaderRels>,
183    pub(crate) footer_rels: Vec<FooterRels>,
184}
185
186impl Default for Docx {
187    fn default() -> Self {
188        let content_type = ContentTypes::new().set_default();
189        let rels = Rels::new().set_default();
190        let doc_props = DocProps::new(CorePropsConfig::new());
191        let styles = Styles::new();
192        let document = Document::new();
193        let document_rels = DocumentRels::new();
194        let settings = Settings::new();
195        let font_table = FontTable::new();
196        let comments = Comments::new();
197        let numberings = Numberings::new();
198        let media = vec![];
199        let comments_extended = CommentsExtended::new();
200        let web_settings = WebSettings::new();
201        let footnotes = Footnotes::default();
202
203        Docx {
204            content_type,
205            rels,
206            document_rels,
207            doc_props,
208            styles,
209            document,
210            comments,
211            numberings,
212            settings,
213            font_table,
214            media,
215            comments_extended,
216            web_settings,
217            taskpanes: None,
218            taskpanes_rels: TaskpanesRels::new(),
219            web_extensions: vec![],
220            custom_items: vec![],
221            custom_item_props: vec![],
222            custom_item_rels: vec![],
223            themes: vec![],
224            images: vec![],
225            hyperlinks: vec![],
226            footnotes,
227        }
228    }
229}
230
231impl Docx {
232    pub fn new() -> Docx {
233        Default::default()
234    }
235
236    pub fn document(mut self, d: Document) -> Docx {
237        for child in &self.document.children {
238            match child {
239                DocumentChild::Paragraph(paragraph) => {
240                    if paragraph.has_numbering {
241                        self.document_rels.has_numberings = true;
242                    }
243                }
244                DocumentChild::Table(table) if table.has_numbering => {
245                    self.document_rels.has_numberings = true;
246                }
247                _ => {}
248            }
249        }
250        self.document = d;
251        self
252    }
253
254    pub fn styles(mut self, s: Styles) -> Self {
255        self.styles = s;
256        self
257    }
258
259    pub fn add_style(mut self, s: Style) -> Self {
260        self.styles = self.styles.add_style(s);
261        self
262    }
263
264    pub fn numberings(mut self, n: Numberings) -> Self {
265        self.numberings = n;
266        self
267    }
268
269    pub fn settings(mut self, s: Settings) -> Self {
270        self.settings = s;
271        self
272    }
273
274    // reader only
275    pub(crate) fn web_settings(mut self, s: WebSettings) -> Self {
276        self.web_settings = s;
277        self
278    }
279
280    // reader only
281    pub(crate) fn add_image(
282        self,
283        id: impl Into<String>,
284        path: impl Into<String>,
285        buf: Vec<u8>,
286    ) -> Self {
287        self.add_image_with_options(id, path, buf, true)
288    }
289
290    // reader only
291    pub(crate) fn add_image_with_options(
292        mut self,
293        id: impl Into<String>,
294        path: impl Into<String>,
295        buf: Vec<u8>,
296        generate_preview: bool,
297    ) -> Self {
298        let path: String = path.into();
299
300        if is_emf(&path, &buf) {
301            self.images
302                .push((id.into(), path, Image(buf), Png(Vec::new())));
303            return self;
304        }
305
306        if !generate_preview {
307            self.images
308                .push((id.into(), path, Image(buf), Png(Vec::new())));
309            return self;
310        }
311
312        if buf.starts_with(PNG_SIGNATURE) {
313            self.images
314                .push((id.into(), path, Image(buf.clone()), Png(buf)));
315            return self;
316        }
317
318        #[cfg(feature = "image")]
319        if let Ok(dimg) = image::load_from_memory(&buf) {
320            let mut png = std::io::Cursor::new(vec![]);
321            // For now only png supported
322            dimg.write_to(&mut png, image::ImageFormat::Png)
323                .expect("Unable to write dynamic image");
324
325            self.images
326                .push((id.into(), path, Image(buf), Png(png.into_inner())));
327        }
328        self
329    }
330
331    // reader only
332    pub(crate) fn add_hyperlink(
333        mut self,
334        id: impl Into<String>,
335        path: impl Into<String>,
336        r#type: impl Into<String>,
337    ) -> Self {
338        self.hyperlinks
339            .push((id.into(), path.into(), r#type.into()));
340        self
341    }
342
343    pub fn comments(mut self, c: Comments) -> Self {
344        self.comments = c;
345        self
346    }
347
348    pub fn comments_extended(mut self, c: CommentsExtended) -> Self {
349        self.comments_extended = c;
350        self
351    }
352
353    pub fn add_paragraph(mut self, p: Paragraph) -> Docx {
354        if p.has_numbering {
355            // If this document has numbering, set numberings.xml to document_rels.
356            // This is because numberings.xml without numbering cause an error on word online.
357            self.document_rels.has_numberings = true;
358        }
359        self.document = self.document.add_paragraph(p);
360        self
361    }
362
363    pub fn add_section(mut self, s: Section) -> Docx {
364        if s.has_numbering {
365            // If this document has numbering, set numberings.xml to document_rels.
366            // This is because numberings.xml without numbering cause an error on word online.
367            self.document_rels.has_numberings = true;
368        }
369
370        let mut new_section = s;
371
372        // header
373        if let Some(header) = new_section.temp_header {
374            if header.has_numbering {
375                self.document_rels.has_numberings = true;
376            }
377            let count = self.document_rels.header_count + 1;
378            new_section = Section {
379                property: new_section
380                    .property
381                    .header(header, &create_header_rid(count)),
382                children: new_section.children,
383                has_numbering: new_section.has_numbering,
384                temp_header: None,
385                ..Default::default()
386            };
387            self.document_rels.header_count = count;
388            self.content_type = self.content_type.add_header();
389        }
390
391        if let Some(header) = new_section.temp_first_header {
392            if header.has_numbering {
393                self.document_rels.has_numberings = true;
394            }
395            let count = self.document_rels.header_count + 1;
396            new_section = Section {
397                property: new_section
398                    .property
399                    .first_header(header, &create_header_rid(count)),
400                children: new_section.children,
401                has_numbering: new_section.has_numbering,
402                temp_first_header: None,
403                ..Default::default()
404            };
405            self.document_rels.header_count = count;
406            self.content_type = self.content_type.add_header();
407        }
408
409        if let Some(header) = new_section.temp_even_header {
410            if header.has_numbering {
411                self.document_rels.has_numberings = true;
412            }
413            let count = self.document_rels.header_count + 1;
414            new_section = Section {
415                property: new_section
416                    .property
417                    .even_header(header, &create_header_rid(count)),
418                children: new_section.children,
419                has_numbering: new_section.has_numbering,
420                temp_even_header: None,
421                ..Default::default()
422            };
423            self.document_rels.header_count = count;
424            self.content_type = self.content_type.add_header();
425        }
426
427        // header
428        if let Some(header) = new_section.temp_header {
429            if header.has_numbering {
430                self.document_rels.has_numberings = true;
431            }
432            let count = self.document_rels.header_count + 1;
433            new_section = Section {
434                property: new_section
435                    .property
436                    .header(header, &create_header_rid(count)),
437                children: new_section.children,
438                has_numbering: new_section.has_numbering,
439                temp_header: None,
440                ..Default::default()
441            };
442            self.document_rels.header_count = count;
443            self.content_type = self.content_type.add_header();
444        }
445
446        if let Some(header) = new_section.temp_first_header {
447            if header.has_numbering {
448                self.document_rels.has_numberings = true;
449            }
450            let count = self.document_rels.header_count + 1;
451            new_section = Section {
452                property: new_section
453                    .property
454                    .first_header(header, &create_header_rid(count)),
455                children: new_section.children,
456                has_numbering: new_section.has_numbering,
457                temp_first_header: None,
458                ..Default::default()
459            };
460            self.document_rels.header_count = count;
461            self.content_type = self.content_type.add_header();
462        }
463
464        if let Some(header) = new_section.temp_even_header {
465            if header.has_numbering {
466                self.document_rels.has_numberings = true;
467            }
468            let count = self.document_rels.header_count + 1;
469            new_section = Section {
470                property: new_section
471                    .property
472                    .even_header(header, &create_header_rid(count)),
473                children: new_section.children,
474                has_numbering: new_section.has_numbering,
475                temp_even_header: None,
476                ..Default::default()
477            };
478            self.document_rels.header_count = count;
479            self.content_type = self.content_type.add_header();
480        }
481
482        // footer
483        if let Some(footer) = new_section.temp_footer {
484            if footer.has_numbering {
485                self.document_rels.has_numberings = true;
486            }
487            let count = self.document_rels.footer_count + 1;
488            new_section = Section {
489                property: new_section
490                    .property
491                    .footer(footer, &create_footer_rid(count)),
492                children: new_section.children,
493                has_numbering: new_section.has_numbering,
494                temp_footer: None,
495                ..Default::default()
496            };
497            self.document_rels.footer_count = count;
498            self.content_type = self.content_type.add_footer();
499        }
500
501        if let Some(footer) = new_section.temp_first_footer {
502            if footer.has_numbering {
503                self.document_rels.has_numberings = true;
504            }
505            let count = self.document_rels.footer_count + 1;
506            new_section = Section {
507                property: new_section
508                    .property
509                    .first_footer(footer, &create_footer_rid(count)),
510                children: new_section.children,
511                has_numbering: new_section.has_numbering,
512                temp_first_footer: None,
513                ..Default::default()
514            };
515            self.document_rels.footer_count = count;
516            self.content_type = self.content_type.add_footer();
517        }
518
519        if let Some(footer) = new_section.temp_even_footer {
520            if footer.has_numbering {
521                self.document_rels.has_numberings = true;
522            }
523            let count = self.document_rels.footer_count + 1;
524            new_section = Section {
525                property: new_section
526                    .property
527                    .even_footer(footer, &create_footer_rid(count)),
528                children: new_section.children,
529                has_numbering: new_section.has_numbering,
530                temp_even_footer: None,
531                ..Default::default()
532            };
533            self.document_rels.footer_count = count;
534            self.content_type = self.content_type.add_footer();
535        }
536
537        self.document = self.document.add_section(new_section);
538        self
539    }
540
541    pub fn add_structured_data_tag(mut self, t: StructuredDataTag) -> Docx {
542        if t.has_numbering {
543            // If this document has numbering, set numberings.xml to document_rels.
544            // This is because numberings.xml without numbering cause an error on word online.
545            self.document_rels.has_numberings = true;
546        }
547        self.document = self.document.add_structured_data_tag(t);
548        self
549    }
550
551    pub fn add_table_of_contents(mut self, t: TableOfContents) -> Docx {
552        self.document = self.document.add_table_of_contents(t);
553        self
554    }
555
556    pub fn add_bookmark_start(mut self, id: usize, name: impl Into<String>) -> Docx {
557        self.document = self.document.add_bookmark_start(id, name);
558        self
559    }
560
561    pub fn add_bookmark_end(mut self, id: usize) -> Docx {
562        self.document = self.document.add_bookmark_end(id);
563        self
564    }
565
566    pub fn add_table(mut self, t: Table) -> Docx {
567        if t.has_numbering {
568            // If this document has numbering, set numberings.xml to document_rels.
569            // This is because numberings.xml without numbering cause an error on word online.
570            self.document_rels.has_numberings = true;
571        }
572        self.document = self.document.add_table(t);
573        self
574    }
575
576    pub fn header(mut self, header: Header) -> Self {
577        if header.has_numbering {
578            self.document_rels.has_numberings = true;
579        }
580        let count = self.document_rels.header_count + 1;
581        self.document.section_property = self
582            .document
583            .section_property
584            .header(header, &create_header_rid(count));
585        self.document_rels.header_count = count;
586        self.content_type = self.content_type.add_header();
587        self
588    }
589
590    pub fn first_header(mut self, header: Header) -> Self {
591        if header.has_numbering {
592            self.document_rels.has_numberings = true;
593        }
594        let count = self.document_rels.header_count + 1;
595        self.document.section_property = self
596            .document
597            .section_property
598            .first_header(header, &create_header_rid(count));
599        self.document_rels.header_count = count;
600        self.content_type = self.content_type.add_header();
601        self
602    }
603
604    pub fn even_header(mut self, header: Header) -> Self {
605        if header.has_numbering {
606            self.document_rels.has_numberings = true;
607        }
608        let count = self.document_rels.header_count + 1;
609        self.document.section_property = self
610            .document
611            .section_property
612            .even_header(header, &create_header_rid(count));
613        self.document_rels.header_count = count;
614        self.content_type = self.content_type.add_header();
615        self.settings = self.settings.even_and_odd_headers();
616        self
617    }
618
619    pub fn footer(mut self, footer: Footer) -> Self {
620        if footer.has_numbering {
621            self.document_rels.has_numberings = true;
622        }
623        let count = self.document_rels.footer_count + 1;
624        self.document.section_property = self
625            .document
626            .section_property
627            .footer(footer, &create_footer_rid(count));
628        self.document_rels.footer_count = count;
629        self.content_type = self.content_type.add_footer();
630        self
631    }
632
633    pub fn first_footer(mut self, footer: Footer) -> Self {
634        if footer.has_numbering {
635            self.document_rels.has_numberings = true;
636        }
637        let count = self.document_rels.footer_count + 1;
638        self.document.section_property = self
639            .document
640            .section_property
641            .first_footer(footer, &create_footer_rid(count));
642        self.document_rels.footer_count = count;
643        self.content_type = self.content_type.add_footer();
644        self
645    }
646
647    pub fn even_footer(mut self, footer: Footer) -> Self {
648        if footer.has_numbering {
649            self.document_rels.has_numberings = true;
650        }
651        let count = self.document_rels.footer_count + 1;
652        self.document.section_property = self
653            .document
654            .section_property
655            .even_footer(footer, &create_footer_rid(count));
656        self.document_rels.footer_count = count;
657        self.content_type = self.content_type.add_footer();
658        self.settings = self.settings.even_and_odd_headers();
659        self
660    }
661
662    pub fn add_abstract_numbering(mut self, num: AbstractNumbering) -> Docx {
663        self.numberings = self.numberings.add_abstract_numbering(num);
664        self
665    }
666
667    pub fn add_numbering(mut self, num: Numbering) -> Docx {
668        self.numberings = self.numberings.add_numbering(num);
669        self
670    }
671
672    pub fn created_at(mut self, date: &str) -> Self {
673        self.doc_props = self.doc_props.created_at(date);
674        self
675    }
676
677    pub fn updated_at(mut self, date: &str) -> Self {
678        self.doc_props = self.doc_props.updated_at(date);
679        self
680    }
681
682    pub fn custom_property(mut self, name: impl Into<String>, item: impl Into<String>) -> Self {
683        self.doc_props = self.doc_props.custom_property(name, item);
684        self
685    }
686
687    pub fn doc_id(mut self, id: &str) -> Self {
688        self.settings = self.settings.doc_id(id);
689        self
690    }
691
692    pub fn default_tab_stop(mut self, stop: usize) -> Self {
693        self.settings = self.settings.default_tab_stop(stop);
694        self
695    }
696
697    pub fn add_doc_var(mut self, name: &str, val: &str) -> Self {
698        self.settings = self.settings.add_doc_var(name, val);
699        self
700    }
701
702    pub fn title_pg(mut self) -> Self {
703        self.document = self.document.title_pg();
704        self
705    }
706
707    pub fn page_size(mut self, w: u32, h: u32) -> Self {
708        self.document = self.document.page_size(PageSize::new().size(w, h));
709        self
710    }
711
712    pub fn page_margin(mut self, margin: crate::types::PageMargin) -> Self {
713        self.document = self.document.page_margin(margin);
714        self
715    }
716
717    pub fn page_orient(mut self, o: crate::types::PageOrientationType) -> Self {
718        self.document = self.document.page_orient(o);
719        self
720    }
721
722    pub fn default_size(mut self, size: usize) -> Self {
723        self.styles = self.styles.default_size(size);
724        self
725    }
726
727    pub fn default_spacing(mut self, spacing: i32) -> Self {
728        self.styles = self.styles.default_spacing(spacing);
729        self
730    }
731
732    pub fn default_fonts(mut self, font: RunFonts) -> Self {
733        self.styles = self.styles.default_fonts(font);
734        self
735    }
736
737    pub fn default_line_spacing(mut self, spacing: LineSpacing) -> Self {
738        self.styles = self.styles.default_line_spacing(spacing);
739        self
740    }
741
742    pub fn taskpanes(mut self) -> Self {
743        self.taskpanes = Some(Taskpanes::new());
744        self.rels = self.rels.add_taskpanes_rel();
745        self.content_type = self.content_type.add_taskpanes();
746        self
747    }
748
749    pub fn web_extension(mut self, ext: WebExtension) -> Self {
750        self.web_extensions.push(ext);
751        self.taskpanes_rels = self.taskpanes_rels.add_rel();
752        self.content_type = self.content_type.add_web_extensions();
753        self
754    }
755
756    pub fn add_custom_item(mut self, id: &str, xml: &str) -> Self {
757        let x = CustomItem::from_str(xml).expect("should parse xml string");
758        self.content_type = self.content_type.add_custom_xml();
759        let rel = CustomItemRels::new().add_item();
760        self.custom_item_props.push(CustomItemProperty::new(id));
761        self.document_rels = self.document_rels.add_custom_item();
762        self.custom_item_rels.push(rel);
763        self.custom_items.push(x);
764        self
765    }
766
767    pub fn page_num_type(mut self, p: PageNumType) -> Self {
768        self.document = self.document.page_num_type(p);
769        self
770    }
771
772    /// Finalizes document-wide identifiers and relationships and renders the
773    /// uncompressed OPC package parts.
774    ///
775    /// This consumes the document. Call [`XMLDocx::pack`] to write a DOCX
776    /// archive.
777    pub fn build(mut self) -> XMLDocx {
778        let package = self.prepare_package();
779
780        let web_extensions = self.web_extensions.iter().map(|ext| ext.build()).collect();
781        let custom_items = self.custom_items.iter().map(|xml| xml.build()).collect();
782        let custom_item_props = self.custom_item_props.iter().map(|p| p.build()).collect();
783        let custom_item_rels = self
784            .custom_item_rels
785            .iter()
786            .map(|rel| rel.build())
787            .collect();
788
789        let mut headers: Vec<&(String, Header)> = self.document.section_property.get_headers();
790
791        self.document.children.iter().for_each(|child| {
792            if let DocumentChild::Section(section) = child {
793                for h in section.property.get_headers() {
794                    headers.push(h);
795                }
796            }
797        });
798
799        headers.sort_by(|a, b| a.0.cmp(&b.0));
800        let headers = headers.iter().map(|h| h.1.build()).collect();
801
802        let mut footers: Vec<&(String, Footer)> = self.document.section_property.get_footers();
803
804        self.document.children.iter().for_each(|child| {
805            if let DocumentChild::Section(section) = child {
806                for h in section.property.get_footers() {
807                    footers.push(h);
808                }
809            }
810        });
811
812        footers.sort_by(|a, b| a.0.cmp(&b.0));
813        let footers = footers.iter().map(|h| h.1.build()).collect();
814
815        XMLDocx {
816            content_type: self.content_type.build(),
817            rels: self.rels.build(),
818            doc_props: self.doc_props.build(),
819            styles: self.styles.build(),
820            document: self.document.build(),
821            comments: self.comments.build(),
822            document_rels: self.document_rels.build(),
823            header_rels: package.header_rels.into_iter().map(|r| r.build()).collect(),
824            footer_rels: package.footer_rels.into_iter().map(|r| r.build()).collect(),
825            settings: self.settings.build(),
826            font_table: self.font_table.build(),
827            numberings: self.numberings.build(),
828            media: package.media,
829            headers,
830            footers,
831            comments_extended: self.comments_extended.build(),
832            taskpanes: self.taskpanes.map(|taskpanes| taskpanes.build()),
833            taskpanes_rels: self.taskpanes_rels.build(),
834            web_extensions,
835            custom_items,
836            custom_item_rels,
837            custom_item_props,
838            footnotes: self.footnotes.build(),
839        }
840    }
841
842    /// Writes this document directly as a DOCX ZIP archive.
843    ///
844    /// Unlike [`Docx::build`] followed by [`XMLDocx::pack`], this method
845    /// streams XML package parts into the archive instead of retaining every
846    /// rendered part in a separate `Vec<u8>`.
847    pub fn pack<W>(mut self, writer: W) -> zip::result::ZipResult<()>
848    where
849        W: std::io::Write + std::io::Seek,
850    {
851        let package = self.prepare_package();
852        crate::zipper::zip_docx(writer, self, package)
853    }
854
855    pub fn json(&self) -> String {
856        self.reset();
857
858        serde_json::to_string_pretty(&self).unwrap()
859    }
860
861    // Internal: for docx-wasm
862    pub fn json_with_update_comments(&mut self) -> String {
863        self.reset();
864
865        self.update_dependencies();
866        serde_json::to_string_pretty(&self).unwrap()
867    }
868
869    // Internal: for docx-wasm
870    pub fn comments_json(&mut self) -> String {
871        self.reset();
872        self.update_dependencies();
873        serde_json::to_string_pretty(&self.comments).unwrap()
874    }
875
876    fn reset(&self) {
877        crate::reset_para_id();
878    }
879
880    /// Expands automatically generated tables of contents before the document
881    /// tree is normalized and inspected for dependencies.
882    fn expand_auto_tocs(&mut self) -> bool {
883        let tocs: Vec<(usize, Box<TableOfContents>)> = self
884            .document
885            .children
886            .iter()
887            .enumerate()
888            .filter_map(|(index, child)| match child {
889                DocumentChild::TableOfContents(toc) => Some((index, toc.clone())),
890                _ => None,
891            })
892            .collect();
893
894        for (index, toc) in &tocs {
895            if toc.items.is_empty() && toc.auto {
896                let children = std::mem::take(&mut self.document.children);
897                self.document.children =
898                    update_document_by_toc(children, &self.styles, *toc.clone(), *index);
899            }
900        }
901
902        !tocs.is_empty()
903    }
904
905    /// Prepares the complete document tree for package-part rendering.
906    fn prepare_for_build(&mut self) -> bool {
907        self.reset();
908        let has_toc = self.expand_auto_tocs();
909        self.refresh_duplicate_para_ids();
910        self.update_dependencies();
911        has_toc
912    }
913
914    /// Normalizes the document and collects metadata shared by buffered and
915    /// streaming package output.
916    fn prepare_package(&mut self) -> PackageMetadata {
917        let has_toc = self.prepare_for_build();
918
919        let mut media = MediaRegistry::default();
920        let document_part = collect_document_part(&mut self.document, &mut media);
921        let header_images = self.collect_header_images(&mut media);
922        let footer_images = self.collect_footer_images(&mut media);
923        self.document_rels.images = document_part.relationships;
924
925        let mut header_rels = vec![HeaderRels::new(); 3];
926        for (rels, images) in header_rels.iter_mut().zip(header_images) {
927            rels.set_images(images);
928        }
929
930        let mut footer_rels = vec![FooterRels::new(); 3];
931        for (rels, images) in footer_rels.iter_mut().zip(footer_images) {
932            rels.set_images(images);
933        }
934
935        if !document_part.footnotes.is_empty() {
936            self.footnotes.add(document_part.footnotes);
937            self.content_type = std::mem::take(&mut self.content_type).add_footnotes();
938            self.document_rels.has_footnotes = true;
939        }
940
941        if has_toc {
942            for level in 1..=9 {
943                if !self
944                    .styles
945                    .styles
946                    .iter()
947                    .any(|style| style.name == Name::new(format!("toc {level}")))
948                {
949                    self.styles = std::mem::take(&mut self.styles)
950                        .add_style(crate::documents::preset_styles::toc(level));
951                }
952            }
953        }
954
955        PackageMetadata {
956            media: media.into_media(),
957            header_rels,
958            footer_rels,
959        }
960    }
961
962    /// Replaces empty and duplicate paragraph IDs with unique values.
963    fn refresh_duplicate_para_ids(&mut self) {
964        let mut counts: HashMap<&str, usize> = HashMap::new();
965        collect_para_ids_in_docx(self, &mut counts);
966
967        let duplicates: HashSet<String> = counts
968            .iter()
969            .filter(|(_, count)| **count > 1)
970            .map(|(id, _)| (*id).to_owned())
971            .collect();
972        let has_empty_id = counts.contains_key("");
973        if duplicates.is_empty() && !has_empty_id {
974            return;
975        }
976
977        let mut used: HashSet<String> = counts.into_keys().map(str::to_owned).collect();
978        let mut seen: HashSet<String> = HashSet::new();
979
980        refresh_para_ids_in_docx(self, &duplicates, &mut used, &mut seen);
981    }
982
983    fn insert_comment_to_map(
984        &self,
985        comment_map: &mut HashMap<usize, String>,
986        c: &CommentRangeStart,
987    ) {
988        let comment = c.get_comment_ref();
989        let comment_id = comment.id();
990        for child in &comment.children {
991            if let CommentChild::Paragraph(child) = child {
992                let para_id = child.id.clone();
993                comment_map.insert(comment_id, para_id);
994            }
995            // TODO: Support table in comment
996        }
997    }
998
999    // Traverse and clone comments from document and add to comments node.
1000    fn update_dependencies(&mut self) {
1001        let mut comments: Vec<Comment> = vec![];
1002        let mut comments_extended: Vec<CommentExtended> = vec![];
1003        let mut comment_map: HashMap<usize, String> = HashMap::new();
1004
1005        let mut hyperlink_map: HashMap<String, String> = HashMap::new();
1006
1007        for child in &self.document.children {
1008            match child {
1009                DocumentChild::Paragraph(paragraph) => {
1010                    for child in &paragraph.children {
1011                        if let ParagraphChild::CommentStart(c) = child {
1012                            self.insert_comment_to_map(&mut comment_map, c);
1013                        }
1014                        if let ParagraphChild::Hyperlink(h) = child {
1015                            if let HyperlinkData::External { rid, path } = &h.link {
1016                                hyperlink_map.insert(rid.clone(), path.clone());
1017                            }
1018                            for child in &h.children {
1019                                if let ParagraphChild::CommentStart(c) = child {
1020                                    self.insert_comment_to_map(&mut comment_map, c);
1021                                }
1022                            }
1023                        }
1024                    }
1025                }
1026                DocumentChild::Table(table) => {
1027                    collect_comment_map_in_table(table, &mut comment_map, &mut hyperlink_map);
1028                }
1029                DocumentChild::TableOfContents(toc) => {
1030                    for child in &toc.before_contents {
1031                        if let TocContent::Paragraph(paragraph) = child {
1032                            collect_comment_map_in_paragraph(
1033                                paragraph,
1034                                &mut comment_map,
1035                                &mut hyperlink_map,
1036                            );
1037                        }
1038                        if let TocContent::Table(table) = child {
1039                            collect_comment_map_in_table(
1040                                table,
1041                                &mut comment_map,
1042                                &mut hyperlink_map,
1043                            );
1044                        }
1045                    }
1046                    for child in &toc.after_contents {
1047                        if let TocContent::Paragraph(paragraph) = child {
1048                            collect_comment_map_in_paragraph(
1049                                paragraph,
1050                                &mut comment_map,
1051                                &mut hyperlink_map,
1052                            );
1053                        }
1054                        if let TocContent::Table(table) = child {
1055                            collect_comment_map_in_table(
1056                                table,
1057                                &mut comment_map,
1058                                &mut hyperlink_map,
1059                            );
1060                        }
1061                    }
1062                }
1063                _ => {}
1064            }
1065        }
1066
1067        if comment_map.is_empty() {
1068            for (id, path) in hyperlink_map {
1069                self.document_rels
1070                    .hyperlinks
1071                    .push((id, path, "External".to_owned()));
1072            }
1073            return;
1074        }
1075
1076        for child in &self.document.children {
1077            match child {
1078                DocumentChild::Paragraph(paragraph) => {
1079                    for child in &paragraph.children {
1080                        if let ParagraphChild::CommentStart(c) = child {
1081                            push_comment_and_comment_extended(
1082                                &mut comments,
1083                                &mut comments_extended,
1084                                &comment_map,
1085                                c,
1086                            );
1087                        }
1088                        if let ParagraphChild::Hyperlink(h) = child {
1089                            if let HyperlinkData::External { rid, path } = &h.link {
1090                                hyperlink_map.insert(rid.clone(), path.clone());
1091                            };
1092                            for child in &h.children {
1093                                if let ParagraphChild::CommentStart(c) = child {
1094                                    push_comment_and_comment_extended(
1095                                        &mut comments,
1096                                        &mut comments_extended,
1097                                        &comment_map,
1098                                        c,
1099                                    );
1100                                }
1101                            }
1102                        }
1103                    }
1104                }
1105                DocumentChild::Table(table) => {
1106                    collect_dependencies_in_table(
1107                        table,
1108                        &mut comments,
1109                        &mut comments_extended,
1110                        &mut comment_map,
1111                        &mut hyperlink_map,
1112                    );
1113                }
1114                DocumentChild::TableOfContents(toc) => {
1115                    // TODO:refine later
1116                    for child in &toc.before_contents {
1117                        if let TocContent::Paragraph(paragraph) = child {
1118                            for child in &paragraph.children {
1119                                if let ParagraphChild::CommentStart(c) = child {
1120                                    push_comment_and_comment_extended(
1121                                        &mut comments,
1122                                        &mut comments_extended,
1123                                        &comment_map,
1124                                        c,
1125                                    );
1126                                }
1127                                if let ParagraphChild::Hyperlink(h) = child {
1128                                    if let HyperlinkData::External { rid, path } = &h.link {
1129                                        hyperlink_map.insert(rid.clone(), path.clone());
1130                                    };
1131                                    for child in &h.children {
1132                                        if let ParagraphChild::CommentStart(c) = child {
1133                                            push_comment_and_comment_extended(
1134                                                &mut comments,
1135                                                &mut comments_extended,
1136                                                &comment_map,
1137                                                c,
1138                                            );
1139                                        }
1140                                    }
1141                                }
1142                            }
1143                        }
1144                        if let TocContent::Table(table) = child {
1145                            collect_dependencies_in_table(
1146                                table,
1147                                &mut comments,
1148                                &mut comments_extended,
1149                                &mut comment_map,
1150                                &mut hyperlink_map,
1151                            );
1152                        }
1153                    }
1154                    for child in &toc.after_contents {
1155                        if let TocContent::Paragraph(paragraph) = child {
1156                            for child in &paragraph.children {
1157                                if let ParagraphChild::CommentStart(c) = child {
1158                                    push_comment_and_comment_extended(
1159                                        &mut comments,
1160                                        &mut comments_extended,
1161                                        &comment_map,
1162                                        c,
1163                                    );
1164                                }
1165                                if let ParagraphChild::Hyperlink(h) = child {
1166                                    if let HyperlinkData::External { rid, path } = &h.link {
1167                                        hyperlink_map.insert(rid.clone(), path.clone());
1168                                    };
1169                                    for child in &h.children {
1170                                        if let ParagraphChild::CommentStart(c) = child {
1171                                            push_comment_and_comment_extended(
1172                                                &mut comments,
1173                                                &mut comments_extended,
1174                                                &comment_map,
1175                                                c,
1176                                            );
1177                                        }
1178                                    }
1179                                }
1180                            }
1181                        }
1182                        if let TocContent::Table(table) = child {
1183                            collect_dependencies_in_table(
1184                                table,
1185                                &mut comments,
1186                                &mut comments_extended,
1187                                &mut comment_map,
1188                                &mut hyperlink_map,
1189                            );
1190                        }
1191                    }
1192                }
1193                _ => {}
1194            }
1195        }
1196        // If this document has comments, set comments.xml to document_rels.
1197        // This is because comments.xml without comment cause an error on word online.
1198        if !comments.is_empty() {
1199            self.document_rels.has_comments = true;
1200        }
1201
1202        self.comments_extended
1203            .add_comments_extended(comments_extended);
1204
1205        self.comments.add_comments(comments);
1206
1207        for (id, d) in hyperlink_map {
1208            self.document_rels
1209                .hyperlinks
1210                .push((id, d, "External".to_string())); // Now support external only
1211        }
1212    }
1213
1214    // Traverse and clone comments from document and add to comments node.
1215    // reader only
1216    pub(crate) fn store_comments(&mut self, comments: &[Comment]) {
1217        let comments_by_id: HashMap<usize, &Comment> = comments
1218            .iter()
1219            .map(|comment| (comment.id(), comment))
1220            .collect();
1221        for child in &mut self.document.children {
1222            match child {
1223                DocumentChild::Paragraph(paragraph) => {
1224                    for child in &mut paragraph.children {
1225                        if let ParagraphChild::CommentStart(ref mut c) = child {
1226                            let comment_id = c.get_id();
1227                            if let Some(comment) = comments_by_id.get(&comment_id).copied() {
1228                                let comment = Comment::clone(comment);
1229                                c.as_mut().comment(comment);
1230                            }
1231                        }
1232                        if let ParagraphChild::Insert(ref mut insert) = child {
1233                            for child in &mut insert.children {
1234                                if let InsertChild::CommentStart(ref mut c) = child {
1235                                    let comment_id = c.get_id();
1236                                    if let Some(comment) = comments_by_id.get(&comment_id).copied()
1237                                    {
1238                                        let comment = Comment::clone(comment);
1239                                        c.as_mut().comment(comment);
1240                                    }
1241                                } else if let InsertChild::Delete(ref mut d) = child {
1242                                    for child in &mut d.children {
1243                                        if let DeleteChild::CommentStart(ref mut c) = child {
1244                                            let comment_id = c.get_id();
1245                                            if let Some(comment) =
1246                                                comments_by_id.get(&comment_id).copied()
1247                                            {
1248                                                let comment = Comment::clone(comment);
1249                                                c.as_mut().comment(comment);
1250                                            }
1251                                        }
1252                                    }
1253                                }
1254                            }
1255                        }
1256                        if let ParagraphChild::Delete(ref mut delete) = child {
1257                            for child in &mut delete.children {
1258                                if let DeleteChild::CommentStart(ref mut c) = child {
1259                                    let comment_id = c.get_id();
1260                                    if let Some(comment) = comments_by_id.get(&comment_id).copied()
1261                                    {
1262                                        let comment = Comment::clone(comment);
1263                                        c.as_mut().comment(comment);
1264                                    }
1265                                }
1266                            }
1267                        }
1268                    }
1269                }
1270                DocumentChild::Table(table) => store_comments_in_table(table, &comments_by_id),
1271                _ => {}
1272            }
1273        }
1274
1275        if !comments.is_empty() {
1276            self.document_rels.has_comments = true;
1277        }
1278    }
1279
1280    /// Collects image relationships for the default, first, and even headers.
1281    ///
1282    /// Each entry deliberately uses a separate relationship scope while all
1283    /// entries share the package-wide media registry. This preserves valid
1284    /// header `.rels` files without storing duplicate image bytes.
1285    fn collect_header_images(&mut self, media: &mut MediaRegistry) -> Vec<Vec<ImageIdAndPath>> {
1286        let mut images = vec![Vec::new(); 3];
1287        let headers = [
1288            &mut self.document.section_property.header,
1289            &mut self.document.section_property.first_header,
1290            &mut self.document.section_property.even_header,
1291        ];
1292
1293        for (images, header) in images.iter_mut().zip(headers) {
1294            if let Some((_, header)) = header.as_mut() {
1295                *images = collect_header_part(header, media).relationships;
1296            }
1297        }
1298        images
1299    }
1300
1301    /// Collects image relationships for the default, first, and even footers.
1302    ///
1303    /// The fixed order matches the package writer's footer part ordering. A
1304    /// shared registry prevents repeated footer and body images from being
1305    /// emitted as separate physical media files.
1306    fn collect_footer_images(&mut self, media: &mut MediaRegistry) -> Vec<Vec<ImageIdAndPath>> {
1307        let mut images = vec![Vec::new(); 3];
1308        let footers = [
1309            &mut self.document.section_property.footer,
1310            &mut self.document.section_property.first_footer,
1311            &mut self.document.section_property.even_footer,
1312        ];
1313
1314        for (images, footer) in images.iter_mut().zip(footers) {
1315            if let Some((_, footer)) = footer.as_mut() {
1316                *images = collect_footer_part(footer, media).relationships;
1317            }
1318        }
1319        images
1320    }
1321
1322    /// Collects footnote references from the complete main document tree.
1323    ///
1324    /// This method remains available for callers that collect dependencies
1325    /// explicitly. Normal package builds collect footnotes together with
1326    /// images, avoiding a second traversal of the document tree.
1327    pub fn collect_footnotes(&mut self) -> bool {
1328        let footnotes = collect_document_footnotes(&mut self.document);
1329        let is_footnotes = !footnotes.is_empty();
1330        self.footnotes.add(footnotes);
1331        is_footnotes
1332    }
1333}
1334
1335fn collect_dependencies_in_paragraph(
1336    paragraph: &Paragraph,
1337    comments: &mut Vec<Comment>,
1338    comments_extended: &mut Vec<CommentExtended>,
1339    comment_map: &mut HashMap<usize, String>,
1340    hyperlink_map: &mut HashMap<String, String>,
1341) {
1342    for child in &paragraph.children {
1343        if let ParagraphChild::CommentStart(c) = child {
1344            push_comment_and_comment_extended(comments, comments_extended, comment_map, c);
1345        }
1346        if let ParagraphChild::Hyperlink(h) = child {
1347            if let HyperlinkData::External { rid, path } = &h.link {
1348                hyperlink_map.insert(rid.clone(), path.clone());
1349            };
1350            for child in &h.children {
1351                if let ParagraphChild::CommentStart(c) = child {
1352                    push_comment_and_comment_extended(comments, comments_extended, comment_map, c);
1353                }
1354            }
1355        }
1356    }
1357}
1358
1359fn collect_comment_map_in_paragraph(
1360    paragraph: &Paragraph,
1361    comment_map: &mut HashMap<usize, String>,
1362    hyperlink_map: &mut HashMap<String, String>,
1363) {
1364    for child in &paragraph.children {
1365        if let ParagraphChild::CommentStart(c) = child {
1366            let comment = c.get_comment_ref();
1367            let comment_id = comment.id();
1368            for child in &comment.children {
1369                if let CommentChild::Paragraph(child) = child {
1370                    comment_map.insert(comment_id, child.id.clone());
1371                }
1372            }
1373        }
1374        if let ParagraphChild::Hyperlink(h) = child {
1375            if let HyperlinkData::External { rid, path } = &h.link {
1376                hyperlink_map.insert(rid.clone(), path.clone());
1377            }
1378            for child in &h.children {
1379                if let ParagraphChild::CommentStart(c) = child {
1380                    let comment = c.get_comment_ref();
1381                    let comment_id = comment.id();
1382                    for child in &comment.children {
1383                        if let CommentChild::Paragraph(child) = child {
1384                            comment_map.insert(comment_id, child.id.clone());
1385                        }
1386                    }
1387                }
1388            }
1389        }
1390    }
1391}
1392
1393fn collect_comment_map_in_table(
1394    table: &Table,
1395    comment_map: &mut HashMap<usize, String>,
1396    hyperlink_map: &mut HashMap<String, String>,
1397) {
1398    for TableChild::TableRow(row) in &table.rows {
1399        for TableRowChild::TableCell(cell) in &row.cells {
1400            for content in &cell.children {
1401                match content {
1402                    TableCellContent::Paragraph(paragraph) => {
1403                        collect_comment_map_in_paragraph(paragraph, comment_map, hyperlink_map);
1404                    }
1405                    TableCellContent::Table(table) => {
1406                        collect_comment_map_in_table(table, comment_map, hyperlink_map)
1407                    }
1408                    TableCellContent::StructuredDataTag(tag) => {
1409                        for child in &tag.children {
1410                            if let StructuredDataTagChild::Paragraph(paragraph) = child {
1411                                collect_comment_map_in_paragraph(
1412                                    paragraph,
1413                                    comment_map,
1414                                    hyperlink_map,
1415                                );
1416                            }
1417                            if let StructuredDataTagChild::Table(table) = child {
1418                                collect_comment_map_in_table(table, comment_map, hyperlink_map);
1419                            }
1420                        }
1421                    }
1422                    TableCellContent::TableOfContents(t) => {
1423                        for child in &t.before_contents {
1424                            if let TocContent::Paragraph(paragraph) = child {
1425                                collect_comment_map_in_paragraph(
1426                                    paragraph,
1427                                    comment_map,
1428                                    hyperlink_map,
1429                                );
1430                            }
1431                            if let TocContent::Table(table) = child {
1432                                collect_comment_map_in_table(table, comment_map, hyperlink_map);
1433                            }
1434                        }
1435                        for child in &t.after_contents {
1436                            if let TocContent::Paragraph(paragraph) = child {
1437                                collect_comment_map_in_paragraph(
1438                                    paragraph,
1439                                    comment_map,
1440                                    hyperlink_map,
1441                                );
1442                            }
1443                            if let TocContent::Table(table) = child {
1444                                collect_comment_map_in_table(table, comment_map, hyperlink_map);
1445                            }
1446                        }
1447                    }
1448                }
1449            }
1450        }
1451    }
1452}
1453
1454fn collect_dependencies_in_table(
1455    table: &Table,
1456    comments: &mut Vec<Comment>,
1457    comments_extended: &mut Vec<CommentExtended>,
1458    comment_map: &mut HashMap<usize, String>,
1459    hyperlink_map: &mut HashMap<String, String>,
1460) {
1461    for TableChild::TableRow(row) in &table.rows {
1462        for TableRowChild::TableCell(cell) in &row.cells {
1463            for content in &cell.children {
1464                match content {
1465                    TableCellContent::Paragraph(paragraph) => {
1466                        collect_dependencies_in_paragraph(
1467                            paragraph,
1468                            comments,
1469                            comments_extended,
1470                            comment_map,
1471                            hyperlink_map,
1472                        );
1473                    }
1474                    TableCellContent::Table(table) => collect_dependencies_in_table(
1475                        table,
1476                        comments,
1477                        comments_extended,
1478                        comment_map,
1479                        hyperlink_map,
1480                    ),
1481                    TableCellContent::StructuredDataTag(tag) => {
1482                        for child in &tag.children {
1483                            if let StructuredDataTagChild::Paragraph(paragraph) = child {
1484                                collect_dependencies_in_paragraph(
1485                                    paragraph,
1486                                    comments,
1487                                    comments_extended,
1488                                    comment_map,
1489                                    hyperlink_map,
1490                                );
1491                            }
1492                            if let StructuredDataTagChild::Table(table) = child {
1493                                collect_dependencies_in_table(
1494                                    table,
1495                                    comments,
1496                                    comments_extended,
1497                                    comment_map,
1498                                    hyperlink_map,
1499                                );
1500                            }
1501                        }
1502                    }
1503                    TableCellContent::TableOfContents(t) => {
1504                        for child in &t.before_contents {
1505                            if let TocContent::Paragraph(paragraph) = child {
1506                                collect_dependencies_in_paragraph(
1507                                    paragraph,
1508                                    comments,
1509                                    comments_extended,
1510                                    comment_map,
1511                                    hyperlink_map,
1512                                );
1513                            }
1514                            if let TocContent::Table(table) = child {
1515                                collect_dependencies_in_table(
1516                                    table,
1517                                    comments,
1518                                    comments_extended,
1519                                    comment_map,
1520                                    hyperlink_map,
1521                                );
1522                            }
1523                        }
1524
1525                        for child in &t.after_contents {
1526                            if let TocContent::Paragraph(paragraph) = child {
1527                                collect_dependencies_in_paragraph(
1528                                    paragraph,
1529                                    comments,
1530                                    comments_extended,
1531                                    comment_map,
1532                                    hyperlink_map,
1533                                );
1534                            }
1535                            if let TocContent::Table(table) = child {
1536                                collect_dependencies_in_table(
1537                                    table,
1538                                    comments,
1539                                    comments_extended,
1540                                    comment_map,
1541                                    hyperlink_map,
1542                                );
1543                            }
1544                        }
1545                    }
1546                }
1547            }
1548        }
1549    }
1550}
1551
1552fn store_comments_in_paragraph(
1553    paragraph: &mut Paragraph,
1554    comments_by_id: &HashMap<usize, &Comment>,
1555) {
1556    for child in &mut paragraph.children {
1557        if let ParagraphChild::CommentStart(ref mut c) = child {
1558            let comment_id = c.get_id();
1559            if let Some(comment) = comments_by_id.get(&comment_id).copied() {
1560                let comment = Comment::clone(comment);
1561                c.as_mut().comment(comment);
1562            }
1563        }
1564        if let ParagraphChild::Insert(ref mut insert) = child {
1565            for child in &mut insert.children {
1566                if let InsertChild::CommentStart(ref mut c) = child {
1567                    let comment_id = c.get_id();
1568                    if let Some(comment) = comments_by_id.get(&comment_id).copied() {
1569                        let comment = Comment::clone(comment);
1570                        c.as_mut().comment(comment);
1571                    }
1572                }
1573            }
1574        }
1575        if let ParagraphChild::Delete(ref mut delete) = child {
1576            for child in &mut delete.children {
1577                if let DeleteChild::CommentStart(ref mut c) = child {
1578                    let comment_id = c.get_id();
1579                    if let Some(comment) = comments_by_id.get(&comment_id).copied() {
1580                        let comment = Comment::clone(comment);
1581                        c.as_mut().comment(comment);
1582                    }
1583                }
1584            }
1585        }
1586    }
1587}
1588
1589fn store_comments_in_table(table: &mut Table, comments_by_id: &HashMap<usize, &Comment>) {
1590    for TableChild::TableRow(row) in &mut table.rows {
1591        for TableRowChild::TableCell(cell) in &mut row.cells {
1592            for content in &mut cell.children {
1593                match content {
1594                    TableCellContent::Paragraph(paragraph) => {
1595                        store_comments_in_paragraph(paragraph, comments_by_id)
1596                    }
1597                    TableCellContent::Table(ref mut table) => {
1598                        store_comments_in_table(table, comments_by_id);
1599                    }
1600                    TableCellContent::StructuredDataTag(ref mut tag) => {
1601                        for child in &mut tag.children {
1602                            if let StructuredDataTagChild::Paragraph(paragraph) = child {
1603                                store_comments_in_paragraph(paragraph, comments_by_id);
1604                            }
1605                            if let StructuredDataTagChild::Table(table) = child {
1606                                store_comments_in_table(table, comments_by_id);
1607                            }
1608                        }
1609                    }
1610                    TableCellContent::TableOfContents(ref mut t) => {
1611                        for child in &mut t.before_contents {
1612                            if let TocContent::Paragraph(paragraph) = child {
1613                                store_comments_in_paragraph(paragraph, comments_by_id);
1614                            }
1615                            if let TocContent::Table(table) = child {
1616                                store_comments_in_table(table, comments_by_id);
1617                            }
1618                        }
1619
1620                        for child in &mut t.after_contents {
1621                            if let TocContent::Paragraph(paragraph) = child {
1622                                store_comments_in_paragraph(paragraph, comments_by_id);
1623                            }
1624                            if let TocContent::Table(table) = child {
1625                                store_comments_in_table(table, comments_by_id);
1626                            }
1627                        }
1628                    }
1629                }
1630            }
1631        }
1632    }
1633}
1634
1635fn collect_para_ids_in_docx<'a>(docx: &'a Docx, counts: &mut HashMap<&'a str, usize>) {
1636    for child in &docx.document.children {
1637        collect_para_ids_in_document_child(child, counts);
1638    }
1639    collect_para_ids_in_section_property(&docx.document.section_property, counts);
1640
1641    for comment in &docx.comments.comments {
1642        collect_para_ids_in_comment(comment, counts);
1643    }
1644
1645    for footnote in &docx.footnotes.footnotes {
1646        for paragraph in &footnote.content {
1647            collect_para_ids_in_paragraph(paragraph, counts);
1648        }
1649    }
1650}
1651
1652fn collect_para_ids_in_document_child<'a>(
1653    child: &'a DocumentChild,
1654    counts: &mut HashMap<&'a str, usize>,
1655) {
1656    match child {
1657        DocumentChild::Paragraph(paragraph) => collect_para_ids_in_paragraph(paragraph, counts),
1658        DocumentChild::Table(table) => collect_para_ids_in_table(table, counts),
1659        DocumentChild::StructuredDataTag(tag) => {
1660            collect_para_ids_in_structured_data_tag(tag, counts)
1661        }
1662        DocumentChild::TableOfContents(toc) => collect_para_ids_in_toc(toc, counts),
1663        DocumentChild::Section(section) => collect_para_ids_in_section(section, counts),
1664        DocumentChild::CommentStart(c) => collect_para_ids_in_comment(&c.comment, counts),
1665        _ => {}
1666    }
1667}
1668
1669fn collect_para_ids_in_section<'a>(section: &'a Section, counts: &mut HashMap<&'a str, usize>) {
1670    for child in &section.children {
1671        match child {
1672            SectionChild::Paragraph(paragraph) => collect_para_ids_in_paragraph(paragraph, counts),
1673            SectionChild::Table(table) => collect_para_ids_in_table(table, counts),
1674            SectionChild::StructuredDataTag(tag) => {
1675                collect_para_ids_in_structured_data_tag(tag, counts)
1676            }
1677            SectionChild::TableOfContents(toc) => collect_para_ids_in_toc(toc, counts),
1678            SectionChild::CommentStart(c) => collect_para_ids_in_comment(&c.comment, counts),
1679            _ => {}
1680        }
1681    }
1682    collect_para_ids_in_section_property(&section.property, counts);
1683}
1684
1685fn collect_para_ids_in_section_property<'a>(
1686    property: &'a SectionProperty,
1687    counts: &mut HashMap<&'a str, usize>,
1688) {
1689    if let Some((_, header)) = property.header.as_ref() {
1690        collect_para_ids_in_header(header, counts);
1691    }
1692    if let Some((_, header)) = property.first_header.as_ref() {
1693        collect_para_ids_in_header(header, counts);
1694    }
1695    if let Some((_, header)) = property.even_header.as_ref() {
1696        collect_para_ids_in_header(header, counts);
1697    }
1698    if let Some((_, footer)) = property.footer.as_ref() {
1699        collect_para_ids_in_footer(footer, counts);
1700    }
1701    if let Some((_, footer)) = property.first_footer.as_ref() {
1702        collect_para_ids_in_footer(footer, counts);
1703    }
1704    if let Some((_, footer)) = property.even_footer.as_ref() {
1705        collect_para_ids_in_footer(footer, counts);
1706    }
1707}
1708
1709fn collect_para_ids_in_header<'a>(header: &'a Header, counts: &mut HashMap<&'a str, usize>) {
1710    for child in &header.children {
1711        match child {
1712            HeaderChild::Paragraph(paragraph) => collect_para_ids_in_paragraph(paragraph, counts),
1713            HeaderChild::Table(table) => collect_para_ids_in_table(table, counts),
1714            HeaderChild::StructuredDataTag(tag) => {
1715                collect_para_ids_in_structured_data_tag(tag, counts)
1716            }
1717        }
1718    }
1719}
1720
1721fn collect_para_ids_in_footer<'a>(footer: &'a Footer, counts: &mut HashMap<&'a str, usize>) {
1722    for child in &footer.children {
1723        match child {
1724            FooterChild::Paragraph(paragraph) => collect_para_ids_in_paragraph(paragraph, counts),
1725            FooterChild::Table(table) => collect_para_ids_in_table(table, counts),
1726            FooterChild::StructuredDataTag(tag) => {
1727                collect_para_ids_in_structured_data_tag(tag, counts)
1728            }
1729        }
1730    }
1731}
1732
1733fn collect_para_ids_in_toc<'a>(toc: &'a TableOfContents, counts: &mut HashMap<&'a str, usize>) {
1734    for child in &toc.before_contents {
1735        match child {
1736            TocContent::Paragraph(paragraph) => collect_para_ids_in_paragraph(paragraph, counts),
1737            TocContent::Table(table) => collect_para_ids_in_table(table, counts),
1738        }
1739    }
1740    for child in &toc.after_contents {
1741        match child {
1742            TocContent::Paragraph(paragraph) => collect_para_ids_in_paragraph(paragraph, counts),
1743            TocContent::Table(table) => collect_para_ids_in_table(table, counts),
1744        }
1745    }
1746}
1747
1748fn collect_para_ids_in_table<'a>(table: &'a Table, counts: &mut HashMap<&'a str, usize>) {
1749    for TableChild::TableRow(row) in &table.rows {
1750        for TableRowChild::TableCell(cell) in &row.cells {
1751            for content in &cell.children {
1752                match content {
1753                    TableCellContent::Paragraph(paragraph) => {
1754                        collect_para_ids_in_paragraph(paragraph, counts)
1755                    }
1756                    TableCellContent::Table(table) => collect_para_ids_in_table(table, counts),
1757                    TableCellContent::StructuredDataTag(tag) => {
1758                        collect_para_ids_in_structured_data_tag(tag, counts)
1759                    }
1760                    TableCellContent::TableOfContents(toc) => collect_para_ids_in_toc(toc, counts),
1761                }
1762            }
1763        }
1764    }
1765}
1766
1767fn collect_para_ids_in_paragraph<'a>(
1768    paragraph: &'a Paragraph,
1769    counts: &mut HashMap<&'a str, usize>,
1770) {
1771    *counts.entry(paragraph.id.as_str()).or_insert(0) += 1;
1772
1773    for child in &paragraph.children {
1774        match child {
1775            ParagraphChild::Run(run) => collect_para_ids_in_run(run, counts),
1776            ParagraphChild::CommentStart(c) => collect_para_ids_in_comment(&c.comment, counts),
1777            ParagraphChild::Insert(insert) => collect_para_ids_in_insert(insert, counts),
1778            ParagraphChild::Delete(delete) => collect_para_ids_in_delete(delete, counts),
1779            ParagraphChild::MoveFrom(moved) => collect_para_ids_in_move_from(moved, counts),
1780            ParagraphChild::MoveTo(moved) => collect_para_ids_in_move_to(moved, counts),
1781            ParagraphChild::Hyperlink(hyperlink) => {
1782                collect_para_ids_in_hyperlink(hyperlink, counts)
1783            }
1784            ParagraphChild::StructuredDataTag(tag) => {
1785                collect_para_ids_in_structured_data_tag(tag, counts)
1786            }
1787            _ => {}
1788        }
1789    }
1790}
1791
1792fn collect_para_ids_in_hyperlink<'a>(
1793    hyperlink: &'a Hyperlink,
1794    counts: &mut HashMap<&'a str, usize>,
1795) {
1796    for child in &hyperlink.children {
1797        match child {
1798            ParagraphChild::Run(run) => collect_para_ids_in_run(run, counts),
1799            ParagraphChild::CommentStart(c) => collect_para_ids_in_comment(&c.comment, counts),
1800            ParagraphChild::Insert(insert) => collect_para_ids_in_insert(insert, counts),
1801            ParagraphChild::Delete(delete) => collect_para_ids_in_delete(delete, counts),
1802            ParagraphChild::MoveFrom(moved) => collect_para_ids_in_move_from(moved, counts),
1803            ParagraphChild::MoveTo(moved) => collect_para_ids_in_move_to(moved, counts),
1804            ParagraphChild::Hyperlink(hyperlink) => {
1805                collect_para_ids_in_hyperlink(hyperlink, counts)
1806            }
1807            ParagraphChild::StructuredDataTag(tag) => {
1808                collect_para_ids_in_structured_data_tag(tag, counts)
1809            }
1810            _ => {}
1811        }
1812    }
1813}
1814
1815fn collect_para_ids_in_insert<'a>(insert: &'a Insert, counts: &mut HashMap<&'a str, usize>) {
1816    for child in &insert.children {
1817        match child {
1818            InsertChild::Run(run) => collect_para_ids_in_run(run, counts),
1819            InsertChild::CommentStart(c) => collect_para_ids_in_comment(&c.comment, counts),
1820            InsertChild::Delete(delete) => collect_para_ids_in_delete(delete, counts),
1821            _ => {}
1822        }
1823    }
1824}
1825
1826fn collect_para_ids_in_delete<'a>(delete: &'a Delete, counts: &mut HashMap<&'a str, usize>) {
1827    for child in &delete.children {
1828        match child {
1829            DeleteChild::Run(run) => collect_para_ids_in_run(run, counts),
1830            DeleteChild::CommentStart(c) => collect_para_ids_in_comment(&c.comment, counts),
1831            DeleteChild::CommentEnd(_) => {}
1832        }
1833    }
1834}
1835
1836fn collect_para_ids_in_move_from<'a>(moved: &'a MoveFrom, counts: &mut HashMap<&'a str, usize>) {
1837    for child in &moved.children {
1838        match child {
1839            MoveFromChild::Run(run) => collect_para_ids_in_run(run, counts),
1840            MoveFromChild::CommentStart(c) => collect_para_ids_in_comment(&c.comment, counts),
1841            MoveFromChild::CommentEnd(_) => {}
1842        }
1843    }
1844}
1845
1846fn collect_para_ids_in_move_to<'a>(moved: &'a MoveTo, counts: &mut HashMap<&'a str, usize>) {
1847    for child in &moved.children {
1848        match child {
1849            MoveToChild::Run(run) => collect_para_ids_in_run(run, counts),
1850            MoveToChild::Delete(delete) => collect_para_ids_in_delete(delete, counts),
1851            MoveToChild::CommentStart(c) => collect_para_ids_in_comment(&c.comment, counts),
1852            _ => {}
1853        }
1854    }
1855}
1856
1857fn collect_para_ids_in_run<'a>(run: &'a Run, counts: &mut HashMap<&'a str, usize>) {
1858    for child in &run.children {
1859        if let RunChild::CommentStart(c) = child {
1860            collect_para_ids_in_comment(&c.comment, counts);
1861        }
1862    }
1863}
1864
1865fn collect_para_ids_in_structured_data_tag<'a>(
1866    tag: &'a StructuredDataTag,
1867    counts: &mut HashMap<&'a str, usize>,
1868) {
1869    for child in &tag.children {
1870        match child {
1871            StructuredDataTagChild::Run(run) => collect_para_ids_in_run(run, counts),
1872            StructuredDataTagChild::Paragraph(paragraph) => {
1873                collect_para_ids_in_paragraph(paragraph, counts)
1874            }
1875            StructuredDataTagChild::Table(table) => collect_para_ids_in_table(table, counts),
1876            StructuredDataTagChild::CommentStart(c) => {
1877                collect_para_ids_in_comment(&c.comment, counts)
1878            }
1879            StructuredDataTagChild::StructuredDataTag(inner) => {
1880                collect_para_ids_in_structured_data_tag(inner, counts)
1881            }
1882            _ => {}
1883        }
1884    }
1885}
1886
1887fn collect_para_ids_in_comment<'a>(comment: &'a Comment, counts: &mut HashMap<&'a str, usize>) {
1888    for child in &comment.children {
1889        match child {
1890            CommentChild::Paragraph(paragraph) => collect_para_ids_in_paragraph(paragraph, counts),
1891            CommentChild::Table(table) => collect_para_ids_in_table(table, counts),
1892        }
1893    }
1894}
1895
1896fn refresh_para_ids_in_docx(
1897    docx: &mut Docx,
1898    duplicates: &HashSet<String>,
1899    used: &mut HashSet<String>,
1900    seen: &mut HashSet<String>,
1901) {
1902    for child in &mut docx.document.children {
1903        refresh_para_ids_in_document_child(child, duplicates, used, seen);
1904    }
1905    refresh_para_ids_in_section_property(
1906        &mut docx.document.section_property,
1907        duplicates,
1908        used,
1909        seen,
1910    );
1911
1912    for comment in &mut docx.comments.comments {
1913        refresh_para_ids_in_comment(comment, duplicates, used, seen);
1914    }
1915
1916    for footnote in &mut docx.footnotes.footnotes {
1917        for paragraph in &mut footnote.content {
1918            refresh_para_ids_in_paragraph(paragraph, duplicates, used, seen);
1919        }
1920    }
1921}
1922
1923fn refresh_para_ids_in_document_child(
1924    child: &mut DocumentChild,
1925    duplicates: &HashSet<String>,
1926    used: &mut HashSet<String>,
1927    seen: &mut HashSet<String>,
1928) {
1929    match child {
1930        DocumentChild::Paragraph(paragraph) => {
1931            refresh_para_ids_in_paragraph(paragraph, duplicates, used, seen)
1932        }
1933        DocumentChild::Table(table) => refresh_para_ids_in_table(table, duplicates, used, seen),
1934        DocumentChild::StructuredDataTag(tag) => {
1935            refresh_para_ids_in_structured_data_tag(tag, duplicates, used, seen)
1936        }
1937        DocumentChild::TableOfContents(toc) => refresh_para_ids_in_toc(toc, duplicates, used, seen),
1938        DocumentChild::Section(section) => {
1939            refresh_para_ids_in_section(section, duplicates, used, seen)
1940        }
1941        DocumentChild::CommentStart(c) => {
1942            refresh_para_ids_in_comment(&mut c.comment, duplicates, used, seen)
1943        }
1944        _ => {}
1945    }
1946}
1947
1948fn refresh_para_ids_in_section(
1949    section: &mut Section,
1950    duplicates: &HashSet<String>,
1951    used: &mut HashSet<String>,
1952    seen: &mut HashSet<String>,
1953) {
1954    for child in &mut section.children {
1955        match child {
1956            SectionChild::Paragraph(paragraph) => {
1957                refresh_para_ids_in_paragraph(paragraph, duplicates, used, seen)
1958            }
1959            SectionChild::Table(table) => refresh_para_ids_in_table(table, duplicates, used, seen),
1960            SectionChild::StructuredDataTag(tag) => {
1961                refresh_para_ids_in_structured_data_tag(tag, duplicates, used, seen)
1962            }
1963            SectionChild::TableOfContents(toc) => {
1964                refresh_para_ids_in_toc(toc, duplicates, used, seen)
1965            }
1966            SectionChild::CommentStart(c) => {
1967                refresh_para_ids_in_comment(&mut c.comment, duplicates, used, seen)
1968            }
1969            _ => {}
1970        }
1971    }
1972    refresh_para_ids_in_section_property(&mut section.property, duplicates, used, seen);
1973}
1974
1975fn refresh_para_ids_in_section_property(
1976    property: &mut SectionProperty,
1977    duplicates: &HashSet<String>,
1978    used: &mut HashSet<String>,
1979    seen: &mut HashSet<String>,
1980) {
1981    if let Some((_, header)) = property.header.as_mut() {
1982        refresh_para_ids_in_header(header, duplicates, used, seen);
1983    }
1984    if let Some((_, header)) = property.first_header.as_mut() {
1985        refresh_para_ids_in_header(header, duplicates, used, seen);
1986    }
1987    if let Some((_, header)) = property.even_header.as_mut() {
1988        refresh_para_ids_in_header(header, duplicates, used, seen);
1989    }
1990    if let Some((_, footer)) = property.footer.as_mut() {
1991        refresh_para_ids_in_footer(footer, duplicates, used, seen);
1992    }
1993    if let Some((_, footer)) = property.first_footer.as_mut() {
1994        refresh_para_ids_in_footer(footer, duplicates, used, seen);
1995    }
1996    if let Some((_, footer)) = property.even_footer.as_mut() {
1997        refresh_para_ids_in_footer(footer, duplicates, used, seen);
1998    }
1999}
2000
2001fn refresh_para_ids_in_header(
2002    header: &mut Header,
2003    duplicates: &HashSet<String>,
2004    used: &mut HashSet<String>,
2005    seen: &mut HashSet<String>,
2006) {
2007    for child in &mut header.children {
2008        match child {
2009            HeaderChild::Paragraph(paragraph) => {
2010                refresh_para_ids_in_paragraph(paragraph, duplicates, used, seen)
2011            }
2012            HeaderChild::Table(table) => refresh_para_ids_in_table(table, duplicates, used, seen),
2013            HeaderChild::StructuredDataTag(tag) => {
2014                refresh_para_ids_in_structured_data_tag(tag, duplicates, used, seen)
2015            }
2016        }
2017    }
2018}
2019
2020fn refresh_para_ids_in_footer(
2021    footer: &mut Footer,
2022    duplicates: &HashSet<String>,
2023    used: &mut HashSet<String>,
2024    seen: &mut HashSet<String>,
2025) {
2026    for child in &mut footer.children {
2027        match child {
2028            FooterChild::Paragraph(paragraph) => {
2029                refresh_para_ids_in_paragraph(paragraph, duplicates, used, seen)
2030            }
2031            FooterChild::Table(table) => refresh_para_ids_in_table(table, duplicates, used, seen),
2032            FooterChild::StructuredDataTag(tag) => {
2033                refresh_para_ids_in_structured_data_tag(tag, duplicates, used, seen)
2034            }
2035        }
2036    }
2037}
2038
2039fn refresh_para_ids_in_toc(
2040    toc: &mut TableOfContents,
2041    duplicates: &HashSet<String>,
2042    used: &mut HashSet<String>,
2043    seen: &mut HashSet<String>,
2044) {
2045    for child in &mut toc.before_contents {
2046        match child {
2047            TocContent::Paragraph(paragraph) => {
2048                refresh_para_ids_in_paragraph(paragraph, duplicates, used, seen)
2049            }
2050            TocContent::Table(table) => refresh_para_ids_in_table(table, duplicates, used, seen),
2051        }
2052    }
2053    for child in &mut toc.after_contents {
2054        match child {
2055            TocContent::Paragraph(paragraph) => {
2056                refresh_para_ids_in_paragraph(paragraph, duplicates, used, seen)
2057            }
2058            TocContent::Table(table) => refresh_para_ids_in_table(table, duplicates, used, seen),
2059        }
2060    }
2061}
2062
2063fn refresh_para_ids_in_table(
2064    table: &mut Table,
2065    duplicates: &HashSet<String>,
2066    used: &mut HashSet<String>,
2067    seen: &mut HashSet<String>,
2068) {
2069    for TableChild::TableRow(row) in &mut table.rows {
2070        for TableRowChild::TableCell(cell) in &mut row.cells {
2071            for content in &mut cell.children {
2072                match content {
2073                    TableCellContent::Paragraph(paragraph) => {
2074                        refresh_para_ids_in_paragraph(paragraph, duplicates, used, seen)
2075                    }
2076                    TableCellContent::Table(table) => {
2077                        refresh_para_ids_in_table(table, duplicates, used, seen)
2078                    }
2079                    TableCellContent::StructuredDataTag(tag) => {
2080                        refresh_para_ids_in_structured_data_tag(tag, duplicates, used, seen)
2081                    }
2082                    TableCellContent::TableOfContents(toc) => {
2083                        refresh_para_ids_in_toc(toc, duplicates, used, seen)
2084                    }
2085                }
2086            }
2087        }
2088    }
2089}
2090
2091fn refresh_para_ids_in_paragraph(
2092    paragraph: &mut Paragraph,
2093    duplicates: &HashSet<String>,
2094    used: &mut HashSet<String>,
2095    seen: &mut HashSet<String>,
2096) {
2097    ensure_unique_paragraph_id(paragraph, duplicates, used, seen);
2098
2099    for child in &mut paragraph.children {
2100        match child {
2101            ParagraphChild::Run(run) => refresh_para_ids_in_run(run, duplicates, used, seen),
2102            ParagraphChild::CommentStart(c) => {
2103                refresh_para_ids_in_comment(&mut c.comment, duplicates, used, seen)
2104            }
2105            ParagraphChild::Insert(insert) => {
2106                refresh_para_ids_in_insert(insert, duplicates, used, seen)
2107            }
2108            ParagraphChild::Delete(delete) => {
2109                refresh_para_ids_in_delete(delete, duplicates, used, seen)
2110            }
2111            ParagraphChild::MoveFrom(moved) => {
2112                refresh_para_ids_in_move_from(moved, duplicates, used, seen)
2113            }
2114            ParagraphChild::MoveTo(moved) => {
2115                refresh_para_ids_in_move_to(moved, duplicates, used, seen)
2116            }
2117            ParagraphChild::Hyperlink(hyperlink) => {
2118                refresh_para_ids_in_hyperlink(hyperlink, duplicates, used, seen)
2119            }
2120            ParagraphChild::StructuredDataTag(tag) => {
2121                refresh_para_ids_in_structured_data_tag(tag, duplicates, used, seen)
2122            }
2123            _ => {}
2124        }
2125    }
2126}
2127
2128fn refresh_para_ids_in_hyperlink(
2129    hyperlink: &mut Hyperlink,
2130    duplicates: &HashSet<String>,
2131    used: &mut HashSet<String>,
2132    seen: &mut HashSet<String>,
2133) {
2134    for child in &mut hyperlink.children {
2135        match child {
2136            ParagraphChild::Run(run) => refresh_para_ids_in_run(run, duplicates, used, seen),
2137            ParagraphChild::CommentStart(c) => {
2138                refresh_para_ids_in_comment(&mut c.comment, duplicates, used, seen)
2139            }
2140            ParagraphChild::Insert(insert) => {
2141                refresh_para_ids_in_insert(insert, duplicates, used, seen)
2142            }
2143            ParagraphChild::Delete(delete) => {
2144                refresh_para_ids_in_delete(delete, duplicates, used, seen)
2145            }
2146            ParagraphChild::MoveFrom(moved) => {
2147                refresh_para_ids_in_move_from(moved, duplicates, used, seen)
2148            }
2149            ParagraphChild::MoveTo(moved) => {
2150                refresh_para_ids_in_move_to(moved, duplicates, used, seen)
2151            }
2152            ParagraphChild::Hyperlink(hyperlink) => {
2153                refresh_para_ids_in_hyperlink(hyperlink, duplicates, used, seen)
2154            }
2155            ParagraphChild::StructuredDataTag(tag) => {
2156                refresh_para_ids_in_structured_data_tag(tag, duplicates, used, seen)
2157            }
2158            _ => {}
2159        }
2160    }
2161}
2162
2163fn refresh_para_ids_in_insert(
2164    insert: &mut Insert,
2165    duplicates: &HashSet<String>,
2166    used: &mut HashSet<String>,
2167    seen: &mut HashSet<String>,
2168) {
2169    for child in &mut insert.children {
2170        match child {
2171            InsertChild::Run(run) => refresh_para_ids_in_run(run, duplicates, used, seen),
2172            InsertChild::CommentStart(c) => {
2173                refresh_para_ids_in_comment(&mut c.comment, duplicates, used, seen)
2174            }
2175            InsertChild::Delete(delete) => {
2176                refresh_para_ids_in_delete(delete, duplicates, used, seen)
2177            }
2178            _ => {}
2179        }
2180    }
2181}
2182
2183fn refresh_para_ids_in_delete(
2184    delete: &mut Delete,
2185    duplicates: &HashSet<String>,
2186    used: &mut HashSet<String>,
2187    seen: &mut HashSet<String>,
2188) {
2189    for child in &mut delete.children {
2190        match child {
2191            DeleteChild::Run(run) => refresh_para_ids_in_run(run, duplicates, used, seen),
2192            DeleteChild::CommentStart(c) => {
2193                refresh_para_ids_in_comment(&mut c.comment, duplicates, used, seen)
2194            }
2195            DeleteChild::CommentEnd(_) => {}
2196        }
2197    }
2198}
2199
2200fn refresh_para_ids_in_move_from(
2201    moved: &mut MoveFrom,
2202    duplicates: &HashSet<String>,
2203    used: &mut HashSet<String>,
2204    seen: &mut HashSet<String>,
2205) {
2206    for child in &mut moved.children {
2207        match child {
2208            MoveFromChild::Run(run) => refresh_para_ids_in_run(run, duplicates, used, seen),
2209            MoveFromChild::CommentStart(c) => {
2210                refresh_para_ids_in_comment(&mut c.comment, duplicates, used, seen)
2211            }
2212            MoveFromChild::CommentEnd(_) => {}
2213        }
2214    }
2215}
2216
2217fn refresh_para_ids_in_move_to(
2218    moved: &mut MoveTo,
2219    duplicates: &HashSet<String>,
2220    used: &mut HashSet<String>,
2221    seen: &mut HashSet<String>,
2222) {
2223    for child in &mut moved.children {
2224        match child {
2225            MoveToChild::Run(run) => refresh_para_ids_in_run(run, duplicates, used, seen),
2226            MoveToChild::Delete(delete) => {
2227                refresh_para_ids_in_delete(delete, duplicates, used, seen)
2228            }
2229            MoveToChild::CommentStart(c) => {
2230                refresh_para_ids_in_comment(&mut c.comment, duplicates, used, seen)
2231            }
2232            _ => {}
2233        }
2234    }
2235}
2236
2237fn refresh_para_ids_in_run(
2238    run: &mut Run,
2239    duplicates: &HashSet<String>,
2240    used: &mut HashSet<String>,
2241    seen: &mut HashSet<String>,
2242) {
2243    for child in &mut run.children {
2244        if let RunChild::CommentStart(c) = child {
2245            refresh_para_ids_in_comment(&mut c.comment, duplicates, used, seen);
2246        }
2247    }
2248}
2249
2250fn refresh_para_ids_in_structured_data_tag(
2251    tag: &mut StructuredDataTag,
2252    duplicates: &HashSet<String>,
2253    used: &mut HashSet<String>,
2254    seen: &mut HashSet<String>,
2255) {
2256    for child in &mut tag.children {
2257        match child {
2258            StructuredDataTagChild::Run(run) => {
2259                refresh_para_ids_in_run(run, duplicates, used, seen)
2260            }
2261            StructuredDataTagChild::Paragraph(paragraph) => {
2262                refresh_para_ids_in_paragraph(paragraph, duplicates, used, seen)
2263            }
2264            StructuredDataTagChild::Table(table) => {
2265                refresh_para_ids_in_table(table, duplicates, used, seen)
2266            }
2267            StructuredDataTagChild::CommentStart(c) => {
2268                refresh_para_ids_in_comment(&mut c.comment, duplicates, used, seen)
2269            }
2270            StructuredDataTagChild::StructuredDataTag(inner) => {
2271                refresh_para_ids_in_structured_data_tag(inner, duplicates, used, seen)
2272            }
2273            _ => {}
2274        }
2275    }
2276}
2277
2278fn refresh_para_ids_in_comment(
2279    comment: &mut Comment,
2280    duplicates: &HashSet<String>,
2281    used: &mut HashSet<String>,
2282    seen: &mut HashSet<String>,
2283) {
2284    for child in &mut comment.children {
2285        match child {
2286            CommentChild::Paragraph(paragraph) => {
2287                refresh_para_ids_in_paragraph(paragraph, duplicates, used, seen)
2288            }
2289            CommentChild::Table(table) => refresh_para_ids_in_table(table, duplicates, used, seen),
2290        }
2291    }
2292}
2293
2294fn ensure_unique_paragraph_id(
2295    paragraph: &mut Paragraph,
2296    duplicates: &HashSet<String>,
2297    used: &mut HashSet<String>,
2298    seen: &mut HashSet<String>,
2299) {
2300    if !paragraph.id.is_empty() && !duplicates.contains(&paragraph.id) {
2301        return;
2302    }
2303    if !paragraph.id.is_empty() && seen.insert(paragraph.id.clone()) {
2304        return;
2305    }
2306
2307    let new_id = next_unique_paragraph_id(used);
2308    paragraph.id = new_id.clone();
2309    used.insert(new_id);
2310}
2311
2312/// Returns a generated paragraph ID, falling back to a deterministic scan if
2313/// the generator collides with an existing value.
2314fn next_unique_paragraph_id(used: &HashSet<String>) -> String {
2315    let generated = crate::generate_para_id();
2316    if !used.contains(&generated) {
2317        return generated;
2318    }
2319
2320    (1usize..)
2321        .map(|value| format!("{value:08x}"))
2322        .find(|candidate| !used.contains(candidate))
2323        .expect("the paragraph ID space should not be exhausted")
2324}
2325
2326fn push_comment_and_comment_extended(
2327    comments: &mut Vec<Comment>,
2328    comments_extended: &mut Vec<CommentExtended>,
2329    comment_map: &HashMap<usize, String>,
2330    c: &CommentRangeStart,
2331) {
2332    let comment = c.get_comment_ref();
2333    for child in &comment.children {
2334        if let CommentChild::Paragraph(child) = child {
2335            let para_id = child.id.clone();
2336            comments.push(comment.clone());
2337            let comment_extended = CommentExtended::new(para_id);
2338            if let Some(parent_comment_id) = comment.parent_comment_id {
2339                if let Some(parent_para_id) = comment_map.get(&parent_comment_id) {
2340                    comments_extended
2341                        .push(comment_extended.parent_paragraph_id(parent_para_id.clone()));
2342                }
2343            } else {
2344                comments_extended.push(comment_extended);
2345            }
2346        }
2347        // TODO: Support table in comment
2348    }
2349}
2350
2351fn update_document_by_toc(
2352    document_children: Vec<DocumentChild>,
2353    styles: &Styles,
2354    toc: TableOfContents,
2355    toc_index: usize,
2356) -> Vec<DocumentChild> {
2357    let heading_map = styles.create_heading_style_map();
2358    let mut items = vec![];
2359    let mut children = vec![];
2360    let style_map: std::collections::HashMap<String, usize> = toc
2361        .instr
2362        .styles_with_levels
2363        .iter()
2364        .map(|sl| sl.0.clone())
2365        .collect();
2366
2367    if toc.instr.heading_styles_range.is_none() && !toc.instr.styles_with_levels.is_empty() {
2368        // INFO: if \t option set without heading styles ranges, Microsoft word does not show ToC items...
2369        return document_children;
2370    }
2371
2372    let (min, max) = toc.instr.heading_styles_range.unwrap_or((0, 9));
2373
2374    for child in document_children.into_iter() {
2375        match child {
2376            DocumentChild::Paragraph(mut paragraph) => {
2377                if let Some(heading_level) = paragraph
2378                    .property
2379                    .style
2380                    .as_ref()
2381                    .map(|p| p.val.to_string())
2382                    .and_then(|sid| heading_map.get(&sid))
2383                {
2384                    if min <= *heading_level && max >= *heading_level {
2385                        let toc_key = TocKey::generate();
2386                        items.push(
2387                            TableOfContentsItem::new()
2388                                .text(paragraph.raw_text())
2389                                .toc_key(&toc_key)
2390                                .level(*heading_level),
2391                        );
2392                        paragraph =
2393                            Box::new(paragraph.wrap_by_bookmark(generate_bookmark_id(), &toc_key));
2394                    }
2395
2396                    if let Some((_min, _max)) = toc.instr.tc_field_level_range {
2397                        // TODO: check tc field
2398                    }
2399                }
2400
2401                // Support \t option. Collect toc items if style id matched.
2402                if let Some(level) = paragraph
2403                    .property
2404                    .style
2405                    .as_ref()
2406                    .and_then(|s| style_map.get(&s.val))
2407                {
2408                    if min <= *level && max >= *level {
2409                        let toc_key = TocKey::generate();
2410                        items.push(
2411                            TableOfContentsItem::new()
2412                                .text(paragraph.raw_text())
2413                                .toc_key(&toc_key)
2414                                .level(*level),
2415                        );
2416                        paragraph =
2417                            Box::new(paragraph.wrap_by_bookmark(generate_bookmark_id(), &toc_key));
2418                    }
2419                }
2420
2421                children.push(DocumentChild::Paragraph(paragraph));
2422            }
2423            DocumentChild::Table(ref _table) => {
2424                // TODO:
2425                // for row in &table.rows {
2426                //     for cell in &row.cells {
2427                //         for content in &cell.children {
2428                //             match content {
2429                //                 TableCellContent::Paragraph(paragraph) => {}
2430                //                 TableCellContent::Table(_) => {
2431                //                     // TODO: Support table in table
2432                //                 }
2433                //             }
2434                //         }
2435                //     }
2436                // }
2437                children.push(child);
2438            }
2439            _ => {
2440                children.push(child);
2441            }
2442        }
2443    }
2444
2445    let mut toc = toc;
2446    toc.items = items;
2447    children[toc_index] = DocumentChild::TableOfContents(Box::new(toc));
2448    children
2449}
2450
2451#[cfg(test)]
2452mod image_collection_tests {
2453    use super::*;
2454
2455    fn picture(id: &str, bytes: Vec<u8>) -> Pic {
2456        let mut picture = Pic::new_with_dimensions(bytes, 1, 1);
2457        picture.id = id.to_owned();
2458        picture
2459    }
2460
2461    fn image_paragraph(id: &str, bytes: Vec<u8>) -> Paragraph {
2462        Paragraph::new().add_run(Run::new().add_image(picture(id, bytes)))
2463    }
2464
2465    #[test]
2466    fn structured_footer_images_use_footer_relationship_ids() {
2467        let tag = StructuredDataTag::new().add_paragraph(
2468            Paragraph::new().add_run(Run::new().add_image(Pic::new_with_dimensions(
2469                vec![1, 2, 3],
2470                1,
2471                1,
2472            ))),
2473        );
2474        let footer = Footer::new().add_structured_data_tag(tag);
2475        let xml = Docx::new().footer(footer).build();
2476
2477        assert!(String::from_utf8_lossy(&xml.footer_rels[0]).contains("Id=\"footer"));
2478        assert!(xml.media[0].0.starts_with("footer"));
2479    }
2480
2481    #[test]
2482    fn repeated_header_images_keep_relationships_in_each_part() {
2483        let bytes = vec![1, 2, 3, 4];
2484        let xml = Docx::new()
2485            .header(Header::new().add_paragraph(image_paragraph("default", bytes.clone())))
2486            .first_header(Header::new().add_paragraph(image_paragraph("first", bytes)))
2487            .build();
2488
2489        assert_eq!(xml.media.len(), 1);
2490        assert!(String::from_utf8_lossy(&xml.header_rels[0]).contains("relationships/image"));
2491        assert!(String::from_utf8_lossy(&xml.header_rels[1]).contains("relationships/image"));
2492    }
2493
2494    #[test]
2495    fn repeated_images_share_one_media_entry_across_package_parts() {
2496        let bytes = vec![5, 6, 7, 8];
2497        let xml = Docx::new()
2498            .add_paragraph(image_paragraph("body", bytes.clone()))
2499            .header(Header::new().add_paragraph(image_paragraph("header", bytes.clone())))
2500            .footer(Footer::new().add_paragraph(image_paragraph("footer", bytes)))
2501            .build();
2502
2503        assert_eq!(xml.media.len(), 1);
2504        assert!(String::from_utf8_lossy(&xml.document_rels).contains("relationships/image"));
2505        assert!(String::from_utf8_lossy(&xml.header_rels[0]).contains("relationships/image"));
2506        assert!(String::from_utf8_lossy(&xml.footer_rels[0]).contains("relationships/image"));
2507    }
2508
2509    #[test]
2510    fn images_are_collected_from_top_level_sdt_and_section_content() {
2511        let tag = StructuredDataTag::new().add_paragraph(image_paragraph("sdt", vec![9]));
2512        let section = Section::new().add_paragraph(image_paragraph("section", vec![10]));
2513        let xml = Docx::new()
2514            .add_structured_data_tag(tag)
2515            .add_section(section)
2516            .build();
2517
2518        assert_eq!(xml.media.len(), 2);
2519        assert_eq!(
2520            String::from_utf8_lossy(&xml.document_rels)
2521                .matches("relationships/image")
2522                .count(),
2523            2
2524        );
2525    }
2526}
2527
2528#[cfg(test)]
2529mod document_traversal_tests {
2530    use super::*;
2531
2532    #[test]
2533    fn footnotes_are_collected_from_table_paragraphs() {
2534        let footnote = Footnote::new()
2535            .add_content(Paragraph::new().add_run(Run::new().add_text("nested footnote")));
2536        let table = Table::new(vec![TableRow::new(vec![TableCell::new().add_paragraph(
2537            Paragraph::new().add_run(Run::new().add_footnote_reference(footnote)),
2538        )])]);
2539
2540        let xml = Docx::new().add_table(table).build();
2541
2542        assert!(String::from_utf8_lossy(&xml.footnotes).contains("nested footnote"));
2543        assert!(String::from_utf8_lossy(&xml.document_rels).contains("relationships/footnotes"));
2544    }
2545}
2546
2547#[cfg(test)]
2548mod pack_tests {
2549    use super::*;
2550    use std::io::Cursor;
2551
2552    #[test]
2553    fn direct_pack_matches_buffered_package_output() {
2554        let image = vec![1, 2, 3, 4];
2555        let header = Header::new().add_paragraph(
2556            Paragraph::new().add_run(Run::new().add_image(Pic::new_with_dimensions(
2557                image.clone(),
2558                1,
2559                1,
2560            ))),
2561        );
2562        let footer = Footer::new().add_paragraph(
2563            Paragraph::new().add_run(Run::new().add_image(Pic::new_with_dimensions(image, 1, 1))),
2564        );
2565        let docx = Docx::new()
2566            .header(header)
2567            .footer(footer)
2568            .add_paragraph(Paragraph::new().add_run(Run::new().add_text("streamed package")));
2569
2570        let mut buffered = Cursor::new(Vec::new());
2571        let xml = docx.clone().build();
2572        xml.pack(&mut buffered).unwrap();
2573
2574        let mut streamed = Cursor::new(Vec::new());
2575        docx.pack(&mut streamed).unwrap();
2576
2577        assert_eq!(streamed.into_inner(), buffered.into_inner());
2578    }
2579}
2580
2581#[cfg(test)]
2582mod paragraph_id_tests {
2583    use super::*;
2584
2585    fn paragraph_ids(docx: &Docx) -> Vec<&str> {
2586        docx.document
2587            .children
2588            .iter()
2589            .filter_map(|child| match child {
2590                DocumentChild::Paragraph(paragraph) => Some(paragraph.id.as_str()),
2591                _ => None,
2592            })
2593            .collect()
2594    }
2595
2596    #[test]
2597    fn unique_paragraph_ids_remain_unchanged() {
2598        let mut docx = Docx::new()
2599            .add_paragraph(Paragraph::new().id("first"))
2600            .add_paragraph(Paragraph::new().id("second"));
2601
2602        docx.refresh_duplicate_para_ids();
2603
2604        assert_eq!(paragraph_ids(&docx), ["first", "second"]);
2605    }
2606
2607    #[test]
2608    fn duplicate_paragraph_ids_are_refreshed() {
2609        let mut docx = Docx::new()
2610            .add_paragraph(Paragraph::new().id("duplicate"))
2611            .add_paragraph(Paragraph::new().id("duplicate"));
2612
2613        docx.refresh_duplicate_para_ids();
2614
2615        let ids = paragraph_ids(&docx);
2616        assert_eq!(ids[0], "duplicate");
2617        assert_ne!(ids[1], "duplicate");
2618        assert_eq!(ids.iter().copied().collect::<HashSet<_>>().len(), 2);
2619    }
2620
2621    #[test]
2622    fn empty_paragraph_id_is_refreshed() {
2623        let mut docx = Docx::new().add_paragraph(Paragraph::new().id(""));
2624
2625        docx.refresh_duplicate_para_ids();
2626
2627        assert!(!paragraph_ids(&docx)[0].is_empty());
2628    }
2629
2630    #[test]
2631    fn duplicate_paragraph_ids_in_tracked_change_comments_are_refreshed() {
2632        let moved_from_comment = Comment::new(1).add_paragraph(Paragraph::new().id("duplicate"));
2633        let moved_to_comment = Comment::new(2).add_paragraph(Paragraph::new().id("duplicate"));
2634        let paragraph = Paragraph::new()
2635            .id("outer")
2636            .add_move_from(MoveFrom::new().add_comment_start(moved_from_comment))
2637            .add_move_to(MoveTo::new_with_empty().add_comment_start(moved_to_comment));
2638        let mut docx = Docx::new().add_paragraph(paragraph);
2639
2640        docx.refresh_duplicate_para_ids();
2641
2642        let mut counts = HashMap::new();
2643        collect_para_ids_in_docx(&docx, &mut counts);
2644        assert_eq!(counts.len(), 3);
2645        assert!(counts.values().all(|count| *count == 1));
2646    }
2647
2648    #[test]
2649    fn auto_toc_paragraph_ids_are_normalized_after_expansion() {
2650        let heading = Paragraph::new()
2651            .id("00000001")
2652            .style("Heading1")
2653            .add_run(Run::new().add_text("Heading"));
2654        let mut docx = Docx::new()
2655            .add_style(Style::new("Heading1", crate::StyleType::Paragraph).name("Heading 1"))
2656            .add_table_of_contents(TableOfContents::new().heading_styles_range(1, 3).auto())
2657            .add_paragraph(heading);
2658
2659        docx.prepare_for_build();
2660
2661        let mut counts = HashMap::new();
2662        collect_para_ids_in_docx(&docx, &mut counts);
2663        assert!(!counts.contains_key(""));
2664        assert!(counts.values().all(|count| *count == 1));
2665    }
2666}
2667
2668#[cfg(test)]
2669mod emf_tests {
2670    use super::*;
2671
2672    /// Build a syntactically-valid, minimal EMF: an EMR_HEADER followed by
2673    /// an EMR_EOF.
2674    pub(super) fn minimal_valid_emf() -> Vec<u8> {
2675        let mut buf = Vec::<u8>::with_capacity(108);
2676
2677        // ---- EMR_HEADER (88 bytes) ----
2678        buf.extend_from_slice(&1u32.to_le_bytes()); // record type
2679        buf.extend_from_slice(&88u32.to_le_bytes()); // record size
2680                                                     // Bounds rect (RECTL)
2681        buf.extend_from_slice(&0i32.to_le_bytes());
2682        buf.extend_from_slice(&0i32.to_le_bytes());
2683        buf.extend_from_slice(&100i32.to_le_bytes());
2684        buf.extend_from_slice(&100i32.to_le_bytes());
2685        // Frame rect (RECTL)
2686        buf.extend_from_slice(&0i32.to_le_bytes());
2687        buf.extend_from_slice(&0i32.to_le_bytes());
2688        buf.extend_from_slice(&2540i32.to_le_bytes());
2689        buf.extend_from_slice(&2540i32.to_le_bytes());
2690        // " EMF" signature, version, total bytes, record count
2691        buf.extend_from_slice(&0x464D_4520u32.to_le_bytes());
2692        buf.extend_from_slice(&0x0001_0000u32.to_le_bytes());
2693        buf.extend_from_slice(&108u32.to_le_bytes());
2694        buf.extend_from_slice(&2u32.to_le_bytes());
2695        // handles, reserved, description, palette
2696        buf.extend_from_slice(&0u16.to_le_bytes());
2697        buf.extend_from_slice(&0u16.to_le_bytes());
2698        buf.extend_from_slice(&0u32.to_le_bytes());
2699        buf.extend_from_slice(&0u32.to_le_bytes());
2700        buf.extend_from_slice(&0u32.to_le_bytes());
2701        // Device size (SIZEL)
2702        buf.extend_from_slice(&1024i32.to_le_bytes());
2703        buf.extend_from_slice(&768i32.to_le_bytes());
2704        // Millimeters size (SIZEL)
2705        buf.extend_from_slice(&320i32.to_le_bytes());
2706        buf.extend_from_slice(&240i32.to_le_bytes());
2707        debug_assert_eq!(buf.len(), 88);
2708
2709        // ---- EMR_EOF (20 bytes) ----
2710        buf.extend_from_slice(&14u32.to_le_bytes()); // record type
2711        buf.extend_from_slice(&20u32.to_le_bytes()); // record size
2712        buf.extend_from_slice(&0u32.to_le_bytes()); // nPalEntries
2713        buf.extend_from_slice(&0u32.to_le_bytes()); // offPalEntries
2714        buf.extend_from_slice(&20u32.to_le_bytes()); // SizeLast
2715
2716        buf
2717    }
2718
2719    /// 44-byte buffer with the right magic bytes but no payload. Detected
2720    /// as EMF, but can't actually be parsed — used to exercise the
2721    /// best-effort fallback.
2722    fn corrupt_emf_header() -> Vec<u8> {
2723        let mut buf = vec![0u8; 44];
2724        buf[0..4].copy_from_slice(&[0x01, 0x00, 0x00, 0x00]);
2725        buf[4..8].copy_from_slice(&[0x2C, 0x00, 0x00, 0x00]);
2726        buf[40..44].copy_from_slice(&[0x20, 0x45, 0x4D, 0x46]);
2727        buf
2728    }
2729
2730    fn png_signature_bytes() -> Vec<u8> {
2731        // 8-byte PNG signature followed by zeros — enough to fail PNG
2732        // decoding but plenty to verify routing decisions.
2733        let mut buf = vec![137, 80, 78, 71, 13, 10, 26, 10];
2734        buf.resize(64, 0);
2735        buf
2736    }
2737
2738    // ---------- is_emf ----------
2739
2740    #[test]
2741    fn is_emf_detects_lowercase_extension() {
2742        assert!(is_emf("word/media/image1.emf", &[]));
2743    }
2744
2745    #[test]
2746    fn is_emf_detects_uppercase_extension() {
2747        assert!(is_emf("word/media/IMAGE1.EMF", &[]));
2748    }
2749
2750    #[test]
2751    fn is_emf_rejects_png_extension() {
2752        assert!(!is_emf("word/media/image1.png", &png_signature_bytes()));
2753    }
2754
2755    #[test]
2756    fn is_emf_rejects_empty_buffer_with_unrelated_extension() {
2757        assert!(!is_emf("word/media/image1.bin", &[]));
2758    }
2759
2760    #[test]
2761    fn is_emf_detects_magic_bytes_without_extension() {
2762        let buf = minimal_valid_emf();
2763        assert!(is_emf("word/media/image1.bin", &buf));
2764    }
2765
2766    #[test]
2767    fn is_emf_rejects_buffer_too_short_for_signature() {
2768        // Has the record-type bytes but is shorter than 44 bytes, so the
2769        // signature check at offset 40 cannot pass.
2770        let buf = vec![0x01, 0x00, 0x00, 0x00];
2771        assert!(!is_emf("x.bin", &buf));
2772    }
2773
2774    #[test]
2775    fn is_emf_rejects_wrong_signature() {
2776        // Right size, right record type, wrong signature bytes.
2777        let mut buf = vec![0u8; 44];
2778        buf[0..4].copy_from_slice(&[0x01, 0x00, 0x00, 0x00]);
2779        buf[40..44].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
2780        assert!(!is_emf("x.bin", &buf));
2781    }
2782
2783    // ---------- add_image routing ----------
2784
2785    #[test]
2786    fn add_image_routes_valid_emf_into_images_with_empty_preview() {
2787        let buf = minimal_valid_emf();
2788        let docx = Docx::new().add_image("rId1", "word/media/image1.emf", buf.clone());
2789
2790        assert_eq!(
2791            docx.images.len(),
2792            1,
2793            "EMF should land in the unified `images` vector"
2794        );
2795        let (id, path, original, preview) = &docx.images[0];
2796        assert_eq!(id, "rId1");
2797        assert_eq!(path, "word/media/image1.emf");
2798        assert_eq!(original.0, buf);
2799        assert!(preview.0.is_empty());
2800    }
2801
2802    #[test]
2803    fn add_image_preserves_emf_detected_by_magic_bytes() {
2804        let buf = corrupt_emf_header();
2805        let docx = Docx::new().add_image("rId1", "word/media/image1.bin", buf.clone());
2806        assert_eq!(docx.images.len(), 1);
2807        let (_, _, original, preview) = &docx.images[0];
2808        assert_eq!(original.0, buf);
2809        assert!(preview.0.is_empty());
2810    }
2811
2812    #[test]
2813    fn add_image_routes_png_as_png_preview() {
2814        let png_bytes = png_signature_bytes();
2815        let docx = Docx::new().add_image("rId1", "word/media/image1.png", png_bytes.clone());
2816        assert_eq!(docx.images.len(), 1);
2817        let (_, _, original, preview) = &docx.images[0];
2818        // PNG preview starts with the PNG signature, not `<svg`.
2819        assert_eq!(&preview.0[..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
2820        assert_eq!(original.0, png_bytes);
2821        assert_eq!(preview.0, png_bytes);
2822    }
2823
2824    // ---------- Docx JSON serialization ----------
2825
2826    #[test]
2827    fn docx_serializes_emf_under_images_field() {
2828        let docx = Docx::new().add_image("rId1", "word/media/image1.emf", minimal_valid_emf());
2829        let json = serde_json::to_string(&docx).expect("should serialize");
2830        // EMF entries appear under the existing `images` key — no
2831        // separate `imagesEmf` key is emitted.
2832        assert!(json.contains("\"images\""));
2833        assert!(
2834            !json.contains("imagesEmf"),
2835            "EMF is unified into `images`; no separate `imagesEmf` key"
2836        );
2837        // The original bytes are base64-serialised inside the tuple.
2838        use base64::Engine;
2839        let b64 = base64::engine::general_purpose::STANDARD.encode(minimal_valid_emf());
2840        assert!(json.contains(&b64));
2841    }
2842}
2843
2844/// Reader-level integration tests for EMF passthrough. We construct a tiny
2845/// in-memory docx (just enough relationships + a media entry) and run it
2846/// through `read_docx` to verify the original bytes surface on `Docx.images`.
2847#[cfg(test)]
2848mod emf_reader_tests {
2849    use super::emf_tests::minimal_valid_emf;
2850    use std::io::Write;
2851
2852    /// Builds a minimum-viable docx ZIP that contains one EMF media
2853    /// file referenced from `document.xml.rels`. The XML payloads are
2854    /// the smallest accepted by the reader; the EMF is the same minimal
2855    /// header/EOF pair used in the unit tests.
2856    fn build_docx_with_emf() -> Vec<u8> {
2857        let buf = std::io::Cursor::new(Vec::<u8>::new());
2858        let mut zip = zip::ZipWriter::new(buf);
2859        let opts: zip::write::FileOptions<'_, ()> =
2860            zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
2861
2862        let content_types = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2863<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
2864  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
2865  <Default Extension="xml" ContentType="application/xml"/>
2866  <Default Extension="emf" ContentType="image/x-emf"/>
2867  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
2868</Types>"#;
2869        let root_rels = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2870<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
2871  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
2872</Relationships>"#;
2873        let doc_rels = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2874<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
2875  <Relationship Id="rId10" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.emf"/>
2876</Relationships>"#;
2877        let document = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2878<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2879  <w:body>
2880    <w:p><w:r><w:t>hello</w:t></w:r></w:p>
2881  </w:body>
2882</w:document>"#;
2883
2884        for (path, body) in [
2885            ("[Content_Types].xml", content_types),
2886            ("_rels/.rels", root_rels),
2887            ("word/_rels/document.xml.rels", doc_rels),
2888            ("word/document.xml", document),
2889        ] {
2890            zip.start_file(path, opts).unwrap();
2891            zip.write_all(body.as_bytes()).unwrap();
2892        }
2893        zip.start_file("word/media/image1.emf", opts).unwrap();
2894        zip.write_all(&minimal_valid_emf()).unwrap();
2895
2896        zip.finish().unwrap().into_inner()
2897    }
2898
2899    #[test]
2900    fn read_docx_routes_emf_media_to_images_with_empty_preview() {
2901        let docx_bytes = build_docx_with_emf();
2902        let docx = crate::reader::read_docx(&docx_bytes).expect("should read docx");
2903
2904        assert_eq!(
2905            docx.images.len(),
2906            1,
2907            "EMF media should be routed into the unified `images` vector"
2908        );
2909        let (id, path, original, preview) = &docx.images[0];
2910        assert_eq!(id, "rId10");
2911        assert!(path.ends_with("image1.emf"));
2912        assert_eq!(original.0, minimal_valid_emf());
2913        assert!(preview.0.is_empty());
2914    }
2915}