Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ This directory contains some example of Calamine usage.
- `read_hyperlinks.rs`: Reads the hyperlinks defined in an XLSX worksheet,
either by sheet name or by sheet index.
- `read_picture_data.rs`: Reads pictures and their metadata from an XLSX file.
- `read_properties.rs`: Reads the workbook properties (core and extended)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omit the phrase "core and extended" here.

from an XLSX or XLSB file.

### Serialization examples

Expand Down
61 changes: 61 additions & 0 deletions examples/read_properties.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
//
// Copyright 2016-2026, Johann Tuffe.

//! Demonstrates reading workbook properties from an XLSX or XLSB file.
//!
//! This example reads the core and extended document properties (such as
//! creator, application, and company) from a workbook.
Comment on lines +7 to +8

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omit the phrase "core and extended" here and on any public docs. They won't mean anything to most end users.

//!
//! Run the example like this:
//!
//! ```text
//! $ cargo run -q --example read_properties -- tests/issues.xlsx
//!
//! Core / Extended properties:
//! creator: Some("Johann Tuffe")
//! last_modified_by: Some("Johann Tuffe")
//! application: Some("Microsoft Excel")
//! company: Some("SOCIETE GENERALE")
//! ```

use calamine::open_workbook_auto;
use std::env;
use std::process::exit;

fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <xlsx/xlsb path>", args[0]);
exit(1);
}

let path = &args[1];
let mut excel = match open_workbook_auto(path) {
Ok(excel) => excel,
Err(e) => {
eprintln!("Cannot open {path}: {e}");
exit(1);
}
};

let props = match excel.workbook_properties() {
Ok(props) => props,
Err(e) => {
eprintln!("Cannot read workbook properties from {path}: {e}");
exit(1);
}
};

println!("Core / Extended properties:");
println!(" creator: {:?}", props.creator);
println!(" last_modified_by: {:?}", props.last_modified_by);
println!(" created: {:?}", props.created);
println!(" modified: {:?}", props.modified);
println!(" title: {:?}", props.title);
println!(" application: {:?}", props.application);
println!(" app_version: {:?}", props.app_version);
println!(" company: {:?}", props.company);
println!(" template: {:?}", props.template);
println!(" manager: {:?}", props.manager);
}
15 changes: 14 additions & 1 deletion src/auto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::vba::VbaProject;
use crate::Picture;
use crate::{
open_workbook, open_workbook_from_rs, Data, DataRef, HeaderRow, Metadata, Ods, Range, Reader,
ReaderRef, Xls, Xlsb, Xlsx,
ReaderRef, WorkbookProperties, Xls, Xlsb, Xlsx,
};

use std::fs::File;
Expand All @@ -29,6 +29,19 @@ pub enum Sheets<RS> {
Ods(Ods<RS>),
}

impl<RS: std::io::Read + std::io::Seek> Sheets<RS> {
/// Get workbook document properties for formats that support them.
pub fn workbook_properties(&mut self) -> Result<&WorkbookProperties, Error> {
match self {
Sheets::Xlsx(e) => e.workbook_properties().map_err(Error::Xlsx),
Sheets::Xlsb(e) => e.workbook_properties().map_err(Error::Xlsb),
Sheets::Xls(_) | Sheets::Ods(_) => Err(Error::Msg(
"Workbook properties are not supported for this format",
)),
}
}
}

/// Opens a workbook and define the file type at runtime.
///
/// Whenever possible use the statically known `open_workbook` function instead
Expand Down
62 changes: 62 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,68 @@ pub struct Metadata {
names: Vec<(String, String)>,
}

/// Workbook document properties.
///
/// Depending on the file format, these fields may be read from workbook-level
/// document property parts.
///
/// Most fields are optional because they depend on the file format and
/// whether the producing application wrote them.
#[derive(Debug, Default, Clone, PartialEq)]
#[non_exhaustive]
pub struct WorkbookProperties {
/// Core property: creator (dc:creator).
pub creator: Option<String>,
Comment thread
Alex-ley-scrub marked this conversation as resolved.

/// Core property: last modifier (cp:lastModifiedBy).
pub last_modified_by: Option<String>,

/// Core property: creation date (dcterms:created).
pub created: Option<String>,

/// Core property: modification date (dcterms:modified).
pub modified: Option<String>,

/// Core property: title (dc:title).
pub title: Option<String>,

/// Core property: subject (dc:subject).
pub subject: Option<String>,

/// Core property: description/comments (dc:description).
pub description: Option<String>,

/// Core property: keywords (dc:keywords).
pub keywords: Option<String>,

/// Core property: category (dc:category).
pub category: Option<String>,

/// Core property: content status (cp:contentStatus).
pub content_status: Option<String>,

/// Core property: revision (cp:revision).
pub revision: Option<String>,

/// Core property: version (cp:version).
pub version: Option<String>,

/// Extended property: application name (ap:Application).
pub application: Option<String>,

/// Extended property: application version (ap:AppVersion).
pub app_version: Option<String>,

/// Extended property: company (ap:Company).
pub company: Option<String>,

/// Extended property: template (ap:Template).
pub template: Option<String>,

/// Extended property: manager (ap:Manager).
pub manager: Option<String>,
}

/// Type of sheet.
///
/// Only Excel formats support this. Default value for ODS is
Expand Down
36 changes: 36 additions & 0 deletions src/xlsb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ use crate::utils::{
read_usize,
};
use crate::vba::VbaProject;
use crate::xlsx::{read_app_properties, read_core_properties};
use crate::{
Cell, Data, HeaderRow, Metadata, Range, Reader, ReaderRef, Sheet, SheetType, SheetVisible,
WorkbookProperties,
};

/// A Xlsb specific error
Expand Down Expand Up @@ -160,6 +162,8 @@ pub struct Xlsb<RS> {
formats: Vec<CellFormat>,
is_1904: bool,
metadata: Metadata,
/// Workbook document properties, parsed lazily on first access.
workbook_properties: Option<Box<WorkbookProperties>>,
#[cfg(feature = "picture")]
pictures: Option<Vec<(String, Vec<u8>)>>,
options: XlsbOptions,
Expand Down Expand Up @@ -438,6 +442,24 @@ impl<RS: Read + Seek> Xlsb<RS> {
)
}

/// Get workbook document properties.
///
/// Missing fields are returned as `None`.
pub fn workbook_properties(&mut self) -> Result<&WorkbookProperties, XlsbError> {

@jmcnamara jmcnamara Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this again, it is probably better to return WorkbookProperties via a clone() rather than the reference &WorkbookProperties which could keep the workbook owned and could prevent other method calls.

WorkbookProperties isn't one of the performance critical or heavyweight internal structures that needs to be returned as a ref. Side note, there are probably inconsistencies of this ref/value return usage in other places in the code but that is a separate issue.

if self.workbook_properties.is_none() {
let mut props = WorkbookProperties::default();
read_core_properties(&mut self.zip, &mut props, &self.zip_path_cache)
.map_err(xlsx_error_to_xlsb)?;
read_app_properties(&mut self.zip, &mut props, &self.zip_path_cache)
.map_err(xlsx_error_to_xlsb)?;
self.workbook_properties = Some(Box::new(props));
}
Ok(self
.workbook_properties
.as_deref()
.expect("workbook properties are initialized above"))
}

#[cfg(feature = "picture")]
fn read_pictures(&mut self) -> Result<(), XlsbError> {
let mut pics = Vec::new();
Expand Down Expand Up @@ -483,6 +505,7 @@ impl<RS: Read + Seek> Reader<RS> for Xlsb<RS> {
formats: Vec::new(),
is_1904: false,
metadata: Metadata::default(),
workbook_properties: None,
#[cfg(feature = "picture")]
pictures: None,
options: XlsbOptions::default(),
Expand Down Expand Up @@ -1053,3 +1076,16 @@ fn check_for_password_protected<RS: Read + Seek>(reader: &mut RS) -> Result<(),

Ok(())
}

fn xlsx_error_to_xlsb(e: crate::xlsx::XlsxError) -> XlsbError {
use crate::xlsx::XlsxError;
match e {
XlsxError::Io(e) => XlsbError::Io(e),
XlsxError::Zip(e) => XlsbError::Zip(e),
XlsxError::Vba(e) => XlsbError::Vba(e),
XlsxError::Xml(e) => XlsbError::Xml(e),
XlsxError::XmlAttr(e) => XlsbError::XmlAttr(e),
XlsxError::Encoding(e) => XlsbError::Xml(e.into()),
e => XlsbError::FileNotFound(e.to_string()),
}
}
Loading
Loading