Skip to main content

office_toolkit/
document.rs

1//! File-level convenience: auto-detecting [`open`], and the
2//! [`OpenFile`]/[`SaveToFile`] extension traits.
3//!
4//! None of this exists in `word-ooxml`/`excel-ooxml`/`powerpoint-ooxml`
5//! themselves, deliberately — those three crates only ever read from/write
6//! to an in-memory `Read + Seek`/`Write + Seek` (a `Cursor<Vec<u8>>`, a
7//! `File`, a network stream, ...), never touching `std::fs` directly, so
8//! they stay usable in contexts with no real filesystem (e.g. compiled to
9//! WASM). This module is where that filesystem convenience actually
10//! belongs: one place, in the crate whose entire reason to exist is making
11//! the common case simple, rather than three copies of the same
12//! `File::open`/`File::create` boilerplate pasted into each format crate.
13
14use std::fs::File;
15use std::io::Cursor;
16use std::path::Path;
17
18use crate::error::{Error, Result};
19
20/// A document opened by [`open`], with its format determined at runtime
21/// from the file's own content rather than assumed up front. Match on this
22/// when the caller genuinely doesn't know the format ahead of time; when
23/// it does, [`OpenFile::open_file`] on the concrete type
24/// (`word::Document::open_file(...)`, etc.) skips the enum entirely.
25#[derive(Debug)]
26pub enum OfficeDocument {
27    /// Boxed: a `word_ooxml::Document` is the largest of the three by a
28    /// wide enough margin that clippy's `large_enum_variant` flags the gap
29    /// every other variant would otherwise pay for.
30    #[cfg(feature = "word")]
31    Word(Box<word_ooxml::Document>),
32    #[cfg(feature = "excel")]
33    Excel(excel_ooxml::Workbook),
34    #[cfg(feature = "powerpoint")]
35    Powerpoint(powerpoint_ooxml::Presentation),
36}
37
38impl OfficeDocument {
39    /// Writes this document back out, in whichever format it was opened
40    /// as — same convenience as [`SaveToFile::save_to_file`], without the
41    /// caller having to match on the variant themselves first just to call
42    /// the right type's own method.
43    pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
44        match self {
45            #[cfg(feature = "word")]
46            OfficeDocument::Word(document) => document.save_to_file(path),
47            #[cfg(feature = "excel")]
48            OfficeDocument::Excel(workbook) => workbook.save_to_file(path),
49            #[cfg(feature = "powerpoint")]
50            OfficeDocument::Powerpoint(presentation) => presentation.save_to_file(path),
51        }
52    }
53}
54
55/// Opens a `.docx`/`.xlsx`/`.pptx` file, detecting its format from the OPC
56/// package's own parts (`word/document.xml`/`xl/workbook.xml`/
57/// `ppt/presentation.xml`) rather than from the file's extension or name —
58/// a renamed or extension-less file still opens correctly, and a file
59/// carrying none of the three is reported as [`Error::UnrecognizedFormat`]
60/// rather than silently misidentified as one of them. A file whose format
61/// *is* recognized, but whose matching Cargo feature isn't enabled in this
62/// build, is reported as [`Error::FormatNotEnabled`] instead — a more
63/// useful answer than pretending the file doesn't exist.
64///
65/// Reads the whole file into memory once to inspect it (`opc::Package`
66/// keeps every part's bytes resident regardless), then re-parses those
67/// same already-read bytes through the matching format crate's own
68/// `read_from` — a second, format-specific *parse*, not a second read from
69/// disk.
70pub fn open(path: impl AsRef<Path>) -> Result<OfficeDocument> {
71    let bytes = std::fs::read(path)?;
72    let package = opc::Package::read_from(Cursor::new(&bytes))?;
73
74    if package.part("/word/document.xml").is_some() {
75        #[cfg(feature = "word")]
76        return Ok(OfficeDocument::Word(Box::new(
77            word_ooxml::Document::read_from(Cursor::new(&bytes))?,
78        )));
79        #[cfg(not(feature = "word"))]
80        return Err(Error::FormatNotEnabled("word"));
81    }
82    if package.part("/xl/workbook.xml").is_some() {
83        #[cfg(feature = "excel")]
84        return Ok(OfficeDocument::Excel(excel_ooxml::Workbook::read_from(
85            Cursor::new(&bytes),
86        )?));
87        #[cfg(not(feature = "excel"))]
88        return Err(Error::FormatNotEnabled("excel"));
89    }
90    if package.part("/ppt/presentation.xml").is_some() {
91        #[cfg(feature = "powerpoint")]
92        return Ok(OfficeDocument::Powerpoint(
93            powerpoint_ooxml::Presentation::read_from(Cursor::new(&bytes))?,
94        ));
95        #[cfg(not(feature = "powerpoint"))]
96        return Err(Error::FormatNotEnabled("powerpoint"));
97    }
98
99    Err(Error::UnrecognizedFormat)
100}
101
102/// Reads a `Document`/`Workbook`/`Presentation` directly from a path on
103/// disk — `word::Document::open_file("report.docx")?` instead of manually
104/// opening a [`File`] and calling the type's own `read_from`. For the
105/// common case where the caller already knows which format they're
106/// working with; see [`open`] for auto-detection.
107pub trait OpenFile: Sized {
108    /// Reads `self` from the file at `path`.
109    fn open_file(path: impl AsRef<Path>) -> Result<Self>;
110}
111
112/// Writes a `Document`/`Workbook`/`Presentation` directly to a path on
113/// disk — `doc.save_to_file("report.docx")?` instead of manually creating
114/// a [`File`] and calling the type's own `write_to`.
115pub trait SaveToFile {
116    /// Writes `self` to the file at `path`, creating it (or truncating an
117    /// existing one) as needed.
118    fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()>;
119}
120
121#[cfg(feature = "word")]
122impl OpenFile for word_ooxml::Document {
123    fn open_file(path: impl AsRef<Path>) -> Result<Self> {
124        Ok(Self::read_from(File::open(path)?)?)
125    }
126}
127
128#[cfg(feature = "word")]
129impl SaveToFile for word_ooxml::Document {
130    fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
131        self.write_to(File::create(path)?)?;
132        Ok(())
133    }
134}
135
136#[cfg(feature = "excel")]
137impl OpenFile for excel_ooxml::Workbook {
138    fn open_file(path: impl AsRef<Path>) -> Result<Self> {
139        Ok(Self::read_from(File::open(path)?)?)
140    }
141}
142
143#[cfg(feature = "excel")]
144impl SaveToFile for excel_ooxml::Workbook {
145    fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
146        self.write_to(File::create(path)?)?;
147        Ok(())
148    }
149}
150
151#[cfg(feature = "powerpoint")]
152impl OpenFile for powerpoint_ooxml::Presentation {
153    fn open_file(path: impl AsRef<Path>) -> Result<Self> {
154        Ok(Self::read_from(File::open(path)?)?)
155    }
156}
157
158#[cfg(feature = "powerpoint")]
159impl SaveToFile for powerpoint_ooxml::Presentation {
160    fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
161        self.write_to(File::create(path)?)?;
162        Ok(())
163    }
164}