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}