Skip to main content

docx_rs/reader/
read_docx.rs

1use super::header_or_footer_rels::{read_header_or_footer_rels, ReadHeaderOrFooterRels};
2use super::namespace::*;
3use super::*;
4
5use zip::ZipArchive;
6
7fn read_headers(
8    rels: &ReadDocumentRels,
9    archive: &mut ZipArchive<Cursor<&[u8]>>,
10) -> HashMap<RId, (Header, ReadHeaderOrFooterRels)> {
11    let header_paths = rels.target_paths(HEADER_TYPE);
12    let headers: HashMap<RId, (Header, ReadHeaderOrFooterRels)> = header_paths
13        .into_iter()
14        .flatten()
15        .filter_map(|(rid, path, ..)| {
16            let data = read_zip(archive, path.to_str().expect("should have header path."));
17            if let Ok(d) = data {
18                if let Ok(h) = Header::from_xml(&d[..]) {
19                    let rels = read_header_or_footer_rels(archive, path).unwrap_or_default();
20                    return Some((rid.clone(), (h, rels)));
21                }
22            }
23            None
24        })
25        .collect();
26    headers
27}
28
29fn read_footers(
30    rels: &ReadDocumentRels,
31    archive: &mut ZipArchive<Cursor<&[u8]>>,
32) -> HashMap<RId, (Footer, ReadHeaderOrFooterRels)> {
33    let footer_paths = rels.target_paths(FOOTER_TYPE);
34    let footers: HashMap<RId, (Footer, ReadHeaderOrFooterRels)> = footer_paths
35        .into_iter()
36        .flatten()
37        .filter_map(|(rid, path, ..)| {
38            let data = read_zip(archive, path.to_str().expect("should have footer path."));
39            if let Ok(d) = data {
40                if let Ok(h) = Footer::from_xml(&d[..]) {
41                    let rels = read_header_or_footer_rels(archive, path).unwrap_or_default();
42                    return Some((rid.clone(), (h, rels)));
43                }
44            }
45            None
46        })
47        .collect();
48    footers
49}
50
51fn read_themes(rels: &ReadDocumentRels, archive: &mut ZipArchive<Cursor<&[u8]>>) -> Vec<Theme> {
52    let theme_paths = rels.target_paths(THEME_TYPE);
53    theme_paths
54        .into_iter()
55        .flatten()
56        .filter_map(|(_rid, path, ..)| {
57            let data = read_zip(archive, path.to_str().expect("should have footer path."));
58            if let Ok(d) = data {
59                if let Ok(h) = Theme::from_xml(&d[..]) {
60                    return Some(h);
61                }
62            }
63            None
64        })
65        .collect()
66}
67
68/// Controls optional work performed while reading a DOCX package.
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70#[non_exhaustive]
71pub struct ReadDocxOptions {
72    /// Generate PNG previews for embedded images.
73    ///
74    /// Disabling this avoids decoding and re-encoding non-PNG images. The original
75    /// image bytes are still available in [`Docx::images`], while the preview is empty.
76    pub generate_image_previews: bool,
77}
78
79impl ReadDocxOptions {
80    /// Enables or disables PNG preview generation for embedded images.
81    pub const fn with_image_previews(mut self, generate: bool) -> Self {
82        self.generate_image_previews = generate;
83        self
84    }
85}
86
87impl Default for ReadDocxOptions {
88    fn default() -> Self {
89        Self {
90            generate_image_previews: true,
91        }
92    }
93}
94
95pub fn read_docx(buf: &[u8]) -> Result<Docx, ReaderError> {
96    read_docx_with_options(buf, ReadDocxOptions::default())
97}
98
99pub fn read_docx_with_options(buf: &[u8], options: ReadDocxOptions) -> Result<Docx, ReaderError> {
100    let mut docx = Docx::new();
101    let cur = Cursor::new(buf);
102    let mut archive = zip::ZipArchive::new(cur)?;
103    // First, the content type for relationship parts and the Main Document part
104    // (the only required part) must be defined (physically located at /[Content_Types].xml in the package)
105    let _content_types = {
106        let data = read_zip(&mut archive, "[Content_Types].xml")?;
107        ContentTypes::from_xml(&data[..])?
108    };
109
110    // Next, the single required relationship (the package-level relationship to the Main Document part)
111    //  must be defined (physically located at /_rels/.rels in the package)
112    let rels = {
113        let data = read_zip(&mut archive, "_rels/.rels")?;
114        Rels::from_xml(&data[..])?
115    };
116
117    // Finally, the minimum content for the Main Document part must be defined
118    // (physically located at /document.xml in the package):
119    let main_rel = rels
120        .find_target(DOC_RELATIONSHIP_TYPE)
121        .ok_or(ReaderError::DocumentNotFoundError);
122
123    let document_path = if let Ok(rel) = main_rel {
124        rel.2.clone()
125    } else {
126        "word/document.xml".to_owned()
127    };
128
129    if let Some(custom_props) = rels.find_target(CUSTOM_PROPERTIES_TYPE) {
130        let data = read_zip(&mut archive, &custom_props.2);
131        if let Ok(data) = data {
132            if let Ok(custom) = CustomProps::from_xml(&data[..]) {
133                docx.doc_props.custom = custom;
134            }
135        }
136    }
137
138    let rels = read_document_rels(&mut archive, &document_path)?;
139
140    let headers = read_headers(&rels, &mut archive);
141    let footers = read_footers(&rels, &mut archive);
142
143    docx.themes = read_themes(&rels, &mut archive);
144
145    // Read commentsExtended
146    let comments_extended_path = rels.target_paths(COMMENTS_EXTENDED_TYPE);
147    let comments_extended = if let Some(comments_extended_path) = comments_extended_path {
148        if let Some((_, comments_extended_path, ..)) = comments_extended_path.first() {
149            let data = read_zip(
150                &mut archive,
151                comments_extended_path
152                    .to_str()
153                    .expect("should have comments extended."),
154            );
155            if let Ok(data) = data {
156                CommentsExtended::from_xml(&data[..])?
157            } else {
158                CommentsExtended::default()
159            }
160        } else {
161            CommentsExtended::default()
162        }
163    } else {
164        CommentsExtended::default()
165    };
166
167    // Read comments
168    let comments_path = rels.target_paths(COMMENTS_TYPE);
169    let comments = if let Some(paths) = comments_path {
170        if let Some((_, comments_path, ..)) = paths.first() {
171            let data = read_zip(
172                &mut archive,
173                comments_path.to_str().expect("should have comments."),
174            );
175            if let Ok(data) = data {
176                let mut comments = Comments::from_xml(&data[..])?.into_inner();
177                if !comments.is_empty() && !comments_extended.children.is_empty() {
178                    let mut comment_id_by_paragraph: HashMap<String, usize> = HashMap::new();
179                    for comment in &comments {
180                        for child in &comment.children {
181                            if let CommentChild::Paragraph(p) = child {
182                                comment_id_by_paragraph.insert(p.id.clone(), comment.id);
183                            }
184                        }
185                    }
186
187                    let mut parent_paragraph_by_paragraph: HashMap<String, String> = HashMap::new();
188                    for extended in &comments_extended.children {
189                        if let Some(parent_paragraph_id) = extended.parent_paragraph_id.as_deref() {
190                            parent_paragraph_by_paragraph.insert(
191                                extended.paragraph_id.clone(),
192                                parent_paragraph_id.to_string(),
193                            );
194                        }
195                    }
196
197                    for comment in &mut comments {
198                        let mut parent_comment_id = None;
199                        for child in &comment.children {
200                            if let CommentChild::Paragraph(p) = child {
201                                if let Some(parent_paragraph_id) =
202                                    parent_paragraph_by_paragraph.get(&p.id)
203                                {
204                                    parent_comment_id =
205                                        comment_id_by_paragraph.get(parent_paragraph_id).copied();
206                                    if parent_comment_id.is_some() {
207                                        break;
208                                    }
209                                }
210                            }
211                        }
212                        comment.parent_comment_id = parent_comment_id;
213                    }
214                }
215                Comments { comments }
216            } else {
217                Comments::default()
218            }
219        } else {
220            Comments::default()
221        }
222    } else {
223        Comments::default()
224    };
225
226    let document = {
227        let data = read_zip(&mut archive, &document_path)?;
228        Document::from_xml(&data[..])?
229    };
230    docx = docx.document(document);
231
232    // assign headers
233    if let Some(h) = docx.document.section_property.header_reference.clone() {
234        if let Some((header, rels)) = headers.get(&h.id) {
235            docx.document = docx.document.header(header.clone(), &h.id);
236            let count = docx.document_rels.header_count + 1;
237            docx.document_rels.header_count = count;
238            docx.content_type = docx.content_type.add_header();
239            // Read media
240            let media = rels.target_paths(IMAGE_TYPE);
241            docx = add_images(docx, media, &mut archive, options.generate_image_previews);
242        }
243    }
244    if let Some(ref h) = docx
245        .document
246        .section_property
247        .first_header_reference
248        .clone()
249    {
250        if let Some((header, rels)) = headers.get(&h.id) {
251            docx.document = docx
252                .document
253                .first_header_without_title_pg(header.clone(), &h.id);
254            let count = docx.document_rels.header_count + 1;
255            docx.document_rels.header_count = count;
256            docx.content_type = docx.content_type.add_header();
257            // Read media
258            let media = rels.target_paths(IMAGE_TYPE);
259            docx = add_images(docx, media, &mut archive, options.generate_image_previews);
260        }
261    }
262    if let Some(ref h) = docx.document.section_property.even_header_reference.clone() {
263        if let Some((header, rels)) = headers.get(&h.id) {
264            docx.document = docx.document.even_header(header.clone(), &h.id);
265            let count = docx.document_rels.header_count + 1;
266            docx.document_rels.header_count = count;
267            docx.content_type = docx.content_type.add_header();
268
269            // Read media
270            let media = rels.target_paths(IMAGE_TYPE);
271            docx = add_images(docx, media, &mut archive, options.generate_image_previews);
272        }
273    }
274
275    // assign footers
276    if let Some(f) = docx.document.section_property.footer_reference.clone() {
277        if let Some((footer, rels)) = footers.get(&f.id) {
278            docx.document = docx.document.footer(footer.clone(), &f.id);
279            let count = docx.document_rels.footer_count + 1;
280            docx.document_rels.footer_count = count;
281            docx.content_type = docx.content_type.add_footer();
282
283            // Read media
284            let media = rels.target_paths(IMAGE_TYPE);
285            docx = add_images(docx, media, &mut archive, options.generate_image_previews);
286        }
287    }
288
289    if let Some(ref f) = docx
290        .document
291        .section_property
292        .first_footer_reference
293        .clone()
294    {
295        if let Some((footer, rels)) = footers.get(&f.id) {
296            docx.document = docx
297                .document
298                .first_footer_without_title_pg(footer.clone(), &f.id);
299            let count = docx.document_rels.footer_count + 1;
300            docx.document_rels.footer_count = count;
301            docx.content_type = docx.content_type.add_footer();
302
303            // Read media
304            let media = rels.target_paths(IMAGE_TYPE);
305            docx = add_images(docx, media, &mut archive, options.generate_image_previews);
306        }
307    }
308    if let Some(ref f) = docx.document.section_property.even_footer_reference.clone() {
309        if let Some((footer, rels)) = footers.get(&f.id) {
310            docx.document = docx.document.even_footer(footer.clone(), &f.id);
311            let count = docx.document_rels.footer_count + 1;
312            docx.document_rels.footer_count = count;
313            docx.content_type = docx.content_type.add_footer();
314
315            // Read media
316            let media = rels.target_paths(IMAGE_TYPE);
317            docx = add_images(docx, media, &mut archive, options.generate_image_previews);
318        }
319    }
320
321    // store comments to paragraphs.
322    if !comments.inner().is_empty() {
323        docx.store_comments(comments.inner());
324        docx = docx.comments(comments);
325        docx = docx.comments_extended(comments_extended);
326    }
327
328    // Read document relationships
329    // Read styles
330    let style_path = rels.target_paths(STYLE_RELATIONSHIP_TYPE);
331    if let Some(paths) = style_path {
332        if let Some((_, style_path, ..)) = paths.first() {
333            let data = read_zip(
334                &mut archive,
335                style_path.to_str().expect("should have styles"),
336            )?;
337            let styles = Styles::from_xml(&data[..])?;
338            docx = docx.styles(styles);
339        }
340    }
341
342    // Read numberings
343    let num_path = rels.target_paths(NUMBERING_RELATIONSHIP_TYPE);
344    if let Some(paths) = num_path {
345        if let Some((_, num_path, ..)) = paths.first() {
346            let data = read_zip(
347                &mut archive,
348                num_path.to_str().expect("should have numberings"),
349            )?;
350            let nums = Numberings::from_xml(&data[..])?;
351            docx = docx.numberings(nums);
352        }
353    }
354
355    // Read settings
356    let settings_path = rels.target_paths(SETTINGS_TYPE);
357    if let Some(paths) = settings_path {
358        if let Some((_, settings_path, ..)) = paths.first() {
359            let data = read_zip(
360                &mut archive,
361                settings_path.to_str().expect("should have settings"),
362            )?;
363            let settings = Settings::from_xml(&data[..])?;
364            docx = docx.settings(settings);
365        }
366    }
367
368    // Read web settings
369    let web_settings_path = rels.target_paths(WEB_SETTINGS_TYPE);
370    if let Some(paths) = web_settings_path {
371        if let Some((_, web_settings_path, ..)) = paths.first() {
372            let data = read_zip(
373                &mut archive,
374                web_settings_path
375                    .to_str()
376                    .expect("should have web settings"),
377            )?;
378            let web_settings = WebSettings::from_xml(&data[..])?;
379            docx = docx.web_settings(web_settings);
380        }
381    }
382    // Read media
383    let media = rels.target_paths(IMAGE_TYPE);
384    docx = add_images(docx, media, &mut archive, options.generate_image_previews);
385
386    // Read hyperlinks
387    let links = rels.target_paths(HYPERLINK_TYPE);
388    if let Some(paths) = links {
389        for (id, target, mode) in paths {
390            if let Some(mode) = mode {
391                docx = docx.add_hyperlink(
392                    id.clone(),
393                    target.to_str().expect("should convert to str"),
394                    mode.clone(),
395                );
396            }
397        }
398    }
399
400    Ok(docx)
401}
402
403fn add_images(
404    mut docx: Docx,
405    media: Option<&std::collections::BTreeSet<(RId, PathBuf, Option<String>)>>,
406    archive: &mut ZipArchive<Cursor<&[u8]>>,
407    generate_previews: bool,
408) -> Docx {
409    // Read media
410    if let Some(paths) = media {
411        for (id, media, ..) in paths {
412            if let Ok(data) = read_zip(archive, media.to_str().expect("should have media")) {
413                docx = docx.add_image_with_options(
414                    id.clone(),
415                    media.to_str().unwrap().to_string(),
416                    data,
417                    generate_previews,
418                );
419            }
420        }
421    }
422    docx
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    static JPEG_DOCX: &[u8] = include_bytes!("../../../fixtures/image_output/image.docx");
430
431    #[test]
432    fn read_without_image_previews_preserves_original_image() {
433        let docx = read_docx_with_options(
434            JPEG_DOCX,
435            ReadDocxOptions::default().with_image_previews(false),
436        )
437        .unwrap();
438
439        assert_eq!(docx.images.len(), 1);
440        let (_, _, image, preview) = &docx.images[0];
441        assert!(!image.0.is_empty());
442        assert!(preview.0.is_empty());
443    }
444
445    #[cfg(feature = "image")]
446    #[test]
447    fn read_with_default_options_generates_image_preview() {
448        let docx = read_docx(JPEG_DOCX).unwrap();
449
450        assert_eq!(docx.images.len(), 1);
451        let (_, _, _, preview) = &docx.images[0];
452        assert!(!preview.0.is_empty());
453    }
454}