office-toolkit 1.0.0

A Rust library to create, read, and modify Microsoft Office documents (Word/Excel/PowerPoint) in the modern Open XML (OOXML) format.
Documentation
//! File-level convenience: auto-detecting [`open`], and the
//! [`OpenFile`]/[`SaveToFile`] extension traits.
//!
//! None of this exists in `word-ooxml`/`excel-ooxml`/`powerpoint-ooxml`
//! themselves, deliberately — those three crates only ever read from/write
//! to an in-memory `Read + Seek`/`Write + Seek` (a `Cursor<Vec<u8>>`, a
//! `File`, a network stream, ...), never touching `std::fs` directly, so
//! they stay usable in contexts with no real filesystem (e.g. compiled to
//! WASM). This module is where that filesystem convenience actually
//! belongs: one place, in the crate whose entire reason to exist is making
//! the common case simple, rather than three copies of the same
//! `File::open`/`File::create` boilerplate pasted into each format crate.

use std::fs::File;
use std::io::Cursor;
use std::path::Path;

use crate::error::{Error, Result};

/// A document opened by [`open`], with its format determined at runtime
/// from the file's own content rather than assumed up front. Match on this
/// when the caller genuinely doesn't know the format ahead of time; when
/// it does, [`OpenFile::open_file`] on the concrete type
/// (`word::Document::open_file(...)`, etc.) skips the enum entirely.
#[derive(Debug)]
pub enum OfficeDocument {
    /// Boxed: a `word_ooxml::Document` is the largest of the three by a
    /// wide enough margin that clippy's `large_enum_variant` flags the gap
    /// every other variant would otherwise pay for.
    #[cfg(feature = "word")]
    Word(Box<word_ooxml::Document>),
    #[cfg(feature = "excel")]
    Excel(excel_ooxml::Workbook),
    #[cfg(feature = "powerpoint")]
    Powerpoint(powerpoint_ooxml::Presentation),
}

impl OfficeDocument {
    /// Writes this document back out, in whichever format it was opened
    /// as — same convenience as [`SaveToFile::save_to_file`], without the
    /// caller having to match on the variant themselves first just to call
    /// the right type's own method.
    pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
        match self {
            #[cfg(feature = "word")]
            OfficeDocument::Word(document) => document.save_to_file(path),
            #[cfg(feature = "excel")]
            OfficeDocument::Excel(workbook) => workbook.save_to_file(path),
            #[cfg(feature = "powerpoint")]
            OfficeDocument::Powerpoint(presentation) => presentation.save_to_file(path),
        }
    }
}

/// Opens a `.docx`/`.xlsx`/`.pptx` file, detecting its format from the OPC
/// package's own parts (`word/document.xml`/`xl/workbook.xml`/
/// `ppt/presentation.xml`) rather than from the file's extension or name —
/// a renamed or extension-less file still opens correctly, and a file
/// carrying none of the three is reported as [`Error::UnrecognizedFormat`]
/// rather than silently misidentified as one of them. A file whose format
/// *is* recognized, but whose matching Cargo feature isn't enabled in this
/// build, is reported as [`Error::FormatNotEnabled`] instead — a more
/// useful answer than pretending the file doesn't exist.
///
/// Reads the whole file into memory once to inspect it (`opc::Package`
/// keeps every part's bytes resident regardless), then re-parses those
/// same already-read bytes through the matching format crate's own
/// `read_from` — a second, format-specific *parse*, not a second read from
/// disk.
pub fn open(path: impl AsRef<Path>) -> Result<OfficeDocument> {
    let bytes = std::fs::read(path)?;
    let package = opc::Package::read_from(Cursor::new(&bytes))?;

    if package.part("/word/document.xml").is_some() {
        #[cfg(feature = "word")]
        return Ok(OfficeDocument::Word(Box::new(
            word_ooxml::Document::read_from(Cursor::new(&bytes))?,
        )));
        #[cfg(not(feature = "word"))]
        return Err(Error::FormatNotEnabled("word"));
    }
    if package.part("/xl/workbook.xml").is_some() {
        #[cfg(feature = "excel")]
        return Ok(OfficeDocument::Excel(excel_ooxml::Workbook::read_from(
            Cursor::new(&bytes),
        )?));
        #[cfg(not(feature = "excel"))]
        return Err(Error::FormatNotEnabled("excel"));
    }
    if package.part("/ppt/presentation.xml").is_some() {
        #[cfg(feature = "powerpoint")]
        return Ok(OfficeDocument::Powerpoint(
            powerpoint_ooxml::Presentation::read_from(Cursor::new(&bytes))?,
        ));
        #[cfg(not(feature = "powerpoint"))]
        return Err(Error::FormatNotEnabled("powerpoint"));
    }

    Err(Error::UnrecognizedFormat)
}

/// Reads a `Document`/`Workbook`/`Presentation` directly from a path on
/// disk — `word::Document::open_file("report.docx")?` instead of manually
/// opening a [`File`] and calling the type's own `read_from`. For the
/// common case where the caller already knows which format they're
/// working with; see [`open`] for auto-detection.
pub trait OpenFile: Sized {
    /// Reads `self` from the file at `path`.
    fn open_file(path: impl AsRef<Path>) -> Result<Self>;
}

/// Writes a `Document`/`Workbook`/`Presentation` directly to a path on
/// disk — `doc.save_to_file("report.docx")?` instead of manually creating
/// a [`File`] and calling the type's own `write_to`.
pub trait SaveToFile {
    /// Writes `self` to the file at `path`, creating it (or truncating an
    /// existing one) as needed.
    fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()>;
}

#[cfg(feature = "word")]
impl OpenFile for word_ooxml::Document {
    fn open_file(path: impl AsRef<Path>) -> Result<Self> {
        Ok(Self::read_from(File::open(path)?)?)
    }
}

#[cfg(feature = "word")]
impl SaveToFile for word_ooxml::Document {
    fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
        self.write_to(File::create(path)?)?;
        Ok(())
    }
}

#[cfg(feature = "excel")]
impl OpenFile for excel_ooxml::Workbook {
    fn open_file(path: impl AsRef<Path>) -> Result<Self> {
        Ok(Self::read_from(File::open(path)?)?)
    }
}

#[cfg(feature = "excel")]
impl SaveToFile for excel_ooxml::Workbook {
    fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
        self.write_to(File::create(path)?)?;
        Ok(())
    }
}

#[cfg(feature = "powerpoint")]
impl OpenFile for powerpoint_ooxml::Presentation {
    fn open_file(path: impl AsRef<Path>) -> Result<Self> {
        Ok(Self::read_from(File::open(path)?)?)
    }
}

#[cfg(feature = "powerpoint")]
impl SaveToFile for powerpoint_ooxml::Presentation {
    fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
        self.write_to(File::create(path)?)?;
        Ok(())
    }
}