Preface
This book documents ooxmlsdk 0.10.2. Unless a page explicitly names another version, descriptions and examples in this book should be read as applying to 0.10.2.
Project links:
- Source: https://github.com/KaiserY/ooxmlsdk
- Crate: https://crates.io/crates/ooxmlsdk
- API documentation: https://docs.rs/ooxmlsdk/latest/ooxmlsdk/
This mdBook is an ooxmlsdk-focused Rust guide for working with Office Open XML packages.
The repository originally used public Office Open XML documentation as source material while the guide was being ported. First-round pages have been rewritten around Rust examples, Cargo workspaces, and tested snippets under listings/.
New ooxmlsdk-specific writing, Rust examples, tooling, and repository-maintained material are licensed under this repository's LICENSE-MIT or LICENSE-APACHE files, unless a file states otherwise.
Some articles discuss concepts from ECMA-376 or ISO/IEC 29500. Those specifications remain the authoritative format references.
Welcome to ooxmlsdk
ooxmlsdk is a Rust library for reading, writing, and round-tripping Office Open XML packages such as .docx, .xlsx, and .pptx.
This documentation targets ooxmlsdk 0.10.2. Unless a page explicitly says otherwise, all API descriptions, limitations, and examples refer to 0.10.2.
Core project links:
- Source: https://github.com/KaiserY/ooxmlsdk
- Crate: https://crates.io/crates/ooxmlsdk
- API documentation: https://docs.rs/ooxmlsdk/latest/ooxmlsdk/
Office Open XML is standardized by ECMA-376 and ISO/IEC 29500. The file formats are ZIP packages containing XML parts and explicit relationships, which makes them suitable for Rust tooling that needs deterministic package inspection or transformation.
The crate provides generated schema types, serializers, deserializers, and strongly typed package parts. Its API is Rust-native: methods return Result, package and part types are regular Rust structs, and optional functionality is controlled by Cargo features.
ooxmlsdk works at both layers of the format: the package graph and the XML schema data. You can open an existing document package, follow typed relationships to well-known parts, read or replace part data, load generated root elements, and save the package back to a writer.
Start here
Working with packages
References
Getting started with ooxmlsdk
ooxmlsdk is a Rust library for reading, writing, and round-tripping Office Open XML documents such as .docx, .xlsx, and .pptx. Its package API exposes generated Rust schema types, serializers, deserializers, and strongly typed package parts.
Rust package
Add ooxmlsdk to your Cargo project:
[dependencies]
ooxmlsdk = "0.10.2"
The default feature set enables the parts APIs used for .docx, .xlsx, and .pptx packages.
The documentation examples in this book are backed by real Rust files under listings/ and are checked with cargo test --workspace.
For example, this function opens a WordprocessingML package, confirms that the main document part is attached to the package, and writes the package back to memory:
#![allow(unused)] fn main() { use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::theme_part::ThemePart; use ooxmlsdk::parts::wordprocessing_document::WordprocessingDocument; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, is_encrypted_office_file_path}; pub fn round_trip_word_document(path: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file(path)?; let main_part = document.main_document_part().expect("main document part"); assert!(document.get_id_of_part(&main_part).is_some()); let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Use is_encrypted_office_file_path before opening a file when your tool needs to report encrypted packages separately from malformed or unsupported packages:
#![allow(unused)] fn main() { pub fn detect_encrypted_office_file(path: &Path) -> Result<bool, Box<dyn std::error::Error>> { Ok(is_encrypted_office_file_path(path)?) } }
Crate modules
The always-available modules are:
common: shared package data types and errors.namespaces: generated namespace constants and lookup helpers.schemas: generated schema structs and simple XML parsing/serialization support.sdk: package and part traits, open settings, relationship helpers, and feature-related settings.simple_type: generated simple type support.units: OOXML measure, coordinate, angle, and percentage value helpers.
Feature-gated modules are:
parts: package-level APIs behind thepartsfeature.validator: optional validator APIs behind thevalidatorsfeature.
Feature flags
ooxmlsdk has a small public feature surface:
default: enablesparts; this is the recommended configuration for most users.parts: enables package-level OOXML read/write APIs such asWordprocessingDocument,SpreadsheetDocument, andPresentationDocument.flat-opc: enables Flat OPC package read/write helpers and also enablesparts.mce: enables Markup Compatibility and Extensibility processing and also enablesparts.validators: exposes optional schema and package validation APIs.
For package APIs without extra feature behavior:
[dependencies]
ooxmlsdk = { version = "0.10.2", default-features = false, features = ["parts"] }
For Flat OPC helpers:
[dependencies]
ooxmlsdk = { version = "0.10.2", default-features = false, features = ["flat-opc"] }
For MCE processing during package open and root loading:
[dependencies]
ooxmlsdk = { version = "0.10.2", default-features = false, features = ["mce"] }
The validators feature is used by the validation listing in the Word processing chapter. It validates loaded package roots and generated schema values, and returns structured ValidationErrorInfo diagnostics.
For optional validator APIs:
[dependencies]
ooxmlsdk = { version = "0.10.2", features = ["validators"] }
Package API
With parts enabled, use the package type that matches the document family:
WordprocessingDocumentfor.docxand related WordprocessingML packages.SpreadsheetDocumentfor.xlsxand related SpreadsheetML packages.PresentationDocumentfor.pptxand related PresentationML packages.
Common operations include creating packages with create, opening packages with new, new_with_settings, new_from_file, or new_from_file_with_settings; creating editable packages from templates with create_from_template; checking and changing the package document type with document_type and change_document_type; detecting encrypted Office files with is_encrypted_office_file or is_encrypted_office_file_path; saving with save; inspecting relationships and parts with methods such as parts, get_all_parts, get_part_by_id, and get_parts_of_type; and accessing well-known child parts through typed methods such as main_document_part, workbook_part, presentation_part, and worksheet_parts.
For lower-level traversal, 0.10.2 exposes related-part helpers that can preserve the relationship id alongside the typed target part. Use those when a package edit needs to update XML r:id references and package relationships together.
The package types also expose convenience output helpers:
savewrites the current package to anyWrite + Seektarget.copy_towrites the package without consuming it.to_package_bytesreturns an in-memoryVec<u8>.save_as_filewrites directly to a path.
Version coverage
ooxmlsdk treats Office 2007 as the compatibility baseline while generating Rust support for newer namespaces and parts present in its checked-in metadata. That includes Office 2010, 2013, 2016, 2019, 2021, Microsoft 365-era extensions, and newer upstream namespace revisions tracked by the crate.
In practice this covers later DrawingML and chart extensions, SVG and 3D-related parts, threaded comments, dynamic-array-era spreadsheet extensions, and other post-2007 additions tracked by Open XML SDK metadata.
Schema values
The generated 0.10.2 schema API uses explicit wrappers for OOXML-specific values. Boolean-like schema attributes use types such as BooleanValue and OnOffValue, with conversion helpers such as from_bool() and as_bool(). Many lengths, coordinates, text sizes, and percentages use types from ooxmlsdk::units, which preserve the OOXML lexical form while still offering unit conversions.
About ooxmlsdk
Open XML is an open standard for word-processing documents, presentations, and spreadsheets. It was designed so applications can manipulate document data without depending on older proprietary binary formats or the original office application that created the file.
A .docx, .pptx, or .xlsx file is an Open Packaging Conventions package: a ZIP archive containing XML parts, binary parts, content types, and relationship files.
ooxmlsdk exposes that package structure through Rust types. You can open a package, navigate to strongly typed parts such as WordprocessingDocument, PresentationDocument, and SpreadsheetDocument, parse root XML elements into generated schema structs, modify those structs, and save the package again.
Package structure
An Open XML package contains:
[Content_Types].xml, which maps part names and extensions to content types.- Package-level relationships, usually in
_rels/.rels. - Document parts such as
word/document.xml,ppt/presentation.xml, orxl/workbook.xml. - Part-level relationships, such as
word/_rels/document.xml.rels. - Optional media, embedded objects, custom XML, comments, styles, charts, themes, and other package parts.
ooxmlsdk keeps these package concepts visible. The crate does not hide the file format behind a document-editor abstraction; instead, it gives you typed access to the package and schema model.
The ZIP container also gives package consumers random access to parts. For example, a tool can inspect a slide part without parsing every slide in the presentation, or remove a comments part from a word-processing package without reading all body paragraphs.
Document families
WordprocessingML packages are built around a required main document story. Optional stories and related parts can include:
- glossary document,
- headers and footers,
- comments,
- text boxes,
- footnotes and endnotes.
PresentationML packages can contain:
- slide masters,
- notes masters,
- handout masters,
- slide layouts,
- slides and notes.
SpreadsheetML packages can contain:
- a required workbook part,
- worksheets,
- charts,
- tables,
- custom XML,
- pivot caches and PivotTables.
Strongly typed Rust APIs
The runtime crate is generated from Open XML metadata. The generated surface includes:
- Package types in
ooxmlsdk::parts, behind thepartsfeature. - Schema structs and enums in
ooxmlsdk::schemas. - Shared package traits and settings in
ooxmlsdk::sdk. - Common package, relationship, XML, and error types in
ooxmlsdk::common. - Namespace constants and lookup helpers in
ooxmlsdk::namespaces. - Generated simple type support in
ooxmlsdk::simple_type. - OOXML measure and percentage helpers in
ooxmlsdk::units.
Most package operations return Result<_, ooxmlsdk::common::SdkError> or can be used with Box<dyn std::error::Error> in examples. Optional package relationships are represented with Option, and collections are exposed through Rust iterators or vectors depending on the generated schema shape.
In ooxmlsdk 0.10.2, generated schema fields use explicit simple value wrappers for OOXML booleans and typed unit values for many measures and percentages. Convert those values at the boundary of your application instead of assuming every schema attribute is a Rust bool, integer, or string.
Common tasks
ooxmlsdk supports the same broad task categories that matter when working with Open XML packages:
- Strongly typed package and schema access: use generated Rust types instead of hand-writing every element and attribute name.
- Content construction, search, and manipulation: traverse package relationships, inspect XML parts, load generated roots, and save updated packages. Use typed child accessors for well-known parts, or related-part traversal helpers when you need to keep the relationship id with the target part.
- Validation-oriented workflows: rely on explicit
Resulthandling plus package/schema tests, and enable the optionalvalidatorsfeature when you need structured schema diagnostics.
Feature model
In ooxmlsdk, the default feature set enables parts, which is what most users need for .docx, .xlsx, and .pptx package work.
Additional features are opt-in:
flat-opc: Flat OPC package read/write helpers.mce: Markup Compatibility and Extensibility processing.validators: optional validation APIs for generated schema roots and loaded package roots.
Use default-features = false when you want to make the enabled surface explicit.
Version coverage
The generated runtime uses Office 2007 as the compatibility baseline and includes newer OOXML namespaces and package relationships from later Office generations, including Office 2010, 2013, 2016, 2019, 2021, Microsoft 365-era additions, and newer upstream namespace revisions present in the checked-in metadata.
In practice this includes later DrawingML and chart extensions, SVG and 3D-related parts, threaded comments, dynamic-array-era spreadsheet extensions, and other post-2007 additions tracked by Open XML SDK metadata.
This means newer package parts and schema types are available from the Rust crate, but document validity still depends on the XML you construct and the target applications that will consume the file.
Design considerations
Before using ooxmlsdk, be clear about the level of abstraction it provides.
ooxmlsdk works with Office Open XML packages and generated schema types. It does not behave like Word, Excel, PowerPoint, or a full document layout engine.
What ooxmlsdk does
- Opens and saves OOXML packages such as
.docx,.xlsx, and.pptx. - Exposes strongly typed package parts such as
WordprocessingDocument,SpreadsheetDocument, andPresentationDocument. - Parses XML parts into generated Rust schema structs.
- Serializes generated schema structs back to XML.
- Preserves and round-trips package parts and relationships through the package model.
What ooxmlsdk does not do
- It does not replace the Office application object models.
- It does not convert documents to or from formats such as HTML, PDF, XPS, or images.
- It does not calculate Word layout, paginate documents, refresh spreadsheet data, or recalculate Excel formulas.
- It does not guarantee that arbitrary generated XML is valid for every target Office version.
- It does not hide the OOXML package structure; you still need to understand parts, relationships, content types, and the relevant schema.
- It does not automatically repair files that an Office application would repair interactively.
Rust API expectations
Use normal Rust error handling around package operations. Open, parse, and save calls can fail because input packages may be malformed, relationships may point to missing parts, XML may not match the generated schema, or the output writer may fail.
Keep ownership explicit. Load a package into a document type, mutate typed parts or root elements through &mut bindings, then call save with an output writer or file path flow that your application owns. When direct XML access is unavoidable, treat it as package-level editing and revalidate the affected parts.
Generated schema fields model OOXML values, not only primitive Rust values. Boolean-like attributes, measurements, coordinates, and percentages may use simple_type or units wrappers so unknown lexical forms, compatibility values, and unit categories can round-trip correctly.
When you only need package read/write APIs, the default parts feature is enough. Enable optional features deliberately:
- Use
flat-opconly when you need Flat OPC XML package representations. - Use
mceonly when you want Markup Compatibility and Extensibility processing during package open and root loading. - Enable
validatorswhen you need structured validation diagnostics for generated schema roots or loaded package roots.
Packages and general APIs
This section covers package-level ooxmlsdk APIs that apply across WordprocessingML, SpreadsheetML, and PresentationML documents.
Start here when you need to understand how the Rust crate opens packages, navigates parts and relationships, handles optional feature flags, or works with raw package content.
In this section
- Cargo feature flags
- Introduction to markup compatibility
- Add a new document part that receives a relationship ID to a package
- Add a new document part to a package
- Copy the contents of an Open XML package part to a document part in a different package
- Create a package
- Get the contents of a document part from a package
- Remove a document part from a package
- Replace the theme part in a word processing document
- Search and replace text in a document part
- Diagnostic IDs
Related sections
Cargo feature flags
ooxmlsdk uses Cargo features to control optional API surface.
This page is about Rust crate features. The upstream SDK feature-collection extension model does not have a direct equivalent in ooxmlsdk.
Cargo features are compile-time switches. They choose which optional modules and dependencies are built into your binary; they are not per-package state, event hooks, or service registrations.
Default features
Most users can use the crate with its default features:
[dependencies]
ooxmlsdk = "0.10.2"
The default feature set enables parts, which provides package-level read/write APIs such as:
ooxmlsdk::parts::wordprocessing_document::WordprocessingDocumentooxmlsdk::parts::spreadsheet_document::SpreadsheetDocumentooxmlsdk::parts::presentation_document::PresentationDocument
parts
Enable parts when you want package APIs but want to disable other default behavior explicitly:
[dependencies]
ooxmlsdk = { version = "0.10.2", default-features = false, features = ["parts"] }
This is the minimum feature for reading and writing .docx, .xlsx, and .pptx packages through the strongly typed package model.
flat-opc
Enable flat-opc when you need Flat OPC XML package representations:
[dependencies]
ooxmlsdk = { version = "0.10.2", default-features = false, features = ["flat-opc"] }
This feature also enables parts.
mce
Enable mce when you want Markup Compatibility and Extensibility processing during package open and root loading:
[dependencies]
ooxmlsdk = { version = "0.10.2", default-features = false, features = ["mce"] }
This feature also enables parts.
validators
The validators feature exposes optional validation APIs:
[dependencies]
ooxmlsdk = { version = "0.10.2", features = ["validators"] }
Validation support is opt-in so projects that only need package parsing, modification, and round-tripping do not need to compile validator dependencies. With validators enabled, generated schema roots implement SdkValidator, and package types such as WordprocessingDocument can validate loaded root elements and return ValidationErrorInfo diagnostics.
Not yet modeled
The upstream runtime feature collection includes behaviors such as package events, part events, disposal callbacks, random-number services, and automatic paragraph ID generation. Those are not exposed as ooxmlsdk APIs. When porting examples that depend on those services, write the state management explicitly in Rust or leave the page unchanged until a tested crate API exists.
Introduction to markup compatibility
Markup Compatibility and Extensibility, usually shortened to MCE, is the Open XML mechanism for handling markup that may not be understood by every application or file-format version.
For example, a Word document can contain an mc:AlternateContent element with a newer choice and an older fallback. A consumer chooses the content it understands and ignores or removes markup that is outside its target compatibility set.
ooxmlsdk exposes MCE processing behind the mce Cargo feature.
Enable MCE support
Add the feature to your manifest:
[dependencies]
ooxmlsdk = { version = "0.10.2", default-features = false, features = ["mce"] }
The mce feature also enables parts.
Open settings
MCE behavior is configured through OpenSettings. The relevant types are MarkupCompatibilityProcessSettings, MarkupCompatibilityProcessMode, and FileFormatVersion.
The available process modes are:
NoProcess: do not process MCE markup. This is the default.ProcessLoadedPartsOnly: process MCE markup for loaded parts.ProcessAllParts: process all parts. This forces eager root loading because every part must be considered.
ProcessLoadedPartsOnly and ProcessAllParts are only available when the mce feature is enabled.
The following settings match the upstream example that preprocesses every package part for an Office 2007 target:
#![allow(unused)] fn main() { use ooxmlsdk::sdk::{ FileFormatVersion, MarkupCompatibilityProcessMode, MarkupCompatibilityProcessSettings, OpenSettings, }; pub fn process_all_parts_for_office_2007() -> OpenSettings { OpenSettings { markup_compatibility_process_settings: MarkupCompatibilityProcessSettings { process_mode: MarkupCompatibilityProcessMode::ProcessAllParts, target_file_format_version: FileFormatVersion::Office2007, }, ..Default::default() } } }
Target file format version
MarkupCompatibilityProcessSettings also includes target_file_format_version. This value tells the processor which Office-era namespaces should be treated as understood.
Available values include:
Office2007Office2010Office2013Office2016Office2019Office2021Microsoft365
The default target is Office2007.
Setting the target to Office2013, for example, means Office 2010 and Office 2013 era namespaces are treated as understood, while later namespaces are still candidates for compatibility processing.
Saving after processing
MCE processing changes the loaded root elements. If you save a package after processing, the saved package reflects the processed content. Use NoProcess when you want to inspect or round-trip MCE markup without filtering it.
Without the mce feature, the generated XML reader/writer still preserves common compatibility markup for stable round trips, including mc:* attributes and mc:AlternateContent. The feature is needed when your application wants the crate to actively choose compatibility branches and filter unknown content during loading.
Add a new document part with a relationship ID
Some package edits need a caller-provided relationship ID. ooxmlsdk supports this pattern for many typed child parts through *_with_id methods.
The upstream sample creates a WordprocessingML package and assigns specific relationship IDs to newly added parts. In Rust, use the same idea when the relationship ID is part of an external contract; otherwise, let the crate allocate IDs for you.
Relationship IDs
Open XML relationships are identified by strings such as rId1. Relationship IDs are scoped to the package or part that owns the relationship file. A relationship ID under word/document.xml is separate from a relationship ID under another part.
When you do not care about the exact ID, prefer the auto-ID methods. They avoid collisions with existing relationships.
When interoperating with code that expects a specific relationship ID, use a *_with_id method and handle the error if that ID is already in use.
Add a part with an explicit ID
#![allow(unused)] fn main() { pub fn add_custom_xml_part_with_id( path: &Path, relationship_id: &str, xml: &[u8], ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file(path)?; let main_part = document.main_document_part()?; let custom_xml_part = main_part.add_custom_xml_part_with_id(&mut document, "application/xml", relationship_id)?; custom_xml_part.set_data(&mut document, xml.to_vec())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The method creates both the part and the relationship from main_part to the new part. The corresponding test reopens the package and asserts that the custom XML part uses the requested relationship ID.
Notes
set_data is the Rust equivalent of filling the payload for a raw document part. The bytes you write should already be valid for the part content type; ooxmlsdk does not infer a schema root for arbitrary Custom XML data.
For package-level parts such as core properties, extended properties, custom properties, thumbnails, or the main document part, use the typed package methods when they exist. Use generic add_new_part only after checking that the generated part type has the relationship and content-type metadata you need.
Add a new document part to a package
This example adds a Custom XML part to the main document part of a WordprocessingML package.
Open XML packages are made of parts and relationships. A document part is not just a file in the ZIP archive; it also needs a relationship from its owning package or parent part. In ooxmlsdk, typed part methods create the new part and its relationship together.
Add a Custom XML part
The example opens an existing .docx, gets the main document part, adds a Custom XML part, writes XML bytes into that part, and saves the package to memory.
#![allow(unused)] fn main() { pub fn add_custom_xml_part(path: &Path, xml: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file(path)?; let main_part = document.main_document_part()?; let custom_xml_part = main_part.add_custom_xml_part(&mut document, "application/xml")?; custom_xml_part.set_data(&mut document, xml.to_vec())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The key calls are:
WordprocessingDocument::new_from_file, which opens the package.main_document_part, which gets the required main document part.add_custom_xml_part, which creates a child part relationship from the main document part.set_data, which replaces the part payload.save, which writes the updated package to a writer.
The function returns the updated package bytes so the caller can write them to disk, upload them, or reopen them for further processing.
Relationship behavior
add_custom_xml_part chooses a new relationship ID automatically. Use the corresponding *_with_id methods when you need to control the relationship ID explicitly.
Copy part contents to a part in another package
This example copies the raw data from the theme part in one WordprocessingML package to the theme part in another package.
Theme parts are optional. The example returns Ok(None) when either package does not have a theme part.
Theme parts
A Theme part stores a document theme: color scheme, font scheme, and effect scheme. In WordprocessingML, a theme affects heading colors and styles; in SpreadsheetML, it affects cell and chart formatting; in PresentationML, it affects slides, handouts, notes, and masters.
WordprocessingML and SpreadsheetML packages can have zero or one Theme part associated with the main document or workbook part. PresentationML packages can have Theme parts associated with the presentation, slide masters, notes masters, or handout masters.
Copy the theme part
#![allow(unused)] fn main() { pub fn copy_theme_part( source_path: &Path, target_path: &Path, ) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> { let settings = OpenSettings { open_mode: PackageOpenMode::Lazy, ..Default::default() }; let source = WordprocessingDocument::new_from_file_with_settings(source_path, settings)?; let mut target = WordprocessingDocument::new_from_file_with_settings(target_path, settings)?; let source_main = source.main_document_part()?; let target_main = target.main_document_part()?; let Some(source_theme) = source_main.theme_part(&source) else { return Ok(None); }; let Some(target_theme) = target_main.theme_part(&target) else { return Ok(None); }; let theme_data = source_theme.data_to_vec(&source).unwrap_or_default(); target_theme.set_data(&mut target, theme_data)?; let mut buffer = Cursor::new(Vec::new()); target.save(&mut buffer)?; Ok(Some(buffer.into_inner())) } }
The function opens the source package and target package, finds each main document part, looks up each theme part, copies the source bytes, and saves the updated target package to memory.
This is a raw part-data copy. It does not parse or validate the theme XML.
When to copy raw part data
Raw part copying is useful when:
- You want to preserve a part exactly as stored.
- You do not need to inspect or modify the XML structure.
- The source and target part types are the same.
Use generated schema root elements instead when you need to read or modify specific XML elements or attributes.
Before running this workflow on real files, make sure the source document actually has a Theme part. If the target lacks one, create it first, as shown in Replace the theme part in a word processing document.
Create a package
Open XML files are ZIP-based packages. A valid package needs content types, relationships, and at least the required root part for the document category you are creating.
In ooxmlsdk, package read/write APIs are available through types such as WordprocessingDocument, SpreadsheetDocument, and PresentationDocument. Use each document type's create(...) constructor to start a new package, add the required main part and child parts, then save to the file or writer your application owns.
Rust workflow
The recommended documented workflow is:
- Pick the package family and document type.
- Create the package with
WordprocessingDocument::create,SpreadsheetDocument::create, orPresentationDocument::create. - Add the required main part and any child parts.
- Set part bytes or generated root elements.
- Save the package to a writer or file.
The domain-specific create chapters show minimal package writers:
- Create a word processing document by providing a file name
- Create a spreadsheet document by providing a file name
- Create a presentation document by providing a file name
#![allow(unused)] fn main() { use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::theme_part::ThemePart; use ooxmlsdk::parts::wordprocessing_document::WordprocessingDocument; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, is_encrypted_office_file_path}; pub fn round_trip_word_document(path: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file(path)?; let main_part = document.main_document_part().expect("main document part"); assert!(document.get_id_of_part(&main_part).is_some()); let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Document type and extension
The document type controls the content type written for the main part. Keep it aligned with the extension you persist:
| Family | Normal | Template | Macro-enabled |
|---|---|---|---|
| WordprocessingML | .docx with WordprocessingDocumentType::Document | .dotx with Template | .docm / .dotm with macro-enabled variants |
| SpreadsheetML | .xlsx with SpreadsheetDocumentType::Workbook | .xltx with Template | .xlsm, .xltm, or .xlam with macro-enabled variants |
| PresentationML | .pptx with PresentationDocumentType::Presentation | .potx with Template | .pptm, .potm, .ppsm, or .ppam with macro-enabled variants |
Office applications can reject a package whose extension does not match its main part content type.
Templates
Use create_from_template when a .dotx, .xltx, or .potx should become an editable regular document package. The method opens the template, changes the package document type to the default regular type for that family, and preserves the package content for further mutation and saving.
WordprocessingML structure
The minimum main document part for a word-processing package is a w:document root element with a w:body child:
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body/>
</w:document>
The corresponding generated Rust types live under:
ooxmlsdk::schemas::schemas_openxmlformats_org_wordprocessingml_2006_main
For more about WordprocessingML package structure, see Structure of a WordprocessingML document.
Get the contents of a document part from a package
This example reads the raw XML payload of the Wordprocessing comments part from a .docx package.
The comments part is an optional child of the main document part. In Rust, that optional relationship is represented as Option.
Comments element
The comments part root is w:comments. It contains the comments defined in the current WordprocessingML document. A typical part contains zero or more w:comment children:
<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:comment w:id="0" w:author="Author">
<!-- comment content -->
</w:comment>
</w:comments>
The schema shape is a sequence of comment elements with minOccurs="0" and maxOccurs="unbounded".
Read the comments part
#![allow(unused)] fn main() { pub fn read_comments_part(path: &Path) -> Result<Option<String>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file(path)?; let main_part = document.main_document_part()?; let Some(comments_part) = main_part.wordprocessing_comments_part(&document) else { return Ok(None); }; Ok(comments_part.data_as_str(&document)?.map(str::to_owned)) } }
The function returns:
Ok(Some(xml))when the package has a comments part and the part data is valid UTF-8.Ok(None)when the main document part has no comments part relationship.Err(_)when the package cannot be opened, the main document part is missing, or the part data cannot be read as text.
Raw part data vs. root elements
Use raw part data when you need to inspect or copy XML exactly as stored in the package.
Use root_element when you want ooxmlsdk to parse the part into the generated schema type for that part. Parsing is better when you want typed access to elements and attributes; raw data is better for simple pass-through and diagnostics.
Remove a document part from a package
This example removes the Wordprocessing settings part from a .docx package.
In Open XML, removing a child part means removing the relationship from the parent part and marking the target part as deleted in the package model. ooxmlsdk handles that through delete_part.
Settings element
The document settings part root is w:settings. It stores settings that apply to the WordprocessingML document, such as default tab stops or character spacing behavior:
<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:defaultTabStop w:val="720"/>
<w:characterSpacingControl w:val="dontCompress"/>
</w:settings>
Removing the settings part removes the part relationship and the part payload from the saved package. It does not rewrite document body content.
Remove the settings part
#![allow(unused)] fn main() { pub fn remove_settings_part(path: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file(path)?; let main_part = document.main_document_part()?; if let Some(settings_part) = main_part.document_settings_part(&document) { main_part.delete_part(&mut document, settings_part)?; } let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The function:
- Opens a WordprocessingML package.
- Gets the main document part.
- Checks whether the optional settings part exists.
- Deletes it from the main document part if present.
- Saves the updated package to memory.
If the settings part is not present, the function leaves the package unchanged and still returns saved package bytes.
Optional parts
Many Open XML parts are optional. In ooxmlsdk, optional child-part accessors return Option<T>, so callers should handle both the present and absent cases explicitly.
Replace the theme part in a word-processing document
This example replaces the WordprocessingML package's theme part with caller-provided theme XML.
A theme part contains DrawingML theme information such as color scheme, font scheme, and format scheme. In a word-processing document, the main document part can have an optional relationship to a theme part.
The same theme model is used across Office document families. A theme can affect fonts, colors, fills, backgrounds, and effects for themed objects.
Theme element structure
The root element is a:theme, represented by ooxmlsdk::schemas::a::Theme. Its themeElements child is required and carries the base color, font, and format schemes. Other children are optional:
| XML element | Rust type | Purpose |
|---|---|---|
a:themeElements | ooxmlsdk::schemas::a::ThemeElements | Base theme formatting |
a:objectDefaults | ooxmlsdk::schemas::a::ObjectDefaults | Default formatting for objects |
a:extraClrSchemeLst | ooxmlsdk::schemas::a::ExtraColorSchemeList | Additional color schemes |
a:custClrLst | ooxmlsdk::schemas::a::CustomColorList | Custom colors |
a:extLst | ooxmlsdk::schemas::a::OfficeStyleSheetExtensionList | Future extensibility |
Replace the theme XML
#![allow(unused)] fn main() { pub fn replace_theme_part( path: &Path, theme_xml: &[u8], ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let settings = OpenSettings { open_mode: PackageOpenMode::Lazy, ..Default::default() }; let mut document = WordprocessingDocument::new_from_file_with_settings(path, settings)?; let main_part = document.main_document_part()?; let theme_part = match main_part.theme_part(&document) { Some(theme_part) => theme_part, None => main_part.add_new_part_auto_id::<_, ThemePart>(&mut document)?, }; theme_part.set_data(&mut document, theme_xml.to_vec())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The function:
- Opens an existing
.docx. - Gets the main document part.
- Uses the existing theme part when present.
- Adds a theme part when the main document part does not have one.
- Writes the provided XML bytes into the theme part.
- Saves the updated package to memory.
The theme_xml argument must be valid theme part XML. set_data writes bytes to the part; it does not prove that the XML is semantically valid for every Office version.
You can extract a valid theme XML payload from another .docx or .thmx file by treating the file as a ZIP package and locating its theme part.
Theme part relationship
The theme relationship type is:
http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme
For WordprocessingML packages, the theme part is usually stored under word/theme/theme1.xml, but callers should use the part relationship rather than hard-coding the ZIP path.
Search and replace text in a document part
This example performs a simple string replacement against the raw XML of the main document part.
This approach is intentionally limited. It can be useful for diagnostics or controlled fixtures, but it is not a robust WordprocessingML editing strategy. A search string can cross run boundaries, appear in attributes, or accidentally match XML markup.
Simple raw XML replacement
#![allow(unused)] fn main() { pub fn search_and_replace_main_document( path: &Path, search: &str, replacement: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let settings = OpenSettings { open_mode: PackageOpenMode::Lazy, ..Default::default() }; let mut document = WordprocessingDocument::new_from_file_with_settings(path, settings)?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = xml.replace(search, replacement); main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The function:
- Opens a
.docx. - Reads the main document part as UTF-8 XML.
- Applies
str::replace. - Writes the updated XML bytes back to the same part.
- Saves the package to memory.
Safer approaches
For production document editing, prefer schema-aware traversal and updates where possible. Word text may be split across multiple runs (w:r) and text nodes (w:t), so a naive string replacement can miss visible text or corrupt the part XML.
Use this raw replacement pattern only when you control the input shape.
Diagnostic IDs
This page originally documented compiler diagnostic IDs used by the upstream SDK.
ooxmlsdk does not currently define custom Rust diagnostic IDs for obsolete or experimental APIs. Rust users should rely on the normal Rust compiler diagnostics, Cargo feature errors, crate documentation, and ooxmlsdk::common::SdkError values returned at runtime.
The upstream OOXML0001 diagnostic is specific to an experimental .NET package abstraction and does not apply to this Rust crate.
Common Rust diagnostics
Typical issues while using ooxmlsdk include:
- Missing Cargo features, such as using
ooxmlsdk::partswithout enablingparts. - Calling an MCE process mode without enabling the
mcefeature. - Treating optional parts as always present instead of handling
Option. - Ignoring
Resultfrom package open, parse, write, and save operations.
Runtime errors
Package operations return Result and may fail because:
- The input file is not a valid ZIP package.
[Content_Types].xmlis missing or invalid.- A required relationship target is missing.
- XML does not match the generated schema type being parsed.
- Output writing fails.
Handle these cases with normal Rust error propagation, usually by returning Result from your own helper and using ? on package operations.
Validator errors
Where validator APIs are available, they return ValidationErrorInfo values. Those values include a validation category and, where available, an ID derived from the failed validator, such as required, enum, or field_value. Treat these as runtime validation data rather than compiler diagnostics.
Presentations
This section covers PresentationML packages (.pptx, .pptm, .potx) with ooxmlsdk.
Presentation packages are made of a presentation part, slide parts, slide masters, layouts, notes, themes, media, comments, and relationships between those parts. In ooxmlsdk, the entry point is usually ooxmlsdk::parts::presentation_document::PresentationDocument.
Use the parts feature, enabled by default, to open and save packages. Examples in this section are backed by tested Rust code in listings/presentation.
In this section
- Structure of a PresentationML document
- Open a presentation document for read-only access
- Retrieve the number of slides in a presentation document
- Get all the text in a slide in a presentation
- Get all the text in all slides in a presentation
- Get the titles of all the slides in a presentation
- Working with presentations
- Working with presentation slides
- Working with slide layouts
- Working with slide masters
- Working with notes slides
- Working with handout master slides
- Working with comments
- Working with animation
- Add an audio file to a slide in a presentation
- Add a video to a slide in a presentation
- Add a transition to a slide in a presentation
- Apply a theme to a presentation
- Change the fill color of a shape in a presentation
- Create a presentation document by providing a file name
- Delete a slide from a presentation
- Move a slide to a new position in a presentation
- Move a paragraph from one presentation to another
Related sections
Add an audio file to a slide in a presentation
Audio in PresentationML is stored as media data plus slide relationships and XML markup that binds the media to a shape. The visible slide usually contains a picture or shape; the media relationship supplies the actual audio file.
Package shape
A slide that references audio can contain markup like this:
<p:pic>
<p:nvPicPr>
<p:cNvPr id="7" name="audio">
<a:hlinkClick r:id="" action="ppaction://media"/>
</p:cNvPr>
<p:nvPr>
<a:audioFile r:link="rAudio1"/>
</p:nvPr>
</p:nvPicPr>
</p:pic>
The r:link value points to a relationship on the slide part. The package also needs a media data part, a media reference relationship, and usually an image or shape used as the clickable placeholder.
audioFile is stored under the non-visual properties of the picture or shape. The object appears on the slide like normal drawing content, but actual playback is controlled from the slide timing tree. The drawing object's non-visual ID is what ties the visible placeholder to the media timing node.
The complete shape normally includes:
p:cNvPr, including ana:hlinkClickaction ofppaction://media,p:cNvPicPr, often with picture locks,p:nvPrwitha:audioFile,- a media relationship for the audio data,
- an image relationship and
a:blipwhen a picture is used as the placeholder, - shape properties such as
a:off,a:stretch, anda:fillRect.
Rust workflow
Use ooxmlsdk to open the presentation and find the target slide. The package-side media operation is tested here: create a media data part, write the audio bytes, and add slide-level audio and media reference relationships.
#![allow(unused)] fn main() { pub fn add_audio_media_references( path: &Path, audio_bytes: &[u8], ) -> Result<(Vec<u8>, String, String), Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let Some(slide_part) = presentation_part.slide_parts(&document).next() else { return Err("presentation has no slide parts".into()); }; let media_part = document.create_media_data_part_by_type(MediaDataPartType::Wav)?; media_part.set_data(&mut document, audio_bytes.to_vec())?; let audio_relationship_id = slide_part.add_audio_reference_relationship(&mut document, &media_part)?; let media_relationship_id = slide_part.add_media_reference_relationship(&mut document, &media_part)?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(( buffer.into_inner(), audio_relationship_id, media_relationship_id, )) } }
The listing covers the package-side media relationships. A complete PowerPoint-compatible writer must verify all of these together:
- media data part content type and extension,
- slide audio and media reference relationships,
- placeholder shape or picture XML,
- timing markup for playback,
- package save and PowerPoint compatibility.
Use this chapter as the package map for implementing and testing audio insertion: keep relationship creation, placeholder XML, and timing XML in one save operation so the slide remains internally consistent.
Add a comment to a slide in a presentation
Comments are stored in comment parts related to slides, with author information stored separately at the presentation level. Adding a comment is therefore a package graph update, not just a text edit in the slide XML.
This page describes modern PowerPoint comments. Classic comments use a different, older package shape and should be tested separately before reusing the same writer logic.
Comment parts
A comment list can look like this:
<p:cmLst xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:cm authorId="0" dt="2026-05-04T10:00:00Z" idx="1">
<p:pos x="10" y="20"/>
<p:text>Review this slide.</p:text>
</p:cm>
</p:cmLst>
The matching author entry is stored in a presentation-level comment authors part.
Add-comment workflow
The package update has these steps:
- Open the presentation and get the presentation part.
- Find or create the presentation-level comment authors part.
- Find an existing author by name and initials, or append a new author and assign an ID.
- Locate the target slide by slide ID or slide relationship.
- Find or create the slide's PowerPoint comments part.
- Append the comment with author ID, timestamp, position, and text.
- Ensure the slide has the extension list entries required by modern comments.
Rust workflow
Create or reuse the classic comment authors part, create or reuse the slide comments part, then append a comment:
#![allow(unused)] fn main() { pub fn add_comment_to_slide( path: &Path, slide_index: usize, author_name: &str, initials: &str, text: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let authors_part = if let Some(part) = presentation_part.comment_authors_part(&document) { part } else { presentation_part.add_new_part_auto_id::<_, CommentAuthorsPart>(&mut document)? }; let authors_xml = authors_part .data_as_str(&document)? .map(str::to_string) .filter(|xml| !xml.trim().is_empty()) .unwrap_or_else(empty_comment_authors_xml); let (updated_authors_xml, author_id, comment_index) = upsert_comment_author(&authors_xml, author_name, initials)?; authors_part.set_data(&mut document, updated_authors_xml.into_bytes())?; let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else { return Err("slide index out of range".into()); }; let comments_part = if let Some(part) = slide_part.slide_comments_part(&document) { part } else { slide_part.add_new_part_auto_id::<_, SlideCommentsPart>(&mut document)? }; let comments_xml = comments_part .data_as_str(&document)? .map(str::to_string) .filter(|xml| !xml.trim().is_empty()) .unwrap_or_else(empty_slide_comments_xml); let updated_comments_xml = append_slide_comment(&comments_xml, author_id, comment_index, text)?; comments_part.set_data(&mut document, updated_comments_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Modern PowerPoint comments can also use newer Office extension parts. Keep modern threaded comments separate from the classic p:cmLst path shown above.
When matching author data, use the exact name and initials PowerPoint stores in the file, usually the values from PowerPoint Options under the General tab.
Reply to a comment in a presentation
Comment replies are more complex than simple comment insertion because PowerPoint files may use modern comment extension parts in addition to classic PresentationML comment lists.
Model notes
Classic comments are stored as <p:cm/> entries in a slide comment part. Modern comments and replies can involve Office extension namespaces and additional relationship targets. A reply must preserve author identity, timestamps, threading metadata, and the relationship between the slide and its comment parts.
Reply workflow
The upstream modern-comments sample follows this shape:
- Open the presentation and find or create the comment authors part.
- Match the reply author by name and initials, or create a new author record.
- Locate the first or target slide part and read its comment parts.
- Iterate existing comments and choose the comment that should receive a reply.
- Find or create that comment's reply list.
- Append the reply text with the author ID and timestamp.
Rust workflow
Classic PresentationML comments do not have a reply subtree. For classic comments, add another comment by the reply author and preserve the author/comment ids:
#![allow(unused)] fn main() { pub fn add_comment_to_slide( path: &Path, slide_index: usize, author_name: &str, initials: &str, text: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let authors_part = if let Some(part) = presentation_part.comment_authors_part(&document) { part } else { presentation_part.add_new_part_auto_id::<_, CommentAuthorsPart>(&mut document)? }; let authors_xml = authors_part .data_as_str(&document)? .map(str::to_string) .filter(|xml| !xml.trim().is_empty()) .unwrap_or_else(empty_comment_authors_xml); let (updated_authors_xml, author_id, comment_index) = upsert_comment_author(&authors_xml, author_name, initials)?; authors_part.set_data(&mut document, updated_authors_xml.into_bytes())?; let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else { return Err("slide index out of range".into()); }; let comments_part = if let Some(part) = slide_part.slide_comments_part(&document) { part } else { slide_part.add_new_part_auto_id::<_, SlideCommentsPart>(&mut document)? }; let comments_xml = comments_part .data_as_str(&document)? .map(str::to_string) .filter(|xml| !xml.trim().is_empty()) .unwrap_or_else(empty_slide_comments_xml); let updated_comments_xml = append_slide_comment(&comments_xml, author_id, comment_index, text)?; comments_part.set_data(&mut document, updated_comments_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
For modern threaded comments, use the modern PowerPoint comment and author parts (PowerPointCommentPart and PowerPointAuthorsPart) and preserve the threading metadata. Prefer modifying a fixture that already contains a modern comment and reply so the package structure is known-good.
Add a video to a slide in a presentation
Video in PresentationML follows the same package pattern as audio: a slide contains markup for a clickable visual object, and relationships connect that object to media data in the package.
Package shape
A slide can reference a video file from non-visual picture properties:
<p:pic>
<p:nvPicPr>
<p:cNvPr id="7" name="video">
<a:hlinkClick r:id="" action="ppaction://media"/>
</p:cNvPr>
<p:nvPr>
<a:videoFile r:link="rVideo1"/>
</p:nvPr>
</p:nvPicPr>
</p:pic>
The slide part relationship item resolves rVideo1 to the stored media data. A complete PowerPoint-compatible video insertion normally also includes a preview image, media reference relationship, timing data, and shape geometry.
videoFile is defined inside the non-visual properties of the picture or shape. The visible object sits on the slide like any other drawing object, while playback is described in the slide timing tree. The non-visual drawing ID is used by that timing data to refer back to the media object.
The video timing schema uses CT_TLMediaNodeVideo, whose common media node child is required and whose fullScrn attribute defaults to false.
The full insertion shape normally includes:
p:cNvPr, including ana:hlinkClickaction ofppaction://media,p:cNvPicPrand picture locks,p:nvPrwitha:videoFile,- a video relationship to the media data part,
- a media reference relationship,
- an image part plus
a:blipfor the preview frame, - shape properties such as
a:off,a:stretch, anda:fillRect.
Rust workflow
Create an MP4 media data part, add video and media reference relationships from the target slide, and insert the video picture markup:
#![allow(unused)] fn main() { pub fn add_video_to_slide( path: &Path, slide_index: usize, video_bytes: &[u8], ) -> Result<(Vec<u8>, String, String), Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else { return Err("slide index out of range".into()); }; let media_part = document.create_media_data_part_by_type(MediaDataPartType::Mp4)?; media_part.set_data(&mut document, video_bytes.to_vec())?; let video_relationship_id = slide_part.add_video_reference_relationship(&mut document, &media_part)?; let media_relationship_id = slide_part.add_media_reference_relationship(&mut document, &media_part)?; let slide_xml = slide_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = insert_video_picture(slide_xml, &video_relationship_id, &media_relationship_id)?; slide_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(( buffer.into_inner(), video_relationship_id, media_relationship_id, )) } }
For production presentations, add a preview image and timing tree entries if the target application requires them. The example above covers the package media part, video relationship, media relationship, and slide-level a:videoFile markup.
Apply a theme to a presentation
A presentation theme is stored in a theme part rooted at <a:theme/>. Applying a theme means copying or replacing the target presentation's theme part and keeping relationships valid.
Theme structure
The theme root contains theme elements, optional object defaults, extra color schemes, custom colors, and extensions.
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office">
<a:themeElements>
<a:clrScheme name="Office"/>
<a:fontScheme name="Office"/>
<a:fmtScheme name="Office"/>
</a:themeElements>
</a:theme>
In a PresentationML package, the presentation part usually owns the theme relationship.
The generated Rust root type is ooxmlsdk::schemas::a::Theme. Its main child types correspond to the theme schema:
| XML element | Rust type | Purpose |
|---|---|---|
a:themeElements | ooxmlsdk::schemas::a::ThemeElements | Base color, font, and format schemes |
a:objectDefaults | ooxmlsdk::schemas::a::ObjectDefaults | Object defaults |
a:extraClrSchemeLst | ooxmlsdk::schemas::a::ExtraColorSchemeList | Extra color schemes |
a:custClrLst | ooxmlsdk::schemas::a::CustomColorList | Custom colors |
a:extLst | ooxmlsdk::schemas::a::OfficeStyleSheetExtensionList | Extensibility |
Full presentation theme replacement
The upstream sample does more than copy theme1.xml: it copies a slide master from a source presentation, reuses the target slide master relationship ID, copies the source theme, then relinks every slide to a layout of the same type. If a slide has no matching layout type, the sample uses a default layout such as "Title and Content".
That workflow matters because a presentation theme is normally coupled with slide masters and layouts. Replacing only the theme part can leave slides pointing at layouts whose placeholder geometry, fonts, or colors no longer match the intended design.
Rust workflow
Open the source presentation read-only, open or copy the target package, and use the presentation part's theme accessor to read the theme data. The general package navigation pattern is:
#![allow(unused)] fn main() { use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::comment_authors_part::CommentAuthorsPart; use ooxmlsdk::parts::presentation_document::PresentationDocument; use ooxmlsdk::parts::presentation_part::PresentationPart; use ooxmlsdk::parts::slide_comments_part::SlideCommentsPart; use ooxmlsdk::parts::slide_part::SlidePart; use ooxmlsdk::sdk::MediaDataPartType; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, PresentationDocumentType}; pub fn open_presentation_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; Ok(presentation_part.slide_parts(&document).count()) } }
For a full Rust writer, verify these invariants before saving:
- source theme part exists,
- target theme relationship is added or replaced,
- dependent slide masters and layouts still resolve their theme references,
- the saved package opens in PowerPoint or another strict consumer.
The WordprocessingML theme replacement example in the General section shows the package-level copy-and-replace pattern. PresentationML theme application has a larger package surface because slide masters and layouts are part of the visual result.
Change the fill color of a shape in a presentation
Shape fill color is stored in the slide XML, usually under a shape's <p:spPr/> properties. A solid fill uses DrawingML color markup such as <a:solidFill/>.
Shape fill markup
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Accent shape"/>
</p:nvSpPr>
<p:spPr>
<a:solidFill>
<a:srgbClr val="FF0000"/>
</a:solidFill>
</p:spPr>
</p:sp>
The val attribute stores the RGB color as six hexadecimal digits.
Shape tree
Slide content lives under the shape tree (p:spTree). It contains the non-visual group properties, group shape properties, and then zero or more drawing objects:
| Element | Meaning |
|---|---|
p:sp | Shape |
p:grpSp | Group shape |
p:graphicFrame | Graphic frame |
p:cxnSp | Connection shape |
p:pic | Picture |
p:extLst | Extension list |
The upstream sample changes the first shape on the first slide, so the test file must contain at least one shape. A production writer should select by a stable shape ID or name instead.
Rust workflow
#![allow(unused)] fn main() { pub fn change_first_shape_fill_color( path: &Path, slide_index: usize, rgb_hex: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else { return Err(format!("slide index {slide_index} not found").into()); }; let xml = slide_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = set_first_shape_solid_fill(xml, rgb_hex)?; slide_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The listing updates the first shape on the selected slide. For a broader writer, do not use broad text replacement across the whole slide. Parse the slide XML, locate the intended shape by id or name, update only its fill subtree, and then write the part back through the package.
Create a presentation document by providing a file name
Creating a .pptx from scratch requires more than writing ppt/presentation.xml. A valid package also needs content type declarations, package relationships, presentation relationships, slide parts, and any required masters or layouts.
In ooxmlsdk, create the package with PresentationDocument::create(PresentationDocumentType::Presentation), add the presentation part, add slide parts from the presentation part, write the root XML, and save the package to the file or writer your application owns.
Minimal package pieces
A minimal presentation package includes:
[Content_Types].xml,_rels/.relspointing toppt/presentation.xml,ppt/presentation.xml,ppt/_rels/presentation.xml.rels,- one or more
ppt/slides/slideN.xmlparts.
This minimal writer creates a package with one slide relationship in memory:
#![allow(unused)] fn main() { pub fn create_presentation_document() -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::create(PresentationDocumentType::Presentation); let presentation_part = document.add_new_part_auto_id::<PresentationPart>()?; let slide_part = presentation_part.add_new_part_auto_id::<_, SlidePart>(&mut document)?; let slide_relationship_id = presentation_part .get_id_of_part(&document, &slide_part) .expect("slide relationship id") .to_string(); presentation_part.set_data( &mut document, format!( r#"<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="{slide_relationship_id}"/></p:sldIdLst><p:sldSz cx="12192000" cy="6858000"/><p:notesSz cx="6858000" cy="9144000"/></p:presentation>"# ) .into_bytes(), )?; slide_part.set_data( &mut document, br#"<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld></p:sld>"#.to_vec(), )?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
PresentationML roots
The main presentation part has a single p:presentation root. A usable presentation normally needs related slide, slide master, slide layout, and theme parts. The corresponding generated Rust schema types are:
| PresentationML element | Rust type |
|---|---|
p:presentation | ooxmlsdk::schemas::p::Presentation |
p:sld | ooxmlsdk::schemas::p::Slide |
p:sldMaster | ooxmlsdk::schemas::p::SlideMaster |
p:sldLayout | ooxmlsdk::schemas::p::SlideLayout |
a:theme | ooxmlsdk::schemas::a::Theme |
The p:presentation root usually references slide masters, notes masters, handout masters, and slides by relationship IDs. Slide IDs are stored in p:sldIdLst; the relationship ID points to the slide part, while the numeric slide ID is part of the presentation markup.
Production checks
A production presentation writer should still validate:
- package and presentation relationships,
- content type overrides,
- slide id ordering,
- slide master and layout references when required,
- PowerPoint compatibility after save.
For template workflows, PresentationDocument::create_from_template opens a .potx or .potm as an editable presentation package. Use document_type() to inspect the current type and change_document_type(...) when converting the package content type deliberately.
Delete all the comments by an author from all the slides in a presentation
Deleting comments by author requires scanning slide comment parts and matching each comment's author id against the presentation comment authors part.
This page describes modern PowerPoint comments. Classic comments have a different archived package shape and should be handled by a separate tested fixture.
Package model
A presentation comment is a text note attached to a slide. It stores unformatted text, author information, and a slide position. Comments can be visible while editing the presentation, but they are not part of the slide show; the viewing application decides when and how to display them.
The author list maps author ids to names and initials:
<p:cmAuthorLst xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:cmAuthor id="0" name="Ada" initials="AL" lastIdx="3" clrIdx="0"/>
</p:cmAuthorLst>
Slide comment parts then use authorId:
<p:cm authorId="0" idx="1">
<p:text>Review this slide.</p:text>
</p:cm>
The author name must match the user name stored by PowerPoint. In the PowerPoint UI, that value is shown under File, Options, General.
Delete workflow
The upstream modern-comments sample follows this package traversal:
- Open the presentation for editing and get the presentation part.
- Read the comment authors part and find authors whose
namematches the requested author. - Iterate every slide part in the presentation.
- For each slide comment part, remove comments whose
authorIdmatches one of those author IDs. - If a slide comment part becomes empty, remove that comment part relationship.
- Remove the matched author entries from the comment authors part.
Rust workflow
Match classic comment author entries by name, scan slide comments parts, and remove comments with matching author ids:
#![allow(unused)] fn main() { pub fn delete_comments_by_author( path: &Path, author_name: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let Some(authors_part) = presentation_part.comment_authors_part(&document) else { let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; return Ok(buffer.into_inner()); }; let authors_xml = authors_part.data_as_str(&document)?.unwrap_or_default(); let author_ids = comment_author_ids_by_name(authors_xml, author_name); let updated_authors_xml = remove_comment_authors_by_id(authors_xml, &author_ids); authors_part.set_data(&mut document, updated_authors_xml.into_bytes())?; let slide_parts: Vec<_> = presentation_part.slide_parts(&document).collect(); for slide_part in slide_parts { let Some(comments_part) = slide_part.slide_comments_part(&document) else { continue; }; let comments_xml = comments_part.data_as_str(&document)?.unwrap_or_default(); let updated_comments_xml = remove_slide_comments_by_author_id(comments_xml, &author_ids); comments_part.set_data(&mut document, updated_comments_xml.into_bytes())?; } let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Modern threaded comments use different parts and metadata. Apply the same author-id filtering idea there only after adding fixtures for those modern parts.
Delete a slide from a presentation
Deleting a slide is a coordinated edit to ppt/presentation.xml, the presentation relationships, and possibly related notes, comments, media, and custom show data. Removing only the slide XML file leaves dangling relationships or slide ids.
The upstream workflow first counts slides, then deletes a slide by zero-based index. Counting can be read-only; deletion must open the package for editing.
What must change
A slide is listed in <p:sldIdLst/>:
<p:sldIdLst>
<p:sldId id="256" r:id="rId1"/>
<p:sldId id="257" r:id="rId2"/>
</p:sldIdLst>
The relationship id points to a slide part:
<Relationship
Id="rId2"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"
Target="slides/slide2.xml"/>
A safe deletion removes the slide id entry and the relationship, then checks whether related slide resources are still referenced.
Custom shows need special handling. A custom show stores its own slide list by relationship ID, so every reference to the deleted slide relationship must be removed from every custom show before the slide part is deleted.
More complex presentations can have additional references, such as outline view settings or extension data. Treat slide deletion as package graph cleanup, not just an XML list edit.
Rust workflow
Remove the slide id entry and delete the related slide part from the presentation part:
#![allow(unused)] fn main() { pub fn delete_slide( path: &Path, slide_index: usize, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let slides: Vec<_> = presentation_part.slide_parts(&document).collect(); let Some(slide_part) = slides.get(slide_index).cloned() else { return Err("slide index out of range".into()); }; let presentation_xml = presentation_part .data_as_str(&document)? .unwrap_or_default(); let updated_xml = remove_slide_id(presentation_xml, slide_index)?; presentation_part.delete_part(&mut document, slide_part)?; presentation_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
For presentations that use custom shows, notes, comments, or slide-specific media, delete or rewrite those related resources before saving. The example above covers the primary presentation slide list and slide part relationship.
Get all the external hyperlinks in a presentation
Use PresentationDocument to open a .pptx package, iterate the slide parts, and read hyperlink relationships from each slide. In PresentationML, the visible hyperlink markup stores a relationship id such as r:id="rLink1", while the actual URL is stored in the slide part relationship item.
The example opens the presentation lazily and returns only external hyperlink relationships that are referenced by a hyperlink element in slide XML.
Read hyperlink targets
#![allow(unused)] fn main() { pub fn get_external_hyperlinks(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let mut links = Vec::new(); for slide_part in presentation_part.slide_parts(&document) { let xml = slide_part.data_as_str(&document)?.unwrap_or_default(); let hyperlink_ids = extract_hyperlink_relationship_ids(xml); for relationship in slide_part.hyperlink_relationships(&document) { if hyperlink_ids.iter().any(|id| id == relationship.id()) { links.push(relationship.target().to_string()); } } } Ok(links) } }
This follows the package model instead of reading ZIP entries directly:
presentation_part().slide_parts(&document)walks the slides owned by the presentation part.data_as_str(&document)reads the slide XML so the referenced relationship ids can be found.hyperlink_relationships(&document)returns the hyperlink relationships attached to that slide part.relationship.target()is the external URI target stored in the.relsitem.
The helper returns a Vec<String> so callers can decide whether to print, validate, rewrite, or filter the links.
Hyperlink relationship structure
A slide can contain hyperlink markup like this:
<a:hlinkClick r:id="rLink1"/>
The matching relationship item stores the URL:
<Relationship
Id="rLink1"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
Target="https://example.com/intro"
TargetMode="External"/>
The relationship is scoped to the slide part that owns the markup, so the same id can appear in a different part with a different target.
When the relationship id is omitted, the hyperlink has no external relationship target. It can still point inside the same presentation through an anchor. When both an external relationship id and an anchor are present, the relationship target is the external link target for this lookup.
Get all the text in a slide in a presentation
This example reads DrawingML text runs from one slide part in a PresentationML package.
Slide text is normally stored in a:t elements inside shapes, placeholders, tables, SmartArt, charts, and other DrawingML containers. This simple example reads the raw slide XML and extracts a:t element text.
The upstream sample returns one string per paragraph by walking paragraphs and concatenating each paragraph's text runs. The Rust listing below is intentionally smaller: it returns the individual a:t text values in document order for the selected slide.
Read slide text
#![allow(unused)] fn main() { pub fn get_slide_text( path: &Path, slide_index: usize, ) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else { return Ok(Vec::new()); }; let xml = slide_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_drawing_text(xml)) } }
The function:
- Opens the presentation package lazily.
- Gets the presentation part.
- Selects a slide by zero-based index from
slide_parts. - Reads the slide part XML.
- Returns the text values found in
a:telements.
If the index is out of range, it returns an empty vector.
Limitations
This helper is intentionally lightweight. It is suitable for simple extraction and documentation examples, but it is not a full text model for PowerPoint. Production code may need to handle tables, charts, notes, comments, ordering rules, and XML whitespace more carefully.
When you need paragraph-level results, parse the slide root and group runs under each a:p paragraph instead of treating each a:t value as a paragraph.
Get all the text in all slides in a presentation
This example iterates over every slide part in a PresentationML package and extracts DrawingML text from each slide.
Read all slide text
#![allow(unused)] fn main() { pub fn get_all_slide_text(path: &Path) -> Result<Vec<Vec<String>>, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let mut slides = Vec::new(); let slide_parts: Vec<_> = presentation_part.slide_parts(&document).collect(); for slide_part in slide_parts { let xml = slide_part.data_as_str(&document)?.unwrap_or_default(); slides.push(extract_drawing_text(xml)); } Ok(slides) } }
The function returns one Vec<String> per slide. Each inner vector contains the text values found in a:t elements for that slide.
The upstream sample presents the result as slide IDs paired with paragraph strings. This Rust listing keeps the output as nested vectors of extracted DrawingML text values.
This keeps package traversal explicit:
- Open the package with
PresentationDocument. - Get the main presentation part.
- Iterate
presentation_part.slide_parts(&document). - Read each slide part's XML payload.
Ordering
This example uses the order returned by the package relationship model. For workflows that require exact presentation order or slide IDs, inspect the presentation root's slide ID list and resolve each relationship ID in that order.
Limitations
The helper reads visible text in simple a:t elements. It does not interpret PowerPoint layout inheritance, notes pages, charts, SmartArt, comments, or accessibility metadata.
Get the titles of all the slides in a presentation
PowerPoint slide titles are usually represented by placeholder shapes, but real-world files can vary. This simple example treats the first text value found in each slide as that slide's title.
Read slide titles
#![allow(unused)] fn main() { pub fn get_slide_titles(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let titles = get_all_slide_text(path)? .into_iter() .map(|slide_text| slide_text.into_iter().next().unwrap_or_default()) .collect(); Ok(titles) } }
The function builds on the all-slide text helper and returns the first text value from each slide, or an empty string when a slide has no text.
When to use a richer title detector
Use this helper for simple extraction and examples. For production title detection, inspect the slide's shape tree and placeholder metadata so that only title placeholders are treated as titles.
Insert a new slide into a presentation
Inserting a slide requires creating a slide part, adding a presentation relationship, inserting a <p:sldId/> entry at the desired position, and linking the slide to a layout. A valid slide usually depends on an existing slide master and slide layout.
Relationship model
The presentation part orders slides through <p:sldIdLst/>:
<p:sldIdLst>
<p:sldId id="256" r:id="rId1"/>
<p:sldId id="257" r:id="rId2"/>
</p:sldIdLst>
The id value is a presentation slide id. The r:id value resolves to a slide part relationship.
Insert workflow
The upstream sample takes a file path, a zero-based insertion index, and a title string. It then:
- Creates a new
p:sldroot and common slide data. - Adds a title shape and sets its text.
- Adds a body shape and sets its text or placeholder properties.
- Creates a new slide part.
- Inserts a new
p:sldIdentry at the requested index. - Assigns the new slide root to the new slide part.
In a complete package, the new slide should also have a slide layout relationship. Copying the layout relationship from a nearby slide is often safer than inventing one from scratch.
Rust workflow
Create a slide part, optionally reuse an existing layout relationship, and insert a new slide id at the requested position:
#![allow(unused)] fn main() { pub fn insert_new_slide( path: &Path, insertion_index: usize, title: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let slide_count = presentation_part.slide_parts(&document).count(); if insertion_index > slide_count { return Err("insertion index out of range".into()); } let layout_part = presentation_part .slide_parts(&document) .find_map(|slide| slide.slide_layout_part(&document)); let slide_part = presentation_part.add_new_part_auto_id::<_, SlidePart>(&mut document)?; if let Some(layout_part) = layout_part { slide_part.create_relationship_to_part(&mut document, layout_part)?; } slide_part.set_data(&mut document, slide_xml(title).into_bytes())?; let relationship_id = presentation_part .get_id_of_part(&document, &slide_part) .expect("slide relationship id") .to_string(); let presentation_xml = presentation_part .data_as_str(&document)? .unwrap_or_default(); let updated_xml = insert_slide_id(presentation_xml, insertion_index, &relationship_id)?; presentation_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
For rich layouts, copy an existing slide and adjust its content before attempting a fully from-scratch slide. The example above creates a simple title slide and keeps the package graph consistent with the presentation slide list.
Move a slide to a new position in a presentation
Slide order is controlled by the order of <p:sldId/> elements in the presentation part. Moving a slide does not require changing the slide XML; it requires reordering the slide id list while preserving the same relationship ids.
Slide order markup
<p:sldIdLst>
<p:sldId id="256" r:id="rId1"/>
<p:sldId id="257" r:id="rId2"/>
<p:sldId id="258" r:id="rId3"/>
</p:sldIdLst>
Moving the third slide to the first position means moving the rId3 entry before rId1.
The upstream sample treats both from and to as zero-based indexes. It first counts slides, verifies both indexes are in range and different, removes the source p:sldId entry, and inserts that same entry at the target position. The slide part and relationship ID are preserved.
Rust workflow
#![allow(unused)] fn main() { pub fn move_slide_to_position( path: &Path, from_index: usize, to_index: usize, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let xml = presentation_part .data_as_str(&document)? .unwrap_or_default(); let updated_xml = reorder_slide_ids(xml, from_index, to_index)?; presentation_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The listing reorders only the <p:sldId/> children, preserves the slide ids and relationship ids, saves the package, and verifies that slide relationships still resolve.
Move a paragraph from one presentation to another
Paragraph text in slides is stored inside DrawingML text bodies, usually under <a:p/>. Moving a paragraph between presentations means copying XML from one slide part to another and ensuring any referenced resources still exist.
Shape text body
A shape text body contains all visible text and visible text properties for the shape. It can contain multiple paragraphs, and each paragraph can contain multiple runs.
Its schema shape is:
| Child element | Meaning |
|---|---|
a:bodyPr | Body properties |
a:lstStyle | Text list styles |
a:p | Text paragraphs |
Paragraph markup
<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:r>
<a:t>Paragraph text</a:t>
</a:r>
</a:p>
If the paragraph uses only text properties, copying the <a:p/> subtree can be enough. If it references hyperlinks, images, embedded objects, comments, or custom extension data, the target package also needs matching relationships and parts.
The upstream sample opens both presentations, gets the first slide from each, finds the first text body in each slide, deep-clones the first source paragraph into the target text body, and replaces the source paragraph with a placeholder paragraph. The save operation must persist both packages.
Rust workflow
Move the first plain DrawingML paragraph from the first source slide into the first target slide:
#![allow(unused)] fn main() { pub fn move_first_paragraph_between_presentations( source_path: &Path, target_path: &Path, ) -> Result<(Vec<u8>, Vec<u8>), Box<dyn std::error::Error>> { let mut source = PresentationDocument::new_from_file_with_settings(source_path, lazy_settings())?; let mut target = PresentationDocument::new_from_file_with_settings(target_path, lazy_settings())?; let source_presentation_part = source.presentation_part()?; let target_presentation_part = target.presentation_part()?; let Some(source_slide_part) = source_presentation_part.slide_parts(&source).next() else { return Err("source presentation has no slides".into()); }; let Some(target_slide_part) = target_presentation_part.slide_parts(&target).next() else { return Err("target presentation has no slides".into()); }; let source_xml = source_slide_part.data_as_str(&source)?.unwrap_or_default(); let (updated_source_xml, paragraph_xml) = remove_first_drawing_paragraph(source_xml)?; source_slide_part.set_data(&mut source, updated_source_xml.into_bytes())?; let target_xml = target_slide_part.data_as_str(&target)?.unwrap_or_default(); let updated_target_xml = append_drawing_paragraph(target_xml, ¶graph_xml)?; target_slide_part.set_data(&mut target, updated_target_xml.into_bytes())?; let mut source_buffer = Cursor::new(Vec::new()); source.save(&mut source_buffer)?; let mut target_buffer = Cursor::new(Vec::new()); target.save(&mut target_buffer)?; Ok((source_buffer.into_inner(), target_buffer.into_inner())) } }
This example handles plain text paragraphs. If the paragraph contains hyperlinks, media, embedded objects, comments, or custom extension data, copy the referenced relationships and parts into the target package before saving.
Open a presentation document for read-only access
Use PresentationDocument to open a .pptx package and inspect its parts. In ooxmlsdk, new_from_file and new_from_file_with_settings read from a path and return a package value; changes are only persisted when you call save or copy_to.
For read-only inspection, open the package and avoid saving it.
PresentationDocument::new also accepts a Read + Seek reader, such as a std::io::Cursor<Vec<u8>> or a seekable file stream. Use that form when the package bytes come from storage other than a local path.
Open and inspect slide parts
#![allow(unused)] fn main() { use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::comment_authors_part::CommentAuthorsPart; use ooxmlsdk::parts::presentation_document::PresentationDocument; use ooxmlsdk::parts::presentation_part::PresentationPart; use ooxmlsdk::parts::slide_comments_part::SlideCommentsPart; use ooxmlsdk::parts::slide_part::SlidePart; use ooxmlsdk::sdk::MediaDataPartType; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, PresentationDocumentType}; pub fn open_presentation_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; Ok(presentation_part.slide_parts(&document).count()) } }
The example uses lazy package opening:
OpenSettings { open_mode: PackageOpenMode::Lazy, ..Default::default() }PresentationDocument::new_from_file_with_settingspresentation_part()slide_parts(&document)
Lazy opening is useful for inspection helpers because it lets you navigate the package model without parsing every root element up front.
If a caller asks for a slide index that is out of range, return an error or an empty result deliberately. The upstream sample can throw an out-of-range exception; Rust examples should make that behavior explicit in their Result or Option shape.
Presentation package structure
A PresentationML package stores the main presentation in ppt/presentation.xml. Slides are separate parts, usually under ppt/slides/, and the presentation part owns relationships to those slide parts.
Use relationships and typed part accessors instead of hard-coding ZIP paths whenever possible.
Retrieve the number of slides in a presentation document
This example counts slide parts in a PresentationML package. It can include all slides or skip slides marked hidden with show="0" or show="false" in the slide XML.
Count slides
#![allow(unused)] fn main() { pub fn count_slides( path: &Path, include_hidden: bool, ) -> Result<usize, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; if include_hidden { return Ok(presentation_part.slide_parts(&document).count()); } let mut count = 0; let slide_parts: Vec<_> = presentation_part.slide_parts(&document).collect(); for slide_part in slide_parts { let xml = slide_part.data_as_str(&document)?.unwrap_or_default(); if !xml.contains(r#"show="0""#) && !xml.contains(r#"show="false""#) { count += 1; } } Ok(count) } }
The function opens the package lazily, gets the presentation part, and iterates presentation_part.slide_parts(&document).
When include_hidden is true, the function returns the number of slide parts. When include_hidden is false, it reads each slide part as XML and skips slides whose root has a hidden show value.
Notes
This example uses raw slide XML to check the show attribute. That keeps the example focused on package navigation and avoids loading every slide root. If you are already parsing slide roots for other edits, use the generated slide schema type instead.
In PresentationML, a missing show value means the slide is visible. A value of true also means visible; hidden slides are represented by a false value, commonly serialized as show="0" or show="false".
Structure of a PresentationML document
A PresentationML file is an Open Packaging Convention package. The .pptx file is a ZIP container whose parts are connected by relationship items. ooxmlsdk exposes that graph through PresentationDocument and generated part accessors.
A presentation is not stored as one large XML body. The presentation root, slide masters, slide layouts, slides, themes, notes, comments, media, and other resources live in separate parts. A separate XML part is created for each slide.
The minimum document structure includes the presentation, slide master, slide layout, slide, and theme parts. Real decks can also include notes slides, handout masters, shapes, pictures, tables, animations, audio, video, and slide transitions.
Package parts
| Package part | Root element | ooxmlsdk access |
|---|---|---|
| Presentation | <p:presentation/> | PresentationDocument::presentation_part() |
| Slide | <p:sld/> | PresentationPart::slide_parts(&document) |
| Slide master | <p:sldMaster/> | PresentationPart::slide_master_parts(&document) |
| Slide layout | <p:sldLayout/> | SlidePart::slide_layout_part(&document) |
| Notes slide | <p:notes/> | SlidePart::notes_slide_part(&document) |
| Notes master | <p:notesMaster/> | PresentationPart::notes_master_part(&document) |
| Handout master | <p:handoutMaster/> | PresentationPart::handout_master_part(&document) |
| Theme | <a:theme/> | PresentationPart::theme_part(&document) |
| Comments | <p:cmLst/> | slide comment part accessors when present |
| Comment authors | <p:cmAuthorLst/> | PresentationPart::comment_authors_part(&document) |
The exact set of parts depends on the document. A small deck can contain only the package relationship item, the presentation part, slide parts, and the required content type declarations.
Relationship graph
The package-level relationship points to ppt/presentation.xml. The presentation part then owns relationships to slide parts and other top-level presentation resources.
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship
Id="rId1"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
Target="ppt/presentation.xml"/>
</Relationships>
Inside ppt/presentation.xml, slide ids refer to relationship ids:
<p:sldIdLst>
<p:sldId id="256" r:id="rId1"/>
<p:sldId id="257" r:id="rId2"/>
</p:sldIdLst>
Those ids are resolved in ppt/_rels/presentation.xml.rels:
<Relationship
Id="rId1"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"
Target="slides/slide1.xml"/>
Reading the graph in Rust
Use the package model rather than opening ZIP paths manually:
#![allow(unused)] fn main() { use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::comment_authors_part::CommentAuthorsPart; use ooxmlsdk::parts::presentation_document::PresentationDocument; use ooxmlsdk::parts::presentation_part::PresentationPart; use ooxmlsdk::parts::slide_comments_part::SlideCommentsPart; use ooxmlsdk::parts::slide_part::SlidePart; use ooxmlsdk::sdk::MediaDataPartType; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, PresentationDocumentType}; pub fn open_presentation_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; Ok(presentation_part.slide_parts(&document).count()) } }
That pattern is the base for the other PresentationML examples: open the package, get the presentation part, then walk typed child parts or relationship iterators.
Minimal slide part
A slide part stores only one slide. Text and drawing content are under <p:cSld/>.
<p:sld
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr>
<p:cNvPr id="1" name=""/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr/>
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Text 2"/>
<p:cNvSpPr/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr/>
<p:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p><a:r><a:t>Hello</a:t></a:r></a:p>
</p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
</p:sld>
For schema details, keep the XML element names separate from the Rust package API: XML stores ids and content; ooxmlsdk resolves ids into typed parts and relationships.
Add a transition to a slide in a presentation
Slide transitions are stored on the slide that appears after the transition. The transition markup is a child of the slide root.
Transition markup
<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr>
<p:cNvPr id="1" name=""/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr/>
</p:spTree>
</p:cSld>
<p:transition spd="fast">
<p:fade/>
</p:transition>
</p:sld>
The transition element can specify speed, advance timing, sound, and transition-specific child elements such as fade or push.
Important attributes include:
| Attribute | Meaning |
|---|---|
advClick | Whether a mouse click advances the slide. If omitted, the default is true. |
advTm | Auto-advance time in milliseconds. If omitted, no auto-advance time is set. |
spd | Transition speed, commonly slow, med, or fast. |
For example, a random-bar transition can advance after three seconds:
<p:transition spd="slow" advClick="1" advTm="3000">
<p:randomBar dir="horz"/>
</p:transition>
Markup compatibility
Some transition features are stored with newer Office namespaces. For example, PowerPoint 2010 introduced a transition duration attribute in the p14 namespace. A compatible package can wrap that richer transition in mc:AlternateContent, with a fallback transition that omits the newer attribute.
When documenting a writer for this page, include both the richer choice and a fallback only after the fixture proves that ooxmlsdk preserves or processes the compatibility markup correctly for the selected feature flags.
Rust workflow
#![allow(unused)] fn main() { pub fn add_fade_transition( path: &Path, slide_index: usize, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else { return Err(format!("slide index {slide_index} not found").into()); }; let xml = slide_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = replace_or_insert_transition(xml, r#"<p:transition spd="fast"><p:fade/></p:transition>"#)?; slide_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The listing inserts or replaces only the <p:transition/> element and preserves the rest of the slide XML. For richer Office-version-specific transition markup, include a compatibility fallback and test the selected mce behavior.
Working with animation
PresentationML stores animation data in slide timing markup. The core element is <p:timing/>, which contains timing nodes and behavior elements such as <p:anim/>.
The animation model is loosely based on SMIL. Slide animations are time-based effects applied to slide objects or text. Slide transitions are related, but they are stored in <p:transition/> and occur before the slide's own animation timeline.
Animation structure
An animation behavior usually points at a target element on the slide and defines how a value changes over time.
<p:timing>
<p:tnLst>
<p:par>
<p:cTn id="1" dur="indefinite" restart="never"/>
</p:par>
</p:tnLst>
</p:timing>
Important animation-related elements include:
| PresentationML element | Purpose |
|---|---|
<p:timing/> | Container for slide timing and animation data |
<p:tnLst/> | Time node list |
<p:anim/> | Value animation behavior |
<p:cBhvr/> | Common behavior settings |
<p:tgtEl/> | Target element for the behavior |
<p:tavLst/> | Time-animated value list |
Important <p:anim/> attributes include:
| Attribute | Meaning |
|---|---|
by | Relative offset from the starting value |
from | Starting value |
to | Ending value |
calcmode | Interpolation mode |
valueType | Type of the animated property value |
In ooxmlsdk, the corresponding generated types live under ooxmlsdk::schemas::p, including Animate, CommonBehavior, TimeAnimateValueList, Timing, and TargetElement.
Rust workflow
Open the target slide part and add or replace a minimal timing tree:
#![allow(unused)] fn main() { pub fn add_basic_animation_timing( path: &Path, slide_index: usize, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else { return Err("slide index out of range".into()); }; let slide_xml = slide_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = replace_or_insert_timing(slide_xml, basic_timing_xml())?; slide_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Animation markup is sensitive to ids and target references. The example targets shape id 2 from the simple fixture; in a general writer, discover the target shape id from the slide XML and keep timing node ids unique.
Working with comments
Presentation comments are stored outside the slide XML. A slide can have a comments part, and the presentation can have a comment authors part that identifies authors used by comments.
Comment structure
Classic PresentationML comments use a comment list rooted at <p:cmLst/>. Each comment has an author id, creation time, position, and text.
<p:cmLst xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:cm authorId="0" dt="2026-05-04T10:00:00Z" idx="1">
<p:pos x="10" y="20"/>
<p:text>Review this slide.</p:text>
</p:cm>
</p:cmLst>
The author list is a separate presentation-level part:
<p:cmAuthorLst xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:cmAuthor id="0" name="Ada" initials="AL" lastIdx="1" clrIdx="0"/>
</p:cmAuthorLst>
Important p:cm children and attributes are:
| Item | Meaning |
|---|---|
p:pos | Position of the comment on the slide surface |
p:text | Unformatted comment text |
p:extLst | Extension data for future features |
authorId | ID of an entry in the comment author list |
dt | Last modified date and time |
idx | Comment index unique for that author |
The position point is the upper-left point in left-to-right UI layouts and the upper-right point in right-to-left UI layouts. The display size is application-defined; comments do not appear during slide show playback.
Rust workflow
Use package relationships to find the relevant parts. Start with the presentation part for author data, and with each slide part for slide-specific comments:
#![allow(unused)] fn main() { pub fn add_comment_to_slide( path: &Path, slide_index: usize, author_name: &str, initials: &str, text: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let authors_part = if let Some(part) = presentation_part.comment_authors_part(&document) { part } else { presentation_part.add_new_part_auto_id::<_, CommentAuthorsPart>(&mut document)? }; let authors_xml = authors_part .data_as_str(&document)? .map(str::to_string) .filter(|xml| !xml.trim().is_empty()) .unwrap_or_else(empty_comment_authors_xml); let (updated_authors_xml, author_id, comment_index) = upsert_comment_author(&authors_xml, author_name, initials)?; authors_part.set_data(&mut document, updated_authors_xml.into_bytes())?; let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else { return Err("slide index out of range".into()); }; let comments_part = if let Some(part) = slide_part.slide_comments_part(&document) { part } else { slide_part.add_new_part_auto_id::<_, SlideCommentsPart>(&mut document)? }; let comments_xml = comments_part .data_as_str(&document)? .map(str::to_string) .filter(|xml| !xml.trim().is_empty()) .unwrap_or_else(empty_slide_comments_xml); let updated_comments_xml = append_slide_comment(&comments_xml, author_id, comment_index, text)?; comments_part.set_data(&mut document, updated_comments_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Delete comments by matching author ids from the author list:
#![allow(unused)] fn main() { pub fn delete_comments_by_author( path: &Path, author_name: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let Some(authors_part) = presentation_part.comment_authors_part(&document) else { let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; return Ok(buffer.into_inner()); }; let authors_xml = authors_part.data_as_str(&document)?.unwrap_or_default(); let author_ids = comment_author_ids_by_name(authors_xml, author_name); let updated_authors_xml = remove_comment_authors_by_id(authors_xml, &author_ids); authors_part.set_data(&mut document, updated_authors_xml.into_bytes())?; let slide_parts: Vec<_> = presentation_part.slide_parts(&document).collect(); for slide_part in slide_parts { let Some(comments_part) = slide_part.slide_comments_part(&document) else { continue; }; let comments_xml = comments_part.data_as_str(&document)?.unwrap_or_default(); let updated_comments_xml = remove_slide_comments_by_author_id(comments_xml, &author_ids); comments_part.set_data(&mut document, updated_comments_xml.into_bytes())?; } let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Practical guidance
For read-only tooling, inspect existing comment parts and author parts. For editing modern threaded comments, prefer starting from a real presentation that already contains those comments, then make a small schema-aware change and verify that PowerPoint can open the result.
Working with handout master slides
A handout master describes how printed handouts should look. It is separate from normal slide content and is related from the presentation part.
Handout master structure
The root element is <p:handoutMaster/>. It can contain common slide data, a color map override, header and footer settings, extension data, and relationships to resources such as themes or images.
<p:handoutMaster
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:cSld name="Handout Master"/>
<p:clrMapOvr>
<a:masterClrMapping xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>
</p:clrMapOvr>
</p:handoutMaster>
Common elements:
| PresentationML element | Purpose |
|---|---|
<p:cSld/> | Common handout master shapes and data |
<p:clrMapOvr/> | Color map override |
<p:hf/> | Header and footer placeholders |
<p:extLst/> | Extension data |
p:clrMap maps theme color names such as bg1, tx1, accent1, hlink, and folHlink to actual theme slots. p:cSld holds the common slide data for the handout master, including the shape tree and text bodies used by handout placeholders.
If a handout master is created from scratch, the package also needs a handout master part relationship and any required theme or image relationships. The upstream sample creates common slide data, a shape tree, a title placeholder shape, and a color map.
Rust workflow
Open the package and get the presentation part. If the deck has a handout master, use presentation_part.handout_master_part(&document) and read the part data through the package.
#![allow(unused)] fn main() { use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::comment_authors_part::CommentAuthorsPart; use ooxmlsdk::parts::presentation_document::PresentationDocument; use ooxmlsdk::parts::presentation_part::PresentationPart; use ooxmlsdk::parts::slide_comments_part::SlideCommentsPart; use ooxmlsdk::parts::slide_part::SlidePart; use ooxmlsdk::sdk::MediaDataPartType; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, PresentationDocumentType}; pub fn open_presentation_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; Ok(presentation_part.slide_parts(&document).count()) } }
Not every presentation contains a handout master. Treat the accessor as optional and avoid creating one from scratch until the document also has matching relationships and content type entries.
Working with notes slides
A notes slide stores speaker notes and related presentation content for a single slide. It is a separate part related from the slide part.
Notes slide structure
The root element is <p:notes/>. It can contain common slide data, a color map override, and extension data. The actual note text is usually stored in text bodies under shapes.
<p:notes
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr>
<p:cNvPr id="1" name=""/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr/>
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Notes placeholder 2"/>
<p:cNvSpPr/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr/>
<p:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p><a:r><a:t>Speaker note</a:t></a:r></a:p>
</p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
</p:notes>
Common elements:
| PresentationML element | Purpose |
|---|---|
<p:cSld/> | Common notes slide data |
<p:clrMapOvr/> | Color map override |
<p:extLst/> | Extension data |
Important notes slide attributes include:
| Attribute | Meaning |
|---|---|
showMasterPhAnim | Whether to display animations on master placeholders |
showMasterSp | Whether to show shapes from the master |
p:clrMapOvr can use a:masterClrMapping to inherit the master color scheme, or a:overrideClrMapping to define a notes-specific mapping.
Rust workflow
Start from the slide part and follow its notes slide relationship if present. The general traversal pattern is the same as reading slide text:
#![allow(unused)] fn main() { pub fn get_slide_text( path: &Path, slide_index: usize, ) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else { return Ok(Vec::new()); }; let xml = slide_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_drawing_text(xml)) } }
ooxmlsdk provides the package and typed part graph. A writer for notes slides needs to update the slide relationship, notes part XML, optional notes master references, and content type declarations together; this chapter keeps that as future tested work.
When adding notes to a deck that lacks notes infrastructure, also account for the notes master part and its theme relationship. The upstream sample creates missing notes slide, notes master, and theme parts together.
Working with presentations
The main part of a PresentationML package is ppt/presentation.xml. In ooxmlsdk, it is exposed through PresentationDocument::presentation_part(), and its child relationships connect the presentation to slide, slide master, notes master, handout master, theme, comment author, and other parts.
Use typed part accessors for package navigation whenever possible. They preserve the relationship model and avoid depending on ZIP paths.
PresentationML root
The <p:presentation/> element stores presentation-wide properties and lists the related slides and masters. A small presentation commonly looks like this:
<p:presentation
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:sldMasterIdLst>
<p:sldMasterId id="2147483648" r:id="rId1"/>
</p:sldMasterIdLst>
<p:sldIdLst>
<p:sldId id="256" r:id="rId2"/>
<p:sldId id="257" r:id="rId3"/>
</p:sldIdLst>
<p:sldSz cx="9144000" cy="6858000"/>
<p:notesSz cx="6858000" cy="9144000"/>
</p:presentation>
The r:id values are relationship ids. ooxmlsdk resolves those relationships through the package model.
Common children of p:presentation include:
| Element | Meaning |
|---|---|
p:sldMasterIdLst | Slide masters available in the presentation |
p:sldIdLst | Slides available in presentation order |
p:notesMasterIdLst | Notes masters |
p:handoutMasterIdLst | Handout masters |
p:custShowLst | Custom slide shows with their own slide ordering |
p:sldSz | Slide surface size |
p:notesSz | Notes and handout surface size |
p:defaultTextStyle | Default text styles |
Slide size describes the presentation slide surface; notes size describes the surface used for notes slides and handouts. Both are separate from the physical package part size.
Common Rust workflow
Open the package, get the presentation part, then traverse child parts:
#![allow(unused)] fn main() { use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::comment_authors_part::CommentAuthorsPart; use ooxmlsdk::parts::presentation_document::PresentationDocument; use ooxmlsdk::parts::presentation_part::PresentationPart; use ooxmlsdk::parts::slide_comments_part::SlideCommentsPart; use ooxmlsdk::parts::slide_part::SlidePart; use ooxmlsdk::sdk::MediaDataPartType; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, PresentationDocumentType}; pub fn open_presentation_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; Ok(presentation_part.slide_parts(&document).count()) } }
For read-only analysis, PackageOpenMode::Lazy keeps startup cheap and loads part data as needed. For editing workflows, open the package, mutate supported parts or raw part data deliberately, then call save or copy_to.
Common presentation parts
| Package concept | ooxmlsdk accessor |
|---|---|
| Main presentation | presentation_part() |
| Slides | PresentationPart::slide_parts(&document) |
| Slide masters | PresentationPart::slide_master_parts(&document) |
| Notes master | PresentationPart::notes_master_part(&document) |
| Handout master | PresentationPart::handout_master_part(&document) |
| Theme | PresentationPart::theme_part(&document) |
| Comment authors | PresentationPart::comment_authors_part(&document) |
The Rust API mirrors Open Packaging Convention relationships. Treat the XML schema element names and the package part graph as separate layers: the XML uses r:id, while the SDK resolves that id to a typed part or relationship.
Working with presentation slides
A slide is stored as a separate PresentationML part, usually under ppt/slides/. In ooxmlsdk, slide parts are reached from the presentation part with slide_parts(&document).
The slide list in p:presentation defines normal presentation order. Custom shows can define a separate ordering by listing slide relationship IDs in a p:custShow slide list.
Slide structure
The root element of a slide part is <p:sld/>. It contains common slide data, optional transition and timing data, color map overrides, and extension data.
For a shape, p:nvSpPr and p:spPr are required by the generated schema. The text body p:txBody is optional, but when it is present it must contain a:bodyPr; a:lstStyle is optional.
<p:sld
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr>
<p:cNvPr id="1" name=""/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr/>
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Title 1"/>
<p:cNvSpPr/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr/>
<p:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p><a:r><a:t>Slide title</a:t></a:r></a:p>
</p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
</p:sld>
Common child elements include:
| PresentationML element | Purpose |
|---|---|
<p:cSld/> | Common slide data, including shapes and text |
<p:clrMapOvr/> | Color map override for this slide |
<p:transition/> | Transition from the previous slide |
<p:timing/> | Animation and timing data |
<p:extLst/> | Extension data |
p:cSld stores the slide-specific common data, including the shape tree. p:clrMapOvr can inherit the master color mapping with a:masterClrMapping or define an override mapping.
Reading slide content
For inspection tasks, read each SlidePart through the package and parse the text or relationships you need:
#![allow(unused)] fn main() { pub fn get_slide_text( path: &Path, slide_index: usize, ) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let Some(slide_part) = presentation_part.slide_parts(&document).nth(slide_index) else { return Ok(Vec::new()); }; let xml = slide_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_drawing_text(xml)) } }
The companion example that reads every slide uses the same traversal:
#![allow(unused)] fn main() { pub fn get_all_slide_text(path: &Path) -> Result<Vec<Vec<String>>, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let mut slides = Vec::new(); let slide_parts: Vec<_> = presentation_part.slide_parts(&document).collect(); for slide_part in slide_parts { let xml = slide_part.data_as_str(&document)?.unwrap_or_default(); slides.push(extract_drawing_text(xml)); } Ok(slides) } }
Relationship model
Slide parts can own relationships to layouts, images, charts, embedded packages, media, comments, and hyperlinks. Prefer typed accessors such as slide_layout_part(&document) when the target is a known part type. For reference relationships like external hyperlinks, use relationship iterators such as hyperlink_relationships(&document).
When a feature is not yet represented by a high-level helper in ooxmlsdk, keep the package model intact: read the part data, make a focused XML change, and save the package only after the resulting part graph is still valid.
A valid slide is normally associated with a slide layout, and that layout is associated with a slide master. When creating or copying slides, preserve those relationships rather than copying only ppt/slides/slideN.xml.
Working with slide layouts
A slide layout is a template part that describes the placeholder and formatting structure a slide can inherit from a slide master. In PresentationML it is rooted at <p:sldLayout/>; in ooxmlsdk it is represented as a slide layout part reached through relationships.
Slide layout structure
The layout root can contain common slide data, header/footer settings, timing, transition, color map override, and extension data.
<p:sldLayout
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
type="title">
<p:cSld name="Title Slide">
<p:spTree>
<p:nvGrpSpPr>
<p:cNvPr id="1" name=""/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr/>
<!-- placeholder and shape definitions -->
</p:spTree>
</p:cSld>
</p:sldLayout>
Important attributes include:
| Attribute | Meaning |
|---|---|
type | Layout kind, such as title, blank, or title and content |
matchingName | Name used when matching layouts during template changes |
preserve | Whether the layout should be kept when no slide uses it |
showMasterSp | Whether master slide shapes are shown |
showMasterPhAnim | Whether master placeholder animations are shown |
userDrawn | Whether user-drawn data should be preserved |
Navigating layouts in Rust
A layout is normally reached from a slide part. This example opens a presentation, walks the slides, follows each slide layout relationship, and returns the layout XML.
#![allow(unused)] fn main() { pub fn get_slide_layout_xml(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; let mut layouts = Vec::new(); for slide_part in presentation_part.slide_parts(&document) { if let Some(layout_part) = slide_part.slide_layout_part(&document) { layouts.push( layout_part .data_as_str(&document)? .unwrap_or_default() .to_string(), ); } } Ok(layouts) } }
Layout parts can also relate back to a slide master and to dependent resources such as images, charts, diagrams, embedded packages, and theme overrides. Use the generated part accessors where available, because they resolve relationship ids without hard-coding package paths.
Editing notes
Creating a valid layout from scratch requires a coordinated slide master, layout part, relationship entries, content type overrides, and placeholder XML. In Rust, prefer copying an existing layout and making small schema-aware edits unless your code owns all of those package updates and validates the saved presentation.
Working with slide masters
A slide master stores shared formatting and placeholder structure for a group of slide layouts. In PresentationML, a slide master is rooted at <p:sldMaster/> and is related from the main presentation part.
Slide master structure
Common child elements include:
| PresentationML element | Purpose |
|---|---|
<p:cSld/> | Common slide data, including master shapes |
<p:clrMap/> | Theme color mapping |
<p:sldLayoutIdLst/> | Relationships to slide layouts |
<p:txStyles/> | Default title, body, and other text styles |
<p:hf/> | Header and footer defaults |
<p:timing/> | Master timing data |
<p:transition/> | Master transition data |
<p:extLst/> | Extension data |
<p:sldMaster
xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld name="Office Theme"/>
<p:sldLayoutIdLst>
<p:sldLayoutId id="2147483649" r:id="rId1"/>
</p:sldLayoutIdLst>
<p:txStyles/>
</p:sldMaster>
Navigating masters in Rust
Start at the presentation part and use the generated accessor:
#![allow(unused)] fn main() { use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::comment_authors_part::CommentAuthorsPart; use ooxmlsdk::parts::presentation_document::PresentationDocument; use ooxmlsdk::parts::presentation_part::PresentationPart; use ooxmlsdk::parts::slide_comments_part::SlideCommentsPart; use ooxmlsdk::parts::slide_part::SlidePart; use ooxmlsdk::sdk::MediaDataPartType; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, PresentationDocumentType}; pub fn open_presentation_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = PresentationDocument::new_from_file_with_settings(path, lazy_settings())?; let presentation_part = document.presentation_part()?; Ok(presentation_part.slide_parts(&document).count()) } }
For master-specific traversal, use presentation_part.slide_master_parts(&document). From a slide master, follow layout relationships with the generated slide layout part accessors where available.
Editing notes
Slide masters, slide layouts, and slides form a coordinated graph. A valid edit may require updating:
- the presentation part's master list,
- the slide master's layout id list,
- relationship items,
- content type overrides,
- the affected slide layout and slide XML.
Until a tested construction example exists in this book, prefer inspecting existing master/layout parts or copying a known-good package structure before making focused XML changes.
Spreadsheets
This section covers SpreadsheetML packages (.xlsx, .xlsm, .xltx) with ooxmlsdk.
Spreadsheet packages are made of a workbook part, worksheet parts, optional shared strings, styles, tables, charts, pivot caches, drawings, and relationships between those parts. In ooxmlsdk, the entry point is usually ooxmlsdk::parts::spreadsheet_document::SpreadsheetDocument.
Use the parts feature, enabled by default, to open and save packages. Examples in this section are backed by tested Rust code in listings/spreadsheet.
In this section
- Structure of a SpreadsheetML document
- Open a spreadsheet document for read-only access
- Retrieve a list of the worksheets in a spreadsheet document
- Get worksheet information from a package
- Retrieve the values of cells in a spreadsheet
- Working with sheets
- Working with the shared string table
- Working with formulas
- Working with the calculation chain
- Working with conditional formatting
- Working with PivotTables
- Working with SpreadsheetML tables
Writer-focused chapters are being ported only when the code has a fixture in listings/ and passes cargo test --workspace.
Related sections
Structure of a SpreadsheetML document
A SpreadsheetML file is an Open Packaging Convention package. The .xlsx file is a ZIP container whose parts are connected by relationship items. ooxmlsdk exposes that graph through SpreadsheetDocument and generated part accessors.
Package parts
| Package part | Root element | ooxmlsdk access |
|---|---|---|
| Workbook | <workbook/> | SpreadsheetDocument::workbook_part() |
| Worksheet | <worksheet/> | WorkbookPart::worksheet_parts(&document) |
| Chartsheet | <chartsheet/> | chartsheet parts when present |
| Shared strings | <sst/> | WorkbookPart::shared_string_table_part(&document) |
| Styles | <styleSheet/> | WorkbookPart::workbook_styles_part(&document) |
| Calculation chain | <calcChain/> | WorkbookPart::calculation_chain_part(&document) |
| Table | <table/> | WorksheetPart::table_definition_parts(&document) |
| Drawing | <wsDr/> | WorksheetPart::drawings_part(&document) |
| Pivot table | <pivotTableDefinition/> | WorksheetPart::pivot_table_parts(&document) |
| Pivot cache | <pivotCacheDefinition/> | workbook-level pivot cache parts when present |
| Pivot cache records | <pivotCacheRecords/> | pivot cache record parts when present |
| Conditional formatting | <conditionalFormatting/> | worksheet XML under the generated worksheet schema |
The exact set of parts depends on the workbook. A small workbook can contain only package relationships, xl/workbook.xml, one worksheet, and content type declarations.
The minimum workbook scenario has three spreadsheet-specific requirements: a single sheet entry, a workbook-local sheet ID, and a relationship ID that points from the workbook part to the worksheet part.
Workbook and worksheet references
The workbook contains a <sheets/> collection. Each sheet entry has a workbook-local sheetId and a relationship id:
<workbook
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets>
<sheet name="Summary" sheetId="1" r:id="rId1"/>
<sheet name="Hidden Data" sheetId="2" state="hidden" r:id="rId2"/>
</sheets>
</workbook>
The workbook relationship item resolves those ids to worksheet parts:
<Relationship
Id="rId1"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
Target="worksheets/sheet1.xml"/>
Reading the graph in Rust
Open the package, get the workbook part, and traverse typed child parts:
#![allow(unused)] fn main() { use std::collections::BTreeMap; use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::chart_part::ChartPart; use ooxmlsdk::parts::drawings_part::DrawingsPart; use ooxmlsdk::parts::ribbon_extensibility_part::RibbonExtensibilityPart; use ooxmlsdk::parts::spreadsheet_document::SpreadsheetDocument; use ooxmlsdk::parts::table_definition_part::TableDefinitionPart; use ooxmlsdk::parts::workbook_part::WorkbookPart; use ooxmlsdk::parts::worksheet_part::WorksheetPart; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, SdkPart, SpreadsheetDocumentType}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct FormulaCell { pub reference: String, pub formula: String, pub cached_value: Option<String>, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct PivotTablePartCounts { pub worksheet_pivot_tables: usize, pub workbook_pivot_caches: usize, } pub fn open_spreadsheet_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; Ok(workbook_part.worksheet_parts(&document).count()) } }
That pattern is the base for the other SpreadsheetML examples. Prefer generated accessors over hard-coded ZIP paths when navigating relationships.
Minimal worksheet
A worksheet stores rows and cells under <sheetData/>.
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1">
<c r="A1"><v>100</v></c>
</row>
</sheetData>
</worksheet>
Cells can store raw values, formulas, inline strings, or shared string indexes. The shared string table is a separate workbook-level part.
A typical workbook can add many more parts, including chartsheets, drawings, tables, pivot tables, pivot caches, styles, and shared strings. Keep the workbook XML, relationships, and content type declarations in sync whenever adding or removing parts.
Add custom UI to a spreadsheet document
Custom UI is stored in package parts related from the spreadsheet document package. It is not part of worksheet cell data.
The upstream sample customizes the Excel ribbon. The custom UI XML describes a button on the Add-ins tab and points that button at a macro in the host workbook. For that scenario, the workbook is normally macro-enabled (.xlsm) and already contains the macro that the ribbon callback names.
Package model
Custom UI parts commonly use Office relationship types for ribbon extensibility or user customization. A valid update needs:
- the custom UI XML part,
- the package relationship to that part,
- content type metadata,
- any images or resources referenced by the custom UI.
The ribbon extensibility part is a package-level part. If it does not exist, a writer must create it; if it already exists, a writer should update only the intended custom UI payload and preserve unrelated package relationships.
Rust workflow
Use SpreadsheetDocument to open and save the package. The package-side custom UI operation is now covered by a tested listing: add a ribbon extensibility part at the package level, write the custom UI XML, and save.
#![allow(unused)] fn main() { pub fn add_custom_ui_part( path: &Path, relationship_id: &str, custom_ui_xml: &[u8], ) -> Result<(Vec<u8>, String), Box<dyn std::error::Error>> { let mut document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let custom_ui_part = document.add_new_part::<RibbonExtensibilityPart>(relationship_id)?; custom_ui_part.set_data(&mut document, custom_ui_xml.to_vec())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok((buffer.into_inner(), relationship_id.to_string())) } }
This covers the package relationship and part payload. A complete macro-enabled ribbon scenario still needs the workbook to contain the callback macro named by the custom UI XML, and any referenced custom UI images must be added as related parts.
Calculate the sum of a range of cells in a spreadsheet
ooxmlsdk reads SpreadsheetML packages; it does not act as a spreadsheet calculation engine. To sum a range yourself, read the worksheet cells, select the references in the range, parse numeric values, and add them in Rust.
The upstream sample accepts a workbook path, worksheet name, first cell, last cell, and result cell. It parses row numbers from cell references, parses column names from cell references, compares columns by length and lexical order, sums cells in the rectangular range, inserts the result through the shared string table, and writes the result cell.
Sum a range
#![allow(unused)] fn main() { pub fn sum_cell_range( path: &Path, first_cell: &str, last_cell: &str, ) -> Result<f64, Box<dyn std::error::Error>> { let (first_row, first_column) = cell_position(first_cell)?; let (last_row, last_column) = cell_position(last_cell)?; let min_row = first_row.min(last_row); let max_row = first_row.max(last_row); let min_column = first_column.min(last_column); let max_column = first_column.max(last_column); let mut sum = 0.0; for (reference, value) in get_cell_values(path)? { let (row, column) = cell_position(&reference)?; if (min_row..=max_row).contains(&row) && (min_column..=max_column).contains(&column) { sum += value.parse::<f64>()?; } } Ok(sum) } }
The helper reads display values from the first worksheet, filters cells inside the rectangular range, parses the selected values as f64, and sums them in Rust. Treat values as strings at the package boundary and parse them explicitly so conversion failures are normal Result errors.
For a writer version, keep the read and write halves separate: first compute the numeric result, then insert or update the result cell while preserving row and cell ordering.
Formula alternative
SpreadsheetML can store a formula and cached value:
<c r="B3">
<f>SUM(B1:B2)</f>
<v>42</v>
</c>
Editing formulas safely also involves cached values and calculation metadata. If you write a SUM(...) formula instead of calculating in Rust, decide whether to omit the cached <v/> value and let the spreadsheet application recalculate, or write a cache that matches the formula.
Copy a worksheet using streaming XML
Copying a worksheet with a streaming parser is useful for large sheets because the cell XML can be processed without loading a full worksheet object model.
The upstream page compares two approaches. DOM-style copying is simpler because a worksheet tree can be cloned as a whole, but it loads the full part into memory. Streaming XML copying reads and writes elements forward-only, which is better for very large worksheet parts.
Package model
A copied worksheet needs more than copied XML. The workbook must get a new <sheet/> entry and relationship, and any worksheet-owned relationships may need to be copied or adjusted.
<sheet name="Copied Sheet" sheetId="3" r:id="rId3"/>
Even if the worksheet XML is streamed, the workbook sheets collection is usually small. Updating that workbook list with a structured approach is safer than trying to stream-prepend a new sheet entry and accidentally change sheet order.
Rust workflow
Copy the source worksheet XML into a new worksheet part and append a workbook sheet entry:
#![allow(unused)] fn main() { pub fn copy_worksheet( path: &Path, source_sheet_name: &str, new_sheet_name: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let source_part = worksheet_part_by_name(&document, &workbook_part, source_sheet_name)?; let copied_xml = source_part .data_as_str(&document)? .unwrap_or_default() .to_string(); let new_part = workbook_part.add_new_part_auto_id::<_, WorksheetPart>(&mut document)?; new_part.set_data(&mut document, copied_xml.into_bytes())?; let relationship_id = workbook_part .get_id_of_part(&document, &new_part) .expect("worksheet relationship id") .to_string(); let workbook_xml = workbook_part.data_as_str(&document)?.unwrap_or_default(); let updated_workbook_xml = append_sheet(workbook_xml, new_sheet_name, &relationship_id)?; workbook_part.set_data(&mut document, updated_workbook_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
If the source worksheet owns drawings, tables, comments, printer settings, or other related parts, copy those relationships and rewrite references in the copied worksheet as needed. The example above covers the worksheet XML, workbook relationship, and workbook sheet list.
Create a spreadsheet document by providing a file name
Creating an .xlsx from scratch requires package relationships, content types, a workbook part, a workbook relationship item, and at least one worksheet part.
In ooxmlsdk, create the package with SpreadsheetDocument::create(SpreadsheetDocumentType::Workbook), add the workbook part, add worksheet parts from the workbook part, write the root XML, and save the package to the file or writer your application owns.
Choose the package type and file extension together. A normal workbook uses .xlsx; macro-enabled workbooks, templates, and add-ins use different extensions and content types. Excel can reject a file when the package type and extension do not match.
Minimal package pieces
A minimal workbook includes:
[Content_Types].xml,_rels/.relspointing toxl/workbook.xml,xl/workbook.xml,xl/_rels/workbook.xml.rels,- at least one
xl/worksheets/sheetN.xml.
This minimal writer creates that structure in memory:
#![allow(unused)] fn main() { pub fn create_spreadsheet_document() -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = SpreadsheetDocument::create(SpreadsheetDocumentType::Workbook); let workbook_part = document.add_new_part_auto_id::<WorkbookPart>()?; let worksheet_part = workbook_part.add_new_part_auto_id::<_, WorksheetPart>(&mut document)?; let worksheet_relationship_id = workbook_part .get_id_of_part(&document, &worksheet_part) .expect("worksheet relationship id") .to_string(); workbook_part.set_data( &mut document, format!( r#"<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Sheet1" sheetId="1" r:id="{worksheet_relationship_id}"/></sheets></workbook>"# ) .into_bytes(), )?; worksheet_part.set_data( &mut document, br#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>"#.to_vec(), )?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
At the SpreadsheetML layer, the workbook root owns the sheets collection. Each sheet entry stores the display name, a workbook-local sheetId, and an r:id relationship to the worksheet part. The worksheet part itself owns the worksheet root and sheetData.
For template workflows, SpreadsheetDocument::create_from_template opens an .xltx or .xltm as an editable workbook package. Use document_type() to inspect the current type and change_document_type(...) when converting the package content type deliberately.
Delete text from a cell in a spreadsheet
Deleting text from a cell is a worksheet XML edit. If the cell uses the shared string table, the cell contains a shared string index rather than the literal text.
Cell markup
<c r="A1" t="s"><v>0</v></c>
Removing the text can mean removing the cell, removing its <v/> value, or changing the cell type depending on the desired workbook behavior.
The upstream sample also removes the shared string table entry when no other cell uses it. That is a package-wide edit: after removing one si entry, every later shared string index referenced by every worksheet cell must be decremented. If you do not rewrite those indexes correctly, unrelated cells can display the wrong text.
Rust workflow
#![allow(unused)] fn main() { pub fn delete_text_from_cell( path: &Path, sheet_name: &str, cell_reference: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let worksheet_part = worksheet_part_by_name(&document, &workbook_part, sheet_name)?; let worksheet_xml = worksheet_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = clear_cell_text(worksheet_xml, cell_reference); worksheet_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
This listing clears the target cell content while leaving the worksheet row and shared string table intact. That keeps unrelated shared string indexes stable.
If your application needs to compact the shared string table, treat that as a separate workbook-wide rewrite: remove only unused si entries and update every affected t="s" cell index in every worksheet.
Get a column heading in a spreadsheet
Column headings are ordinary cells. In many workbooks, the first row contains headings such as Region or Sales; tables can also store column names in table definition parts.
Read heading cells
The cell value helper reads the first worksheet and resolves shared string indexes:
#![allow(unused)] fn main() { pub fn get_cell_values(path: &Path) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let shared_strings = workbook_part .shared_string_table_part(&document) .and_then(|part| { part .data_as_str(&document) .ok() .flatten() .map(extract_shared_strings) }) .unwrap_or_default(); let Some(first_sheet) = workbook_part.worksheet_parts(&document).next() else { return Ok(Vec::new()); }; let worksheet_xml = first_sheet.data_as_str(&document)?.unwrap_or_default(); Ok(extract_cell_values(worksheet_xml, &shared_strings)) } }
For a simple worksheet where row 1 contains headings, filter the returned (cell_reference, value) pairs to references ending in row 1, such as A1 or B1.
The upstream algorithm takes a target cell reference, splits it into column letters and row digits, then finds the first cell in that column by ordering cells by row index. In Rust, keep that parsing explicit: BC17 has column BC and row 17.
When a heading cell uses the shared string table (t="s"), resolve the numeric cell value through the workbook's shared string table before returning the heading text.
Table headings
SpreadsheetML tables store table column names in the table definition part:
<tableColumns count="2">
<tableColumn id="1" name="Region"/>
<tableColumn id="2" name="Sales"/>
</tableColumns>
Use worksheet cell values for visible grid headings; use table definition parts when you specifically need structured table column names.
Get worksheet information from a package
Worksheet data lives in worksheet parts related from the workbook part. Use the workbook part to traverse worksheets, then read each worksheet's XML through the package.
Read worksheet XML
#![allow(unused)] fn main() { pub fn get_worksheet_xml(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let mut worksheets = Vec::new(); for worksheet_part in workbook_part.worksheet_parts(&document) { worksheets.push( worksheet_part .data_as_str(&document)? .unwrap_or_default() .to_string(), ); } Ok(worksheets) } }
This returns one XML string per worksheet part. From there you can inspect <sheetData/>, dimensions, merged cells, column definitions, page setup, tables, drawings, or other worksheet-level markup.
If you need user-facing worksheet metadata, inspect the workbook sheets collection instead of only walking worksheet parts. Each sheet element carries the display name, workbook-local sheetId, relationship id, and optional visibility state. The relationship id points from the workbook part to the actual worksheet part.
#![allow(unused)] fn main() { pub fn list_worksheets(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let workbook_xml = workbook_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_sheet_names(workbook_xml)) } }
Worksheet markup
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1">
<c r="A1" t="s"><v>0</v></c>
</row>
</sheetData>
</worksheet>
The order returned by worksheet_parts(&document) follows workbook relationships. If you need sheet display names or hidden state, read the workbook <sheets/> list as well.
Insert a chart into a spreadsheet
Charts in SpreadsheetML involve drawing parts, chart parts, worksheet relationships, anchors, and often cached series data. The visible worksheet data and the chart definition are stored separately.
Package model
A worksheet that contains a chart typically owns a drawing relationship. The drawing part owns a chart relationship. The chart part stores chart type, series, axes, and references back to worksheet ranges.
The worksheet data that feeds the chart is still normal SpreadsheetML. A row (row) represents one worksheet row and contains zero or more cell (c) elements, plus optional extension data. Common row attributes include row index r, cell span spans, style index s, custom height, hidden state, outline level, collapsed state, and thick border flags.
A cell stores its grid address, style, type, value, metadata, and optional formula. Its children can include a formula (f), scalar value (v), inline string (is), and extension list. When a cell contains a shared string, <v> is an index into the shared string table. For numeric and most other scalar cells, <v> contains the value directly. Formula cells use <f> for the expression and <v> for the last calculated result. Inline strings are stored under <is> when a workbook does not use the shared string table for that value.
<row r="2" spans="2:12">
<c r="C2" s="1">
<f>PMT(B3/12,B4,-B5)</f>
<v>672.68336574300008</v>
</c>
<c r="D2"><v>180</v></c>
<c r="E2"><v>360</v></c>
</row>
Rust workflow
Create a worksheet drawing part, add a chart part under it, and reference that drawing from the worksheet:
#![allow(unused)] fn main() { pub fn insert_bar_chart( path: &Path, sheet_name: &str, title: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let worksheet_part = worksheet_part_by_name(&document, &workbook_part, sheet_name)?; let drawings_part = if let Some(part) = worksheet_part.drawings_part(&document) { part } else { worksheet_part.add_new_part_auto_id::<_, DrawingsPart>(&mut document)? }; let drawing_relationship_id = worksheet_part .get_id_of_part(&document, &drawings_part) .expect("drawing relationship id") .to_string(); let chart_part = drawings_part.add_new_part_auto_id::<_, ChartPart>(&mut document)?; let chart_relationship_id = drawings_part .get_id_of_part(&document, &chart_part) .expect("chart relationship id") .to_string(); chart_part.set_data(&mut document, chart_xml(title).into_bytes())?; drawings_part.set_data( &mut document, drawing_xml(&chart_relationship_id).into_bytes(), )?; let worksheet_xml = worksheet_part.data_as_str(&document)?.unwrap_or_default(); let worksheet_xml = ensure_relationship_namespace(worksheet_xml); let updated_xml = append_drawing_reference(&worksheet_xml, &drawing_relationship_id)?; worksheet_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The upstream writer sample follows this package flow: verify that the target worksheet exists, add a drawing part to the worksheet, add a chart part under that drawing, create a chart space with editing language metadata, then build a clustered column chart from keyed values. The chart definition needs category and value axes, scaling, axis positions, crossing axis references, tick label position, label alignment, label offset, and legend settings.
After the chart XML is written, the worksheet drawing positions the chart with a TwoCellAnchor. The anchor records the starting and ending row/column markers so Excel knows how the chart moves or resizes when worksheet rows and columns change. The graphic frame then references the chart relationship and gives the shape a name such as Chart 1.
Insert a new worksheet into a spreadsheet
Inserting a worksheet requires creating a worksheet part, adding a workbook relationship, and adding a <sheet/> entry to the workbook's <sheets/> collection.
The package must already be opened for editing. In ooxmlsdk terms, the operation belongs at the workbook part boundary: create a worksheet part, initialize it with an empty worksheet and sheetData, add a relationship from the workbook part to that new part, then append the corresponding sheet entry in workbook XML.
Workbook markup
<sheets>
<sheet name="Summary" sheetId="1" r:id="rId1"/>
<sheet name="New Sheet" sheetId="2" r:id="rId2"/>
</sheets>
The new sheetId must be unique in the workbook, and the r:id must resolve to the new worksheet part.
Choose the next id by scanning the existing workbook sheetId values and adding one to the maximum. The worksheet name is independent from the part path; a typical generated name is Sheet{sheet_id}, but production code should also avoid collisions with existing sheet names.
Rust workflow
#![allow(unused)] fn main() { pub fn insert_new_worksheet( path: &Path, sheet_name: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let worksheet_part = workbook_part.add_new_part_auto_id::<_, WorksheetPart>(&mut document)?; worksheet_part.set_data( &mut document, br#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>"#.to_vec(), )?; let relationship_id = workbook_part .get_id_of_part(&document, &worksheet_part) .expect("worksheet relationship id") .to_string(); let workbook_xml = workbook_part.data_as_str(&document)?.unwrap_or_default(); let updated_workbook_xml = append_sheet(workbook_xml, sheet_name, &relationship_id)?; workbook_part.set_data(&mut document, updated_workbook_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The listing creates the worksheet part from the workbook part, initializes it with an empty <sheetData/>, reads the new relationship id, appends the matching <sheet/> entry, and saves the package. The important invariant is that workbook XML, workbook relationships, and package content types are saved together; updating only <sheets/> leaves a workbook entry that points nowhere.
Insert text into a cell in a spreadsheet
Text can be stored as an inline string or as an index into the shared string table. Excel commonly uses shared strings when saving workbooks.
The upstream workflow inserts text into a new worksheet, so it combines two package operations: add a worksheet to the workbook, then insert a cell value into that worksheet. In Rust, keep those concerns separate unless the public API intentionally creates both in one call.
Shared string cell
<c r="A1" t="s"><v>0</v></c>
The 0 points into xl/sharedStrings.xml.
For shared strings, first locate or create the shared string table. If the target text already exists, reuse its index. If it does not exist, append a new shared string item and use the new index. The cell then stores t="s" and the shared string index in <v>.
Rust workflow
#![allow(unused)] fn main() { pub fn insert_text_into_cell( path: &Path, sheet_name: &str, cell_reference: &str, text: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let worksheet_part = worksheet_part_by_name(&document, &workbook_part, sheet_name)?; let worksheet_xml = worksheet_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = upsert_inline_string_cell(worksheet_xml, cell_reference, text)?; worksheet_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
This listing writes an inline string cell. That avoids rewriting the shared string table, which is often the safer first implementation when inserting a small number of values.
When expanding this into a general writer, preserve worksheet ordering. Find or create the target row, then place the cell before the first existing cell whose column comes after the target column. If a cell already exists at the requested address, update that cell instead of creating a duplicate c element with the same reference.
If your workbook policy requires shared strings, locate or create the shared string table, append or reuse the text, and write a t="s" cell with the shared string index instead.
Merge two adjacent cells in a spreadsheet
Merged cells are stored in worksheet XML under <mergeCells/>. The original cell values remain in the grid; applications display the merged range as one cell.
Before adding a merge range, make sure both endpoint cells exist. A writer normally creates any missing row or cell elements first, then records the range in mergeCells.
Merge markup
<mergeCells count="1">
<mergeCell ref="A1:B1"/>
</mergeCells>
The ref attribute stores the merged range.
The range is expressed with normal cell references such as A1:B1. Parse a cell reference by separating the column letters from the row digits; this is useful both for checking adjacency and for inserting any missing cells in row and column order.
Rust workflow
#![allow(unused)] fn main() { pub fn merge_adjacent_cells( path: &Path, sheet_name: &str, first_cell: &str, second_cell: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let worksheet_part = worksheet_part_by_name(&document, &workbook_part, sheet_name)?; let worksheet_xml = worksheet_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = add_merge_range(worksheet_xml, first_cell, second_cell)?; worksheet_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The listing checks that the two endpoints are adjacent, appends the merge range, and updates the mergeCells count when the collection already exists. A more complete writer should also reject ranges that overlap existing merged ranges.
Only one cell's displayed content is preserved by spreadsheet applications after a merge. For left-to-right sheets, that is typically the upper-left cell in the merged range; for right-to-left sheets, applications can preserve the upper-right value. Avoid relying on values from the other cells in the range.
Open a spreadsheet document for read-only access
Use SpreadsheetDocument to open an .xlsx package and inspect its workbook and worksheet parts. In ooxmlsdk, opening a package does not modify it; changes are persisted only when you call a save method.
For read-only inspection, open the package and avoid saving it.
Open and inspect worksheet parts
#![allow(unused)] fn main() { use std::collections::BTreeMap; use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::chart_part::ChartPart; use ooxmlsdk::parts::drawings_part::DrawingsPart; use ooxmlsdk::parts::ribbon_extensibility_part::RibbonExtensibilityPart; use ooxmlsdk::parts::spreadsheet_document::SpreadsheetDocument; use ooxmlsdk::parts::table_definition_part::TableDefinitionPart; use ooxmlsdk::parts::workbook_part::WorkbookPart; use ooxmlsdk::parts::worksheet_part::WorksheetPart; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, SdkPart, SpreadsheetDocumentType}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct FormulaCell { pub reference: String, pub formula: String, pub cached_value: Option<String>, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct PivotTablePartCounts { pub worksheet_pivot_tables: usize, pub workbook_pivot_caches: usize, } pub fn open_spreadsheet_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; Ok(workbook_part.worksheet_parts(&document).count()) } }
The example uses lazy package opening:
OpenSettings { open_mode: PackageOpenMode::Lazy, ..Default::default() }SpreadsheetDocument::new_from_file_with_settingsworkbook_part()worksheet_parts(&document)
Lazy opening is useful for inspection helpers because it lets you navigate the package model without parsing every root element up front.
The same read-only pattern applies whether the input comes from a file path or a stream-like byte source. With ooxmlsdk, choose the constructor that matches your source, keep the package in inspection mode, and do not call save methods. This mirrors the upstream guidance of using non-editable open modes when the caller only needs to retrieve information.
Spreadsheet package structure
A SpreadsheetML package stores the main workbook in xl/workbook.xml. Worksheets are separate parts, usually under xl/worksheets/, and the workbook part owns relationships to those worksheet parts.
Use relationships and typed part accessors instead of hard-coding ZIP paths whenever possible.
At minimum, a valid spreadsheet package has a workbook part and at least one worksheet part. The workbook is the container for document-level state, while worksheet parts store the grid content as SpreadsheetML XML.
Open a spreadsheet document from a stream
Some applications receive an .xlsx as bytes instead of a filesystem path. The package still has the same SpreadsheetML structure: workbook part, worksheet parts, relationships, and optional supporting parts.
Use a stream-based open path when the caller already owns the bytes, for example from web upload handling, object storage, or another document pipeline. The callee should not assume responsibility for closing or discarding the caller's stream unless its API explicitly takes ownership.
Open from bytes
Use any reader that implements Read + Seek. For in-memory bytes, wrap the Vec<u8> in std::io::Cursor and open the package from that cursor:
#![allow(unused)] fn main() { pub fn open_spreadsheet_from_bytes(bytes: Vec<u8>) -> Result<usize, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new(Cursor::new(bytes))?; let workbook_part = document.workbook_part()?; Ok(workbook_part.worksheet_parts(&document).count()) } }
If the stream workflow also writes to the workbook, it must follow the same invariants as path-based writing: add or update parts, relationships, and content types together, then write the package back through an explicit save path such as an owned Cursor<Vec<u8>> or file handle.
Parse and read a large spreadsheet
Large worksheets should be processed without constructing unnecessary in-memory models. SpreadsheetML stores worksheet data as XML under each worksheet part, so a scalable reader should stream or scan worksheet XML and resolve shared strings only as needed.
The key tradeoff is DOM-style parsing versus streaming parsing. A DOM-style reader is convenient because it materializes typed elements that are easy to inspect, but it loads the whole part into memory. A streaming reader reads one XML event or element at a time and is the preferred shape for worksheets that can grow to hundreds of megabytes.
Package traversal
Start by opening the workbook, loading shared strings once, and visiting worksheet cells through a callback:
#![allow(unused)] fn main() { pub fn visit_first_worksheet_cells( path: &Path, mut visit: impl FnMut(&str, &str), ) -> Result<usize, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let shared_strings = workbook_part .shared_string_table_part(&document) .and_then(|part| { part .data_as_str(&document) .ok() .flatten() .map(extract_shared_strings) }) .unwrap_or_default(); let Some(first_sheet) = workbook_part.worksheet_parts(&document).next() else { return Ok(0); }; let worksheet_xml = first_sheet.data_as_str(&document)?.unwrap_or_default(); Ok(visit_cell_values( worksheet_xml, &shared_strings, |reference, value| { visit(&reference, &value); }, )) } }
This listing keeps the caller's result shape outside the reader. A caller that writes each visited cell to a database or channel can avoid building a worksheet-sized Vec in its own code. For very large worksheets, keep this shape but replace the inner XML scan with an event parser that reads from the worksheet part stream.
Practical notes
- Load the shared string table once if the workbook uses shared strings.
- Process rows incrementally.
- Avoid collecting all cells unless the caller needs random access.
- Treat formulas, dates, booleans, inline strings, and styled numbers as separate conversion cases.
- Keep shared string resolution separate from row iteration so callers can choose between raw cell values and formatted text.
Retrieve a dictionary of all named ranges in a spreadsheet
Named ranges are stored in the workbook part under <definedNames/>. Each <definedName/> maps a name to a reference or formula expression.
Workbook markup
<definedNames>
<definedName name="SalesRange">Summary!$B$2:$B$10</definedName>
</definedNames>
Names can be workbook-scoped or sheet-scoped. Sheet-scoped names include a localSheetId attribute.
Rust workflow
Open the workbook part and inspect xl/workbook.xml:
#![allow(unused)] fn main() { pub fn get_defined_names( path: &Path, ) -> Result<BTreeMap<String, String>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let workbook_xml = workbook_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_defined_names(workbook_xml)) } }
The function returns an ordered map from defined-name text to the workbook expression stored in the element body. If the workbook has no <definedNames/> collection, the map is empty.
The element body can be a plain range, a sheet-qualified range, or a formula-like expression. Do not treat every value as a rectangular cell range without validating it first.
Retrieve a list of the hidden rows or columns in a spreadsheet
Hidden rows and columns are stored in worksheet XML. Rows use a hidden attribute on <row/>; columns use a hidden attribute on <col/>.
Worksheet markup
<cols>
<col min="2" max="2" hidden="1"/>
</cols>
<sheetData>
<row r="3" hidden="1"/>
</sheetData>
Column definitions can cover ranges with min and max, so a single <col/> entry can hide more than one column.
Rust workflow
Find the sheet entry by name, resolve the corresponding worksheet part, then inspect <cols/> and <row/> elements:
#![allow(unused)] fn main() { pub fn get_hidden_rows_or_columns( path: &Path, sheet_name: &str, detect_rows: bool, ) -> Result<Vec<u32>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let workbook_xml = workbook_part.data_as_str(&document)?.unwrap_or_default(); let Some(sheet_index) = extract_sheet_names(workbook_xml) .iter() .position(|name| name == sheet_name) else { return Ok(Vec::new()); }; let Some(worksheet_part) = workbook_part.worksheet_parts(&document).nth(sheet_index) else { return Ok(Vec::new()); }; let worksheet_xml = worksheet_part.data_as_str(&document)?.unwrap_or_default(); Ok(if detect_rows { extract_hidden_rows(worksheet_xml) } else { extract_hidden_columns(worksheet_xml) }) } }
Rows and columns are numbered starting at 1. Hidden rows can be collected directly from the r attribute of hidden row elements. Hidden columns need one extra step: expand every hidden col range from min through max, inclusive.
Retrieve a list of the hidden worksheets in a spreadsheet
Worksheet visibility is stored in the workbook <sheet/> entries. A hidden worksheet usually has state="hidden" or state="veryHidden".
Workbook markup
<sheets>
<sheet name="Summary" sheetId="1" r:id="rId1"/>
<sheet name="Hidden Data" sheetId="2" state="hidden" r:id="rId2"/>
</sheets>
The worksheet part itself does not carry the display name or workbook visibility state.
Rust workflow
Read the workbook sheet list and filter entries with a hidden state:
#![allow(unused)] fn main() { pub fn get_hidden_worksheets(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let workbook_xml = workbook_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_hidden_sheet_names(workbook_xml)) } }
The workbook XML is the source of truth for this query. worksheet_parts(&document) gives access to worksheet content, but hidden state is part of the workbook sheet entry. Treat both hidden and veryHidden as hidden worksheets; the latter is not normally exposed through the worksheet tab UI.
Retrieve the values of cells in a spreadsheet
Cell values are stored in worksheet XML. Text cells often use the shared string table: the cell stores an integer index and the actual text is stored once in xl/sharedStrings.xml.
Read cell values
#![allow(unused)] fn main() { pub fn get_cell_values(path: &Path) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let shared_strings = workbook_part .shared_string_table_part(&document) .and_then(|part| { part .data_as_str(&document) .ok() .flatten() .map(extract_shared_strings) }) .unwrap_or_default(); let Some(first_sheet) = workbook_part.worksheet_parts(&document).next() else { return Ok(Vec::new()); }; let worksheet_xml = first_sheet.data_as_str(&document)?.unwrap_or_default(); Ok(extract_cell_values(worksheet_xml, &shared_strings)) } }
The helper reads the shared string table when present, then reads the first worksheet and resolves cells with t="s" through that table. Numeric cells are returned from their <v/> value directly.
For a sheet-specific helper, first find the workbook sheet entry by name, use its relationship id to resolve the worksheet part, and then search that worksheet for the requested cell reference. If the sheet or cell is missing, return None or an explicit domain error instead of defaulting to a misleading value.
Cell markup
<row r="1">
<c r="A1" t="s"><v>0</v></c>
<c r="B1"><v>42</v></c>
</row>
In this example, A1 uses shared string index 0; B1 stores the numeric value directly. Dates, formulas, booleans, inline strings, and styled values need additional interpretation from styles and formula cache data.
Cell type drives interpretation. A missing t attribute usually means the value is numeric or date-like, depending on the cell style. t="s" means the value is a shared string index. t="b" stores booleans as 0 or 1. Formula cells can contain <f> plus a cached <v> result; reading the cached value does not recalculate the formula.
Retrieve a list of the worksheets in a spreadsheet document
Worksheet names are stored in the workbook part, not inside the worksheet parts. The workbook <sheets/> collection maps each visible workbook entry to a relationship id.
Read worksheet names
#![allow(unused)] fn main() { pub fn list_worksheets(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let workbook_xml = workbook_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_sheet_names(workbook_xml)) } }
When you need the relationship ids as well as the sheet names, use related_parts_of_type at the workbook part boundary and keep those ids aligned with the r:id values in workbook XML:
#![allow(unused)] fn main() { pub fn list_worksheet_relationship_ids( path: &Path, ) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; Ok( workbook_part .related_parts_of_type::<_, WorksheetPart>(&document) .map(|related| related.relationship_id().to_string()) .collect(), ) } }
The helper opens the workbook part, reads xl/workbook.xml, and extracts each <sheet name="..."/> value. The same workbook XML also contains sheetId, r:id, and optional state attributes such as hidden.
The returned list can be empty only for an invalid or unusual workbook; a well-formed spreadsheet normally has at least one sheet entry.
Workbook markup
<sheets>
<sheet name="Summary" sheetId="1" r:id="rId1"/>
<sheet name="Hidden Data" sheetId="2" state="hidden" r:id="rId2"/>
</sheets>
Use WorkbookPart::worksheet_parts(&document) when you need the actual worksheet XML. Use workbook XML when you need workbook metadata such as names and visibility state.
The workbook sheet element is metadata, not the worksheet part itself. Resolve the r:id relationship when you need to move from a listed sheet to its worksheet XML.
Working with the calculation chain
The calculation chain records the order in which formula cells were last calculated. It is stored in an optional workbook-level part rooted at <calcChain/>.
An instance of this part contains an ordered set of references to formula cells across the workbook. A package can contain no more than one calculation chain part, and its root element is calcChain.
Calculation chain markup
<calcChain xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<c r="B2" i="1"/>
<c r="B3" s="1"/>
<c r="A2"/>
</calcChain>
Each <c/> entry identifies a formula cell. The r attribute is the cell reference, and i can identify the sheet index.
The generated ooxmlsdk schema types include CalculationChain for <calcChain/> and CalculationCell for each <c/> entry. The chain records the last calculation order; it does not describe the full dependency graph between formulas and input cells.
Rust workflow
Start from the workbook part. ooxmlsdk exposes WorkbookPart::calculation_chain_part(&document) when the package contains one.
#![allow(unused)] fn main() { use std::collections::BTreeMap; use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::chart_part::ChartPart; use ooxmlsdk::parts::drawings_part::DrawingsPart; use ooxmlsdk::parts::ribbon_extensibility_part::RibbonExtensibilityPart; use ooxmlsdk::parts::spreadsheet_document::SpreadsheetDocument; use ooxmlsdk::parts::table_definition_part::TableDefinitionPart; use ooxmlsdk::parts::workbook_part::WorkbookPart; use ooxmlsdk::parts::worksheet_part::WorksheetPart; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, SdkPart, SpreadsheetDocumentType}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct FormulaCell { pub reference: String, pub formula: String, pub cached_value: Option<String>, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct PivotTablePartCounts { pub worksheet_pivot_tables: usize, pub workbook_pivot_caches: usize, } pub fn open_spreadsheet_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; Ok(workbook_part.worksheet_parts(&document).count()) } }
When editing formulas, stale calculation chain data can be worse than no chain at all. A writer should either update the chain consistently or remove it and let the spreadsheet application rebuild calculation state.
Spreadsheet applications may rebuild calculation state in memory when opening the workbook. The order in the calculation chain is historical calculation metadata, not a command that forces every application to calculate in exactly that order.
Working with conditional formatting
Conditional formatting is stored in worksheet XML. Each <conditionalFormatting/> element declares the cell range it applies to and contains one or more rules.
Conditional formatting is worksheet-level markup. It can apply to ordinary cell ranges and does not require the cells to be part of a table. The sqref attribute stores one or more target ranges, for example A1:A10.
Conditional formatting markup
<conditionalFormatting sqref="C3:C8">
<cfRule type="top10" dxfId="1" priority="3" rank="2"/>
</conditionalFormatting>
Rules can express cell comparisons, top/bottom items, data bars, color scales, icon sets, duplicate values, formulas, and other conditions.
<conditionalFormatting sqref="E3:E9">
<cfRule type="cellIs" dxfId="0" priority="1" operator="greaterThan">
<formula>0.5</formula>
</cfRule>
</conditionalFormatting>
In ooxmlsdk, the generated schema types include ConditionalFormatting, ConditionalFormattingRule, DataBar, ColorScale, and IconSet. A dxfId references differential formatting in the styles part, so readers often need worksheet XML and styles XML together.
Data bars use conditional-format value objects (cfvo) for minimum and maximum thresholds plus a color. Color scales use two or three cfvo entries and matching colors. Icon sets use threshold values to decide which icon applies to each cell. Rule priority values are global within the worksheet, so insertion code must preserve a coherent priority order.
Rust workflow
Open the worksheet part and append a cellIs rule with the next worksheet priority:
#![allow(unused)] fn main() { pub fn add_greater_than_conditional_formatting( path: &Path, sheet_name: &str, range: &str, threshold: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let worksheet_part = worksheet_part_by_name(&document, &workbook_part, sheet_name)?; let worksheet_xml = worksheet_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = append_greater_than_rule(worksheet_xml, range, threshold)?; worksheet_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Rules that reference dxfId also need matching differential formatting in the styles part. The example above uses a formula-only rule; add a styles part update when the rule should apply custom formatting.
Working with formulas
Formulas are stored in worksheet cells with <f/>. The cached value from the last calculation, when present, is stored in <v/>.
Formulas are SpreadsheetML expressions. They can contain constants, arithmetic and comparison operators, cell references, named ranges, and function calls such as SUM(C6:C10).
Formula markup
<c r="A6">
<f>SUM(A1:A5)</f>
<v>15</v>
</c>
The formula text is not evaluated by ooxmlsdk. Spreadsheet applications may recalculate it when the workbook is opened. If you edit formula inputs or formula text, make sure cached values and calculation metadata are still appropriate for your use case.
The generated schema type for <f/> is CellFormula. The cached <v/> result is optional; omitting it leaves recalculation to the spreadsheet application. Keeping an old cached value after changing a formula can display stale data in software that trusts the cache.
Rust workflow
Use the worksheet traversal pattern to find cells that contain <f/> and report both the formula text and any cached value:
#![allow(unused)] fn main() { pub fn get_formula_cells(path: &Path) -> Result<Vec<FormulaCell>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let Some(first_sheet) = workbook_part.worksheet_parts(&document).next() else { return Ok(Vec::new()); }; let worksheet_xml = first_sheet.data_as_str(&document)?.unwrap_or_default(); Ok(extract_formula_cells(worksheet_xml)) } }
For write paths, update only the intended <f/> and cached <v/> nodes, and decide whether to remove or refresh the calculation chain. ooxmlsdk does not calculate formulas; it preserves the package data you write.
Working with PivotTables
PivotTables are represented by several coordinated parts. A worksheet can own one or more pivot table definition parts, while the workbook owns pivot cache definitions and those cache definitions can own cache records.
PivotTables present aggregated views of source data. The visible PivotTable cells on the worksheet are display data; the reusable source field metadata and cached records live in the pivot cache parts.
PivotTable package model
| Part | Purpose |
|---|---|
| Pivot table definition | Layout of the PivotTable on a worksheet |
| Pivot cache definition | Fields, source data definition, shared cache items |
| Pivot cache records | Cached source records |
| Worksheet | Displayed PivotTable cells and relationship to the definition |
The pivot table definition references a pivot cache. Multiple PivotTables can use the same cache.
The pivot table definition describes which fields appear on the row axis, column axis, values area, and report filter area. The pivot cache definition describes all available source fields, including fields that a particular PivotTable is not currently using.
PivotTable markup
<pivotTableDefinition name="PivotTable1" cacheId="1">
<location ref="A3:C10" firstHeaderRow="1" firstDataRow="2"/>
<pivotFields count="2">
<pivotField axis="axisRow"/>
<pivotField dataField="1"/>
</pivotFields>
</pivotTableDefinition>
Rust workflow
Use the workbook and worksheet part graph to locate pivot-related parts. WorkbookPart exposes pivot cache definition parts, and WorksheetPart exposes pivot table parts.
#![allow(unused)] fn main() { pub fn count_pivottable_parts( path: &Path, ) -> Result<PivotTablePartCounts, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let worksheet_pivot_tables = workbook_part .worksheet_parts(&document) .map(|worksheet_part| worksheet_part.pivot_table_parts(&document).count()) .sum(); let workbook_pivot_caches = workbook_part .pivot_table_cache_definition_parts(&document) .count(); Ok(PivotTablePartCounts { worksheet_pivot_tables, workbook_pivot_caches, }) } }
In ooxmlsdk, the generated part graph includes WorkbookPart::pivot_table_cache_definition_parts, WorksheetPart::pivot_table_parts, PivotTablePart::pivot_table_cache_definition_part, and PivotTableCacheDefinitionPart::pivot_table_cache_records_part. The corresponding schema types include PivotTableDefinition, PivotField, PivotCacheDefinition, and PivotCacheRecords.
Creating or editing PivotTables requires coordinated cache, worksheet, relationship, and display cell updates. Keep those updates together: a pivot table definition without a matching cache, worksheet relationship, and displayed cell range is not a complete PivotTable.
Working with the shared string table
The shared string table stores one copy of each workbook string. Worksheet cells can then store an index into that table instead of repeating the same text in every cell.
A workbook can contain one shared string table part. The part is rooted at <sst/>, and cells reference strings by zero-based index. Excel commonly writes shared strings, but inline strings are also valid SpreadsheetML.
Shared string structure
The table is rooted at <sst/>, and each shared string item is stored in <si/>.
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="3" uniqueCount="3">
<si><t>Region</t></si>
<si><t>Sales</t></si>
<si><t>North</t></si>
</sst>
A cell that references the first shared string stores t="s" and <v>0</v>:
<c r="A1" t="s"><v>0</v></c>
Reading shared strings in Rust
The cell value example reads the workbook's shared string table, then resolves shared string indexes while reading worksheet cells:
#![allow(unused)] fn main() { pub fn get_cell_values(path: &Path) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let shared_strings = workbook_part .shared_string_table_part(&document) .and_then(|part| { part .data_as_str(&document) .ok() .flatten() .map(extract_shared_strings) }) .unwrap_or_default(); let Some(first_sheet) = workbook_part.worksheet_parts(&document).next() else { return Ok(Vec::new()); }; let worksheet_xml = first_sheet.data_as_str(&document)?.unwrap_or_default(); Ok(extract_cell_values(worksheet_xml, &shared_strings)) } }
Rich text shared strings can contain multiple runs under a single <si/>. A reader must concatenate or preserve those runs depending on whether it needs plain text or formatting.
Simple shared strings store one <t/> child. Rich text shared strings store multiple <r/> runs, each with optional run properties (<rPr/>) and text (<t/>). Whitespace-sensitive text can use xml:space="preserve".
In ooxmlsdk, use WorkbookPart::shared_string_table_part(&document) to locate the part when present. The generated schema types include SharedStringTable, SharedStringItem, and Text. When writing shared strings, update the table and every referencing cell together; deleting or reordering items requires rewriting indexes in worksheet cells.
Working with sheets
Sheets are workbook entries that point to sheet parts. The most common sheet type is a worksheet, which stores a grid of rows and cells. Workbooks can also contain chartsheets, dialog sheets, and macro sheets.
Sheets are where most spreadsheet work happens. Worksheets store rows, cells, formulas, formatting references, tables, filters, drawings, and other grid-connected features. Chartsheets store a chart as its own sheet, and dialog sheets represent legacy custom dialog UI.
Workbook sheet list
The workbook part stores sheet metadata:
<sheets>
<sheet name="Summary" sheetId="1" r:id="rId1"/>
<sheet name="Hidden Data" sheetId="2" state="hidden" r:id="rId2"/>
</sheets>
The name and state attributes are workbook metadata. The actual worksheet XML is in the target part resolved by r:id.
Worksheet parts
A worksheet part is rooted at <worksheet/> and contains the required <sheetData/> element.
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1">
<c r="A1" t="s"><v>0</v></c>
</row>
</sheetData>
</worksheet>
Use the workbook part to traverse worksheet parts:
#![allow(unused)] fn main() { pub fn get_worksheet_xml(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let mut worksheets = Vec::new(); for worksheet_part in workbook_part.worksheet_parts(&document) { worksheets.push( worksheet_part .data_as_str(&document)? .unwrap_or_default() .to_string(), ); } Ok(worksheets) } }
For display names or hidden state, read the workbook sheet list:
#![allow(unused)] fn main() { pub fn list_worksheets(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let workbook_xml = workbook_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_sheet_names(workbook_xml)) } }
Cell basics
Rows are stored as <row/> elements and cells as <c/> elements. Cell references use A1 notation such as A1 or B2. The <v/> value is either the raw value or an index into another structure, depending on the cell type.
The smallest valid blank worksheet is a worksheet root with an empty sheetData child. Optional sheet properties can appear before sheetData; optional supporting features such as protection, filters, drawings, and table references can appear after it.
In ooxmlsdk, generated schema types include Worksheet, SheetData, Row, Cell, CellValue, Chartsheet, and Drawing. Prefer typed part accessors such as WorkbookPart::worksheet_parts(&document) and WorkbookPart::chartsheet_parts(&document) when traversing package structure.
Working with SpreadsheetML tables
A SpreadsheetML table is a logical range over worksheet cells. The worksheet stores the actual cell values; a separate table definition part stores table metadata such as name, range, columns, and filtering.
Tables organize worksheet ranges as named datasets. They can expose filter and sort controls, structured references, calculated columns, style information, and automatic expansion behavior in spreadsheet applications.
Table markup
<table
id="1"
name="SalesTable"
displayName="SalesTable"
ref="A1:B10"
totalsRowShown="0"
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<autoFilter ref="A1:B10"/>
<tableColumns count="2">
<tableColumn id="1" name="Region"/>
<tableColumn id="2" name="Sales"/>
</tableColumns>
</table>
The worksheet points to table definition parts with <tableParts/> and relationships.
The table part contains metadata only; the cell data remains in the worksheet. The ref attribute covers the full table range, including headers. id and name must be unique across table parts, and displayName must also be unique across workbook defined names because formulas can reference it.
Rust workflow
Create a table definition part, write table metadata, then add a tableParts reference to the worksheet:
#![allow(unused)] fn main() { pub fn add_table( path: &Path, sheet_name: &str, table_name: &str, range: &str, columns: &[&str], ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?; let workbook_part = document.workbook_part()?; let worksheet_part = worksheet_part_by_name(&document, &workbook_part, sheet_name)?; let table_part = worksheet_part.add_new_part_auto_id::<_, TableDefinitionPart>(&mut document)?; let relationship_id = worksheet_part .get_id_of_part(&document, &table_part) .expect("table relationship id") .to_string(); let table_id = max_table_id(&worksheet_part, &document) + 1; table_part.set_data( &mut document, table_xml(table_id, table_name, range, columns).into_bytes(), )?; let worksheet_xml = worksheet_part.data_as_str(&document)?.unwrap_or_default(); let worksheet_xml = ensure_relationship_namespace(worksheet_xml); let updated_xml = append_table_reference(&worksheet_xml, &relationship_id)?; worksheet_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
To keep autofilter enabled, include an autoFilter element, even if it has no active criteria. Table columns live under tableColumns, whose count must match the number of tableColumn children.
In ooxmlsdk, WorksheetPart::table_definition_parts(&document) traverses table definition parts, and generated schema types include Table, TableColumn, and AutoFilter.
Word processing
This section covers WordprocessingML packages (.docx, .docm, .dotx) with ooxmlsdk.
Wordprocessing packages are made of a main document part, optional styles, comments, numbering, settings, headers, footers, footnotes, endnotes, media, custom properties, and relationships between those parts. In ooxmlsdk, the entry point is usually ooxmlsdk::parts::wordprocessing_document::WordprocessingDocument.
Use the parts feature, enabled by default, to open and save packages. Examples in this section are backed by tested Rust code in listings/word.
In this section
- Structure of a WordprocessingML document
- Accept all revisions in a word processing document
- Add tables to word processing documents
- Apply a style to a paragraph in a word processing document
- Change the print orientation of a word processing document
- Change text in a table in a word processing document
- Convert a word processing document from the DOCM to the DOCX file format
- Create and add a character style to a word processing document
- Create and add a paragraph style to a word processing document
- Create a word processing document by providing a file name
- Delete comments by all or a specific author in a word processing document
- Extract styles from a word processing document
- Insert a comment into a word processing document
- Insert a picture into a word processing document
- Insert a table into a word processing document
- Open and add text to a word processing document
- Open a word processing document for read-only access
- Open a word processing document from a stream
- Remove hidden text from a word processing document
- Remove the headers and footers from a word processing document
- Replace text in a word document using SAX
- Replace the header in a word processing document
- Replace the styles parts in a word processing document
- Retrieve application property values from a word processing document
- Retrieve comments from a word processing document
- Set a custom property in a word processing document
- Set the font for a text run
- Validate a word processing document
- Working with paragraphs
- Working with runs
- Working with WordprocessingML tables
Writer-focused chapters are being ported only when the code has a fixture in listings/ and passes cargo test --workspace.
Related sections
Structure of a WordprocessingML document
A WordprocessingML file is an Open Packaging Convention package. The .docx file is a ZIP container whose parts are connected by relationship items. ooxmlsdk exposes that graph through WordprocessingDocument and generated part accessors.
The basic main-document structure is document -> body -> block-level elements such as paragraphs. A paragraph (p) contains one or more runs (r), and a run contains text (t) plus any run-level properties that apply to that range of text.
Package parts
| Package part | Root element | ooxmlsdk access |
|---|---|---|
| Main document | <w:document/> | WordprocessingDocument::main_document_part() |
| Styles | <w:styles/> | MainDocumentPart::style_definitions_part(&document) |
| Comments | <w:comments/> | MainDocumentPart::wordprocessing_comments_part(&document) |
| Settings | <w:settings/> | MainDocumentPart::document_settings_part(&document) |
| Numbering | <w:numbering/> | MainDocumentPart::numbering_definitions_part(&document) |
| Headers | <w:hdr/> | MainDocumentPart::header_parts(&document) |
| Footers | <w:ftr/> | MainDocumentPart::footer_parts(&document) |
| Footnotes | <w:footnotes/> | MainDocumentPart::footnotes_part(&document) |
| Endnotes | <w:endnotes/> | MainDocumentPart::endnotes_part(&document) |
| Glossary document | <w:glossaryDocument/> | MainDocumentPart::glossary_document_part(&document) |
| Extended properties | <Properties/> | WordprocessingDocument::extended_file_properties_part() |
Stories
WordprocessingML organizes content into stories. The main document story is required and lives in the main document part. Other stories, such as comments, headers, footers, footnotes, endnotes, glossary content, text boxes, and subdocuments, appear only when the package needs them.
A minimal valid .docx only needs the main document story. It does not need comments, styles, numbering, headers, or other supporting parts unless the content references them.
Main document XML
The main document part stores body content under <w:body/>.
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p>
<w:r><w:t>Hello</w:t></w:r>
</w:p>
<w:sectPr/>
</w:body>
</w:document>
Open and inspect it through the package model:
#![allow(unused)] fn main() { use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::header_part::HeaderPart; use ooxmlsdk::parts::image_part::ImagePart; use ooxmlsdk::parts::style_definitions_part::StyleDefinitionsPart; use ooxmlsdk::parts::wordprocessing_comments_part::WordprocessingCommentsPart; use ooxmlsdk::parts::wordprocessing_document::WordprocessingDocument; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, SdkPart, WordprocessingDocumentType}; use ooxmlsdk::validator::ValidationErrorInfo; pub fn open_word_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); Ok(xml.matches("<w:p").count()) } }
Use generated part accessors for relationships, and read XML from parts only when the chapter needs schema-level inspection.
A typical document is larger than the minimum package. It often adds style definitions, section properties, headers and footers, numbering, comments, footnotes, endnotes, media, and document properties. Keep these as separate parts instead of inlining their XML into the main document part.
Accept all revisions in a word processing document
Tracked revisions are stored as WordprocessingML markup in the main document and supporting parts. Accepting revisions means transforming inserted, deleted, moved, and property-change markup into the final accepted content.
The document structure still follows the normal WordprocessingML hierarchy: document, body, paragraphs (p), runs (r), and text (t). Revision elements wrap or annotate that content and must be resolved without breaking the surrounding paragraph, run, and table structure.
Revision markup
Common revision elements include:
| Element | Meaning when accepting |
|---|---|
<w:pPrChange/> | Keep the current paragraph properties and remove the tracked previous state. |
<w:del/> | Remove deleted content or paragraph mark revision metadata, depending on where it appears. |
<w:ins/> | Keep inserted content and remove insertion metadata. |
<w:moveFrom/> / ranges | Remove the move source content. |
<w:moveTo/> / ranges | Keep the move destination content and remove move metadata. |
In ooxmlsdk, generated schema types for these elements include ParagraphPropertiesChange, Deleted, Inserted, MoveFrom, MoveTo, MoveFromRangeStart, MoveFromRangeEnd, MoveToRangeStart, and MoveToRangeEnd.
Rust workflow
Accept common revision markup in the main document XML:
#![allow(unused)] fn main() { pub fn accept_common_revisions(path: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let document_xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = accept_revision_markup(document_xml); main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Do not treat revision acceptance as a simple string replacement. Some revisions live in paragraph properties, table row properties, run content, comments, headers, footers, footnotes, or endnotes. Apply the same acceptance rules to every story that can contain tracked changes when you need whole-document acceptance.
Add tables to word processing documents
Tables are inserted as <w:tbl/> elements in the main document body. A table contains rows, cells, and usually paragraphs inside each cell.
The caller typically supplies a rectangular collection of strings. A writer turns each outer item into a table row and each value into a table cell containing at least one paragraph and run.
Table markup
<w:tbl>
<w:tblPr>
<w:tblBorders>
<w:top w:val="single" w:sz="12"/>
<w:bottom w:val="single" w:sz="12"/>
<w:left w:val="single" w:sz="12"/>
<w:right w:val="single" w:sz="12"/>
<w:insideH w:val="single" w:sz="12"/>
<w:insideV w:val="single" w:sz="12"/>
</w:tblBorders>
</w:tblPr>
<w:tr>
<w:tc>
<w:tcPr><w:tcW w:type="auto"/></w:tcPr>
<w:p><w:r><w:t>Cell text</w:t></w:r></w:p>
</w:tc>
</w:tr>
</w:tbl>
In ooxmlsdk, generated schema types include Table, TableProperties, TableBorders, TableRow, TableCell, TableCellProperties, TableCellWidth, Paragraph, Run, and Text.
Rust workflow
#![allow(unused)] fn main() { pub fn insert_table(path: &Path, rows: &[&[&str]]) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); let table = table_xml(rows); let updated_xml = insert_before_section_properties(xml, &table)?; main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The listing creates a simple table from string rows and cells, then inserts it before the body section properties (<w:sectPr/>) if that element is present at the end of the body.
A production table writer can extend this with table properties, borders, widths, grid columns, and style ids. Appending after section properties can produce invalid or surprising document structure.
Apply a style to a paragraph in a word processing document
Paragraph styles are referenced from paragraph properties with <w:pStyle/>. The style definition lives in word/styles.xml.
Applying a style requires both a paragraph reference and a style id. The style id is the stable value used in markup; the friendly style name can be different, such as Heading 1 for the style id Heading1.
Style reference markup
<w:pPr>
<w:pStyle w:val="Heading1"/>
</w:pPr>
If the target paragraph has no <w:pPr/>, create paragraph properties before adding <w:pStyle/>. Paragraph properties are the place for paragraph-level formatting such as alignment, indentation, borders, line spacing, shading, text direction, and style references.
Rust workflow
Verify the style id exists, then update the first paragraph's paragraph properties:
#![allow(unused)] fn main() { pub fn apply_style_to_first_paragraph( path: &Path, style_id: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let Some(styles_part) = main_part.style_definitions_part(&document) else { return Err("document has no styles part".into()); }; let styles_xml = styles_part.data_as_str(&document)?.unwrap_or_default(); if !styles_xml.contains(&format!(r#"w:styleId="{style_id}""#)) { return Err(format!("style {style_id} not found").into()); } let document_xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = set_first_paragraph_style(document_xml, style_id)?; main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Applying a nonexistent style id does not make the document display that style. A robust writer should read the style definitions part, check by styleId, optionally map from style name to id, and either add the missing style or return a clear error. A styles part is optional in a minimal document, so code must handle the missing-part case explicitly.
In ooxmlsdk, generated schema types include Styles, Style, ParagraphProperties, ParagraphStyleId, and StyleRunProperties.
Change the print orientation of a word processing document
Page orientation is stored in section properties, usually in <w:pgSz/> under <w:sectPr/>.
A document can have more than one section. Updating orientation for the whole document means walking every section properties element, not just the final <w:sectPr/> in the body.
Orientation markup
<w:sectPr>
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/>
<w:pgSz w:w="15840" w:h="12240" w:orient="landscape"/>
</w:sectPr>
When changing between portrait and landscape, update w:orient and swap page width and height. If margins are present, rotate them to match the new page orientation. Some printers treat the rotation direction differently, so code that must preserve exact physical margins should make that policy explicit.
Rust workflow
Open the main document part, locate section properties, and write a matching page size:
#![allow(unused)] fn main() { pub fn change_print_orientation( path: &Path, landscape: bool, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let document_xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = set_section_orientation(document_xml, landscape)?; main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
In ooxmlsdk, generated schema types include SectionProperties, PageSize, and PageMargin. The w:orient attribute can be absent; absence normally behaves like portrait, so a writer should avoid rewriting sections whose effective orientation already matches the requested value.
Change text in a table in a word processing document
Table cell text is stored in paragraphs inside <w:tc/> cells. Changing table text requires locating the correct table, row, cell, paragraph, and run.
Tables are block-level content in the document body. A table (tbl) contains table properties, an optional table grid, rows (tr), cells (tc), and block-level content inside each cell. Even an otherwise empty cell normally contains a paragraph.
Table structure
<w:tbl>
<w:tblPr>
<w:tblW w:w="5000" w:type="pct"/>
<w:tblBorders>
<w:top w:val="single" w:sz="4"/>
<w:left w:val="single" w:sz="4"/>
<w:bottom w:val="single" w:sz="4"/>
<w:right w:val="single" w:sz="4"/>
</w:tblBorders>
</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="10296"/>
</w:tblGrid>
<w:tr>
<w:tc>
<w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>
<w:p><w:r><w:t>Cell text</w:t></w:r></w:p>
</w:tc>
</w:tr>
</w:tbl>
Rust workflow
#![allow(unused)] fn main() { pub fn change_text_in_first_table_cell( path: &Path, new_text: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = replace_first_table_cell_text(xml, new_text)?; main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The listing scopes the edit to the first table and replaces the first text element in that table. Avoid broad string replacement across document.xml; parse table boundaries and update only the target cell content.
The upstream sample targets the first table, second row, and third cell, then replaces text in the first run of the first paragraph. A production Rust API should make table, row, and cell selection explicit, handle missing rows or cells as errors, and decide whether replacing text should preserve existing runs or rebuild the cell paragraph.
In ooxmlsdk, generated schema types include Table, TableProperties, TableGrid, GridColumn, TableRow, TableCell, TableCellProperties, Paragraph, Run, and Text.
Convert a word processing document from DOCM to DOCX
Converting from macro-enabled .docm to .docx requires removing macro-related parts and changing package content types so the package no longer advertises VBA content.
The macro project is stored as a binary vbaProject part. Removing only the .docm extension is not a conversion; the package must no longer contain or reference the VBA project, and the main document content type must match a standard .docx document.
Rust workflow
Open the package, delete the VBA project relationship when present, change the document type to the standard Wordprocessing document content type, and save the package to the caller's output path:
#![allow(unused)] fn main() { pub fn convert_docm_to_docx(path: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; if let Some(vba_part) = main_part.vba_project_part(&document) { main_part.delete_part(&mut document, vba_part)?; } document.change_document_type(WordprocessingDocumentType::Document)?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
This listing covers the package-level conversion path. A production converter should still decide how to handle macro-related custom UI, application-specific metadata, signatures, or external workflow rules before writing the output .docx.
In ooxmlsdk, MainDocumentPart::vba_project_part(&document) locates a VBA project part when one is related from the main document part. delete_part removes that relationship and target part from the package model, and change_document_type(WordprocessingDocumentType::Document) updates the main document content type.
If the source file has no VBA project part, the conversion can be a no-op at the package level, but code should still avoid overwriting an existing .docx output unexpectedly.
Create and add a character style to a word processing document
Character styles are stored in the styles part with w:type="character" and are referenced from run properties.
Style ids are the internal identifiers used in WordprocessingML. Display names and aliases are UI-facing metadata and can differ from the styleId.
Style markup
<w:style w:type="character" w:styleId="Emphasis">
<w:aliases w:val="Important, Highlight"/>
<w:name w:val="Emphasis"/>
<w:rPr>
<w:rFonts w:ascii="Tahoma"/>
<w:sz w:val="48"/>
<w:color w:themeColor="accent2"/>
<w:b/>
<w:i/>
</w:rPr>
</w:style>
Character styles apply to runs, not whole paragraphs. Reference the style from run properties with <w:rStyle w:val="Emphasis"/>.
WordprocessingML supports paragraph, character, linked, table, numbering, and default paragraph/character property styles. Character styles should contain character-level run properties (rPr) and should not be used as paragraph styles.
Rust workflow
Create or reuse the styles part, append a character style definition, and avoid duplicating an existing style id:
#![allow(unused)] fn main() { pub fn create_character_style( path: &Path, style_id: &str, style_name: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { add_style(path, style_id, style_name, "character") } }
In ooxmlsdk, generated schema types include Styles, Style, Aliases, StyleName, StyleRunProperties, RunStyle, RunFonts, FontSize, Color, Bold, and Italic.
Create and add a paragraph style to a word processing document
Paragraph styles are stored in the styles part with w:type="paragraph".
A paragraph style applies to an entire paragraph and its paragraph mark. It can define paragraph-level properties (pPr) and character-level properties (rPr) that apply to runs in that paragraph.
Style markup
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="heading 1"/>
<w:basedOn w:val="Normal"/>
<w:next w:val="Normal"/>
<w:link w:val="Heading1Char"/>
<w:pPr/>
<w:rPr/>
</w:style>
The next element controls the style applied to the next paragraph when editing. The link element can associate a paragraph style with a character style. The styleId is the value referenced by paragraph properties:
<w:pPr>
<w:pStyle w:val="Heading1"/>
</w:pPr>
Rust workflow
Create or reuse the styles part, append a paragraph style definition, and preserve existing styles:
#![allow(unused)] fn main() { pub fn create_paragraph_style( path: &Path, style_id: &str, style_name: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { add_style(path, style_id, style_name, "paragraph") } }
In ooxmlsdk, generated schema types include Styles, Style, Aliases, StyleName, BasedOn, NextParagraphStyle, LinkedStyle, PrimaryStyle, StyleParagraphProperties, StyleRunProperties, and ParagraphStyleId.
Create a word processing document by providing a file name
Creating a .docx from scratch requires package relationships, content types, and at least a main document part with valid WordprocessingML.
In ooxmlsdk, create the package with WordprocessingDocument::create(WordprocessingDocumentType::Document), add the main document part, write valid WordprocessingML, and save the package to the file or writer your application owns.
Choose the package type and file extension together. A standard document should be saved as .docx; macro-enabled documents and templates require different content types and extensions. Use WordprocessingDocumentType::MacroEnabledDocument, Template, or MacroEnabledTemplate when the target package is not a normal .docx.
Minimal package pieces
A minimal document includes:
[Content_Types].xml,_rels/.relspointing toword/document.xml,word/document.xml,- optional supporting parts such as styles, settings, and app properties.
This minimal writer creates that structure in memory:
#![allow(unused)] fn main() { pub fn create_word_document(text: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::create(WordprocessingDocumentType::Document); let main_part = document.add_main_document_part()?; let escaped_text = escape_xml_text(text); let xml = format!( r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>{escaped_text}</w:t></w:r></w:p></w:body></w:document>"# ); main_part.set_data(&mut document, xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
For template workflows, WordprocessingDocument::create_from_template opens a .dotx or .dotm as an editable regular document package. Use document_type() to inspect the current type and change_document_type(...) when converting the package content type deliberately.
The minimal main document XML is a document root with a body, usually containing at least one paragraph, run, and text element:
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p>
<w:r><w:t>Create text in body</w:t></w:r>
</w:p>
</w:body>
</w:document>
In ooxmlsdk, generated schema types include Document, Body, Paragraph, Run, and Text. The example above writes XML bytes directly for compactness; a larger writer can instead build the generated root type and call set_root_element.
Delete comments by all or a specific author in a word processing document
Deleting comments requires editing both the comments part and references in the main document body.
The comments part stores <w:comment/> elements with ids and author metadata. The main document story stores matching range and reference markers. Filtering by author should happen in the comments part first; then use the matching ids to remove references from document content.
Rust workflow
Filter the comments part by author, then remove the corresponding body markers:
#![allow(unused)] fn main() { pub fn delete_comments_by_author( path: &Path, author: Option<&str>, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let Some(comments_part) = main_part.wordprocessing_comments_part(&document) else { let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; return Ok(buffer.into_inner()); }; let comments_xml = comments_part.data_as_str(&document)?.unwrap_or_default(); let deleted_ids = matching_comment_ids(comments_xml, author); let updated_comments = remove_comments_by_id(comments_xml, &deleted_ids); comments_part.set_data(&mut document, updated_comments.into_bytes())?; let document_xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_document = remove_comment_markers(document_xml, &deleted_ids); main_part.set_data(&mut document, updated_document.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
For each deleted comment id, remove all matching commentRangeStart, commentRangeEnd, and commentReference elements from the main document. If a package has comments in headers, footers, footnotes, or endnotes, apply the same marker cleanup in those stories too.
In ooxmlsdk, generated schema types include Comments, Comment, CommentRangeStart, CommentRangeEnd, and CommentReference. MainDocumentPart::wordprocessing_comments_part(&document) locates the comments part when it exists.
Extract styles from a word processing document
Styles are stored in the style definitions part, usually word/styles.xml. Paragraphs and runs refer to styles by id.
Some documents also contain a stylesWithEffects part. Word 2013 and later can keep both the original styles part and a styles-with-effects part so the document can round-trip through older Word versions. Callers need to decide which part they want to inspect.
Read style ids
#![allow(unused)] fn main() { pub fn get_style_ids(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let Some(styles_part) = main_part.style_definitions_part(&document) else { return Ok(Vec::new()); }; let xml = styles_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_style_ids(xml)) } }
The helper follows the main document's styles relationship and extracts w:styleId values.
Style markup
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="heading 1"/>
</w:style>
Styles can define paragraph, character, table, and numbering behavior. A style id is the stable value referenced from document content.
For full extraction, return the complete XML for style_definitions_part or styles_with_effects_part rather than only style ids. If the requested part is absent, return None or an empty result explicitly.
In ooxmlsdk, MainDocumentPart::style_definitions_part(&document) locates word/styles.xml, and MainDocumentPart::styles_with_effects_part(&document) locates the styles-with-effects part when present. Generated schema types include Styles and Style.
Insert a comment into a word processing document
Inserting a comment requires updating the comments part and adding matching comment range markers or references in the main document body.
The upstream sample attaches a comment to the first paragraph. A general Rust API should make the target range explicit and should fail clearly if the selected paragraph or range does not exist.
Comment markup
<w:comment w:id="0" w:author="Ada">
<w:p><w:r><w:t>Review this paragraph</w:t></w:r></w:p>
</w:comment>
The same id must be used in the comment and in the document markers:
<w:commentRangeStart w:id="0"/>
<w:r><w:t>Commented text</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:commentReference w:id="0"/></w:r>
Rust workflow
Read existing comments, allocate the next id, update word/comments.xml, and add markers around the first paragraph:
#![allow(unused)] fn main() { pub fn insert_comment( path: &Path, author: &str, comment_text: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let comments_part = if let Some(part) = main_part.wordprocessing_comments_part(&document) { part } else { main_part.add_new_part_auto_id::<_, WordprocessingCommentsPart>(&mut document)? }; let comments_xml = comments_part .data_as_str(&document)? .map(str::to_string) .unwrap_or_else(empty_comments_xml); let comment_id = next_comment_id(&comments_xml); let updated_comments = append_comment(&comments_xml, comment_id, author, comment_text)?; comments_part.set_data(&mut document, updated_comments.into_bytes())?; let document_xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_document = add_comment_markers_to_first_paragraph(document_xml, comment_id)?; main_part.set_data(&mut document, updated_document.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Allocate a new comment id by scanning existing comments and adding one to the maximum id. If the comments part is absent, create it with a comments root before appending the new comment.
In ooxmlsdk, generated schema types include Comments, Comment, CommentRangeStart, CommentRangeEnd, and CommentReference.
Insert a picture into a word processing document
Pictures require an image part, a relationship from the main document part, and DrawingML markup in the document body.
Package model
The body markup references an image relationship id. The relationship resolves to an image part under the package.
The image bytes are stored outside document.xml. The body contains a run with drawing markup, and that DrawingML references the relationship id for the image part. The graphic object data element can contain application-specific graphic data under a uri, so the picture markup must use the expected DrawingML picture structure for Word to render it.
<w:r>
<w:drawing>
<!-- inline or anchored DrawingML that references r:embed="rId..." -->
</w:drawing>
</w:r>
Rust workflow
Use the main document part as the insertion point. Add an image part, write the image bytes, and insert DrawingML that references the image relationship id.
#![allow(unused)] fn main() { pub fn insert_picture( path: &Path, relationship_id: &str, content_type: &str, image_bytes: &[u8], ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let image_part = main_part.add_image_part_with_id(&mut document, content_type.to_string(), relationship_id)?; image_part.set_data(&mut document, image_bytes.to_vec())?; let document_xml = main_part.data_as_str(&document)?.unwrap_or_default(); let document_xml = ensure_relationship_namespace(document_xml); let picture = picture_paragraph_xml(relationship_id, 990_000, 792_000); let updated_xml = insert_before_section_properties(&document_xml, &picture)?; main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
When inspecting or updating existing picture relationships, preserve the relationship ids that body DrawingML references:
#![allow(unused)] fn main() { pub fn list_image_relationship_ids(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; Ok( main_part .related_parts_of_type::<_, ImagePart>(&document) .map(|related| related.relationship_id().to_string()) .collect(), ) } }
In ooxmlsdk, MainDocumentPart::image_parts(&document) traverses existing image parts, while related_parts_of_type::<_, ImagePart>(&document) keeps the relationship id with each image part.
Insert a table into a word processing document
Inserting a table is a main document edit. The table must be placed in the body before section properties and should contain valid cell content.
Tables are represented by tbl elements. A table includes table-wide properties (tblPr) and can also contain a table grid (tblGrid), rows (tr), cells (tc), optional cell properties (tcPr), and paragraphs inside each cell.
Table markup
<w:tbl>
<w:tblPr>
<w:tblW w:w="5000" w:type="pct"/>
<w:tblBorders>
<w:top w:val="single" w:sz="4"/>
<w:left w:val="single" w:sz="4"/>
<w:bottom w:val="single" w:sz="4"/>
<w:right w:val="single" w:sz="4"/>
</w:tblBorders>
</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="10296"/>
</w:tblGrid>
<w:tr>
<w:tc>
<w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>
<w:p><w:r><w:t>Hello, World!</w:t></w:r></w:p>
</w:tc>
</w:tr>
</w:tbl>
Rust workflow
#![allow(unused)] fn main() { pub fn insert_table(path: &Path, rows: &[&[&str]]) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); let table = table_xml(rows); let updated_xml = insert_before_section_properties(xml, &table)?; main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The sample flow creates table properties, adds border elements, creates a row, creates a cell with width properties and a paragraph/run/text child, then appends the table to the document body. If cloning cells, clone XML structure carefully so relationship-backed content is not duplicated incorrectly.
In ooxmlsdk, generated schema types include Table, TableProperties, TableWidth, TableBorders, TopBorder, LeftBorder, BottomBorder, RightBorder, TableGrid, GridColumn, TableRow, TableCell, TableCellProperties, TableCellWidth, Paragraph, Run, and Text.
Open and add text to a word processing document
Adding text means editing the main document XML, usually by inserting a new paragraph or run under <w:body/>.
The main document part contains the text of the document as WordprocessingML. Opening a package for editing is only the first step; a writer must ensure the document root and body exist before appending content.
Text markup
<w:p>
<w:r><w:t>New text</w:t></w:r>
</w:p>
Rust workflow
#![allow(unused)] fn main() { pub fn add_paragraph_text(path: &Path, text: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); let paragraph = paragraph_xml(text); let updated_xml = insert_before_section_properties(xml, ¶graph)?; main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The listing appends a paragraph before a trailing <w:sectPr/> if the body has section properties. A broader writer should also handle documents with missing or unusual body structure, and can build the paragraph from generated schema types instead of raw XML strings.
Unlike the upstream .NET SDK's AutoSave behavior, this book shows explicit save behavior through document.save(...).
Open a word processing document for read-only access
Use WordprocessingDocument to open a .docx package and inspect the main document part. In ooxmlsdk, opening a package does not modify it; changes are persisted only when you call a save method.
Use read-only access when callers only need to inspect text, metadata, styles, comments, or relationships and the package should remain unchanged.
Open and inspect paragraphs
#![allow(unused)] fn main() { use std::io::Cursor; use std::path::Path; use ooxmlsdk::parts::header_part::HeaderPart; use ooxmlsdk::parts::image_part::ImagePart; use ooxmlsdk::parts::style_definitions_part::StyleDefinitionsPart; use ooxmlsdk::parts::wordprocessing_comments_part::WordprocessingCommentsPart; use ooxmlsdk::parts::wordprocessing_document::WordprocessingDocument; use ooxmlsdk::sdk::{OpenSettings, PackageOpenMode, SdkPart, WordprocessingDocumentType}; use ooxmlsdk::validator::ValidationErrorInfo; pub fn open_word_read_only(path: &Path) -> Result<usize, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); Ok(xml.matches("<w:p").count()) } }
The example uses lazy package opening:
OpenSettings { open_mode: PackageOpenMode::Lazy, ..Default::default() }WordprocessingDocument::new_from_file_with_settingsmain_document_part()data_as_str(&document)
Lazy opening is useful for read-only inspection because it lets you navigate package parts without parsing every root element up front.
The same read-only pattern applies to path-based and stream-based inputs. A valid word processing package has at least a main document part; optional parts such as styles, comments, settings, headers, and footers may be absent and should be handled with Option-style control flow.
Do not call save methods from read-only helpers. If a helper attempts to mutate and save a package that was intended for inspection, treat that as a bug in the helper design.
Open a word processing document from a stream
Some applications receive a .docx as bytes instead of a filesystem path. The package still has the same WordprocessingML structure: main document part, relationships, and optional supporting parts.
Use a stream-based open path when the caller already owns an open byte source, such as an upload, object-store response, or document-processing pipeline. The callee should not close or discard a borrowed stream unless the API explicitly takes ownership.
Open from bytes
Use any reader that implements Read + Seek. For in-memory bytes, wrap the Vec<u8> in std::io::Cursor and open the package from that cursor:
#![allow(unused)] fn main() { pub fn open_word_from_bytes(bytes: Vec<u8>) -> Result<usize, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new(Cursor::new(bytes))?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); Ok(xml.matches("<w:p").count()) } }
If the stream workflow writes back to the document, it must still update parts, relationships, and content types consistently, then persist the package through an explicit output path such as an owned Cursor<Vec<u8>> or file handle.
Remove hidden text from a word processing document
Hidden text is represented by run properties, usually <w:vanish/>, on runs or inherited styles.
vanish is a toggle property. As direct run formatting, values such as on, 1, or true hide the run, and off, 0, or false turn direct hidden formatting off. In styles, toggle behavior interacts with the style hierarchy, so direct markup alone is not always enough to know the effective result.
Hidden text markup
<w:r>
<w:rPr><w:vanish/></w:rPr>
<w:t>Hidden text</w:t>
</w:r>
Rust workflow
Remove runs whose direct run properties contain active vanish formatting:
#![allow(unused)] fn main() { pub fn remove_hidden_text(path: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let document_xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = remove_runs_with_direct_vanish(document_xml); main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The upstream sample removes runs whose run properties contain vanish, and removes extra vanish elements when they are not under a run. That is a useful cleanup pattern for direct formatting, but a complete Rust implementation should also account for hidden text inherited from character or paragraph styles.
In ooxmlsdk, generated schema types include Vanish, RunProperties, Run, ParagraphProperties, and Text.
Remove the headers and footers from a word processing document
Headers and footers are separate parts related from the main document part and referenced from section properties.
Removing headers and footers requires both package and document XML changes. Deleting only the parts leaves orphaned headerReference or footerReference elements; deleting only the references can leave unused parts in the package.
Section references
<w:sectPr>
<w:headerReference w:type="default" r:id="rId5"/>
<w:footerReference w:type="default" r:id="rId6"/>
</w:sectPr>
Rust workflow
Delete the related header and footer parts, then remove the corresponding references from section properties:
#![allow(unused)] fn main() { pub fn remove_headers_and_footers(path: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let header_parts: Vec<_> = main_part.header_parts(&document).collect(); let footer_parts: Vec<_> = main_part.footer_parts(&document).collect(); for header_part in header_parts { main_part.delete_part(&mut document, header_part)?; } for footer_part in footer_parts { main_part.delete_part(&mut document, footer_part)?; } let document_xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = remove_header_footer_references(document_xml); main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
A document can contain multiple sections, and each section can have first-page, even-page, and default header/footer references. Remove all matching HeaderReference and FooterReference elements from every SectionProperties node.
In ooxmlsdk, MainDocumentPart::header_parts(&document) and MainDocumentPart::footer_parts(&document) traverse the related parts. Generated schema types include Header, Footer, HeaderReference, FooterReference, and SectionProperties.
Replace text in a Word document using streaming XML
Streaming replacement is useful for large documents because the main document XML can be processed without loading a full model in memory.
The tradeoff is the same as other large-part workflows: a DOM-style reader is easier to query and edit, but it materializes the part; a streaming reader processes XML forward-only and can use much less memory.
Rust workflow
Open the main document part, stream across text elements, and replace matches inside each w:t value:
#![allow(unused)] fn main() { pub fn replace_text_in_main_document( path: &Path, search: &str, replacement: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let document_xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = replace_text_values(document_xml, search, replacement); main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The upstream streaming pattern reads the main document part and writes updated XML to a separate in-memory stream, then replaces the original part after both reader and writer are closed. This avoids opening the same part for simultaneous read and write streams.
Text in a Word document can be split across multiple w:t elements. Single-word replacement is straightforward; phrase replacement needs a buffering strategy that spans runs while preserving run properties.
Replace the header in a word processing document
Headers are stored in separate header parts and referenced from section properties. Replacing a header can mean editing the existing header part or creating a new part and updating the section reference.
Each headerReference points to a header part by relationship id. The relationship must be internal and use the header relationship type. The reference also has a type attribute that selects which header slot it fills.
Header reference markup
<w:sectPr>
<w:headerReference r:id="rId3" w:type="first"/>
<w:headerReference r:id="rId5" w:type="default"/>
<w:headerReference r:id="rId2" w:type="even"/>
</w:sectPr>
Sections can define first-page, even-page, and default headers. Missing references are inherited or substituted according to section settings such as titlePg and document settings such as even/odd headers.
Rust workflow
Use the main document part to locate or create a header part, write the header XML, and update the default section reference:
#![allow(unused)] fn main() { pub fn replace_header( path: &Path, header_text: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let header_part = if let Some(part) = main_part.header_parts(&document).next() { part } else { main_part.add_new_part_auto_id::<_, HeaderPart>(&mut document)? }; let relationship_id = main_part .get_id_of_part(&document, &header_part) .expect("header relationship id") .to_string(); header_part.set_data(&mut document, header_xml(header_text).into_bytes())?; let document_xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = set_header_reference(document_xml, &relationship_id)?; main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
This example updates the default header slot. Apply the same relationship and section-property pattern for first-page or even-page headers by changing the w:type value and targeting the corresponding header part.
In ooxmlsdk, MainDocumentPart::header_parts(&document) traverses header parts. Generated schema types include Header, HeaderReference, and SectionProperties.
Replace the styles parts in a word processing document
Replacing styles means copying or rewriting word/styles.xml and keeping the main document relationship intact.
Word 2013 and later documents can contain both styles.xml and stylesWithEffects.xml. To round-trip across Word versions, replace both parts when the source and target both contain them.
Rust workflow
Create or reuse the normal styles part, then replace its XML payload:
#![allow(unused)] fn main() { pub fn replace_styles_part( path: &Path, styles_xml: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { if !styles_xml.contains("<w:styles") { return Err("styles XML must contain a w:styles root".into()); } let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let styles_part = if let Some(part) = main_part.style_definitions_part(&document) { part } else { main_part.add_new_part_auto_id::<_, StyleDefinitionsPart>(&mut document)? }; styles_part.set_data(&mut document, styles_xml.as_bytes().to_vec())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The upstream workflow extracts a complete styles part from a source document, then writes that XML into the target styles part. If the requested target part does not exist, decide whether to create it or return an explicit error. Replacing styles can change document appearance immediately because paragraphs, runs, tables, and numbering reference style ids.
In ooxmlsdk, use MainDocumentPart::style_definitions_part(&document) for the normal styles part and MainDocumentPart::styles_with_effects_part(&document) when present.
Retrieve application property values from a word processing document
Application properties are stored in the extended file properties part, usually docProps/app.xml.
These are package-level extended properties, not content in the main document part. They can describe application name, page count, word count, template, company, presentation format, and other producer-maintained metadata.
Read application properties
#![allow(unused)] fn main() { pub fn get_application_properties( path: &Path, ) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let Some(app_part) = document.extended_file_properties_part() else { return Ok(Vec::new()); }; let xml = app_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_known_app_properties(xml)) } }
The example reads a few common extended properties from the package-level part.
Extended properties markup
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<Application>ooxmlsdk-doc</Application>
<Pages>1</Pages>
<Words>4</Words>
</Properties>
Not every document contains every property. Treat this part as optional.
Unlike core properties, individual extended properties can be absent when the producing application did not set them. Check the part and each requested element before reading text.
In ooxmlsdk, WordprocessingDocument::extended_file_properties_part() locates the part when present.
Retrieve comments from a word processing document
Word comments are stored in a comments part related from the main document part. The body XML contains comment references; the comment text lives in word/comments.xml.
Open the package for read-only access when retrieving comments. If the main document part has no comments relationship, the document has no comment entries to return.
Read comments
#![allow(unused)] fn main() { pub fn get_comments(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let Some(comments_part) = main_part.wordprocessing_comments_part(&document) else { return Ok(Vec::new()); }; let xml = comments_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_text_values(xml)) } }
The helper opens the main document part, follows the comments relationship when present, and extracts text from the comments XML.
Comment markup
<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:comment w:id="0" w:author="Ada">
<w:p><w:r><w:t>Review this paragraph</w:t></w:r></w:p>
</w:comment>
</w:comments>
Use the comment id to connect body references with comment entries.
The comments element is the root of the comments part and contains zero or more comment children. A comment can contain block-level WordprocessingML content such as paragraphs. If a comment id is not referenced by a matching commentReference in document content, a consuming application may ignore it; if more than one comment has the same id, only one may be loaded.
In ooxmlsdk, generated schema types include Comments, Comment, and CommentReference.
Set a custom property in a word processing document
Custom properties are stored in docProps/custom.xml, separate from the main document body and extended application properties.
Each custom property stores a name, a property id (pid), a fixed format id (fmtid), and exactly one typed value element from the document property value namespace.
Custom property markup
<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" name="Reviewed" pid="2">
<vt:bool>true</vt:bool>
</property>
Common value element names include vt:lpwstr for strings, vt:filetime for timestamps, integer value elements, floating-point value elements, and vt:bool for booleans.
Rust workflow
Custom properties are written through a package-level CustomFilePropertiesPart. This tested listing creates the part when it is absent, writes one string property, and saves the package:
#![allow(unused)] fn main() { pub fn set_custom_string_property( path: &Path, name: &str, value: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let custom_properties_part = match document.custom_file_properties_part() { Some(part) => part, None => document.add_custom_file_properties_part()?, }; let name = escape_xml_text(name); let value = escape_xml_text(value); let xml = format!( r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"> <property fmtid="{{D5CDD505-2E9C-101B-9397-08002B2CF9AE}}" pid="2" name="{name}"> <vt:lpwstr>{value}</vt:lpwstr> </property> </Properties>"# ); custom_properties_part.set_data(&mut document, xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
Application properties are separate from custom properties and are read through the extended properties part:
#![allow(unused)] fn main() { pub fn get_application_properties( path: &Path, ) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let Some(app_part) = document.extended_file_properties_part() else { return Ok(Vec::new()); }; let xml = app_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_known_app_properties(xml)) } }
The listing is deliberately narrow: it writes a single vt:lpwstr value. A full custom-property updater should preserve unrelated properties, allocate unique pid values, choose the correct value element for each type, and replace an existing property by name without duplicating it.
When updating an existing property, replacing the whole property element is often simpler than mutating the old value because the value element name encodes the property type. After insertion or replacement, keep pid values unique and stable for the saved part.
Set the font for a text run
Run font settings are stored in run properties with <w:rFonts/>.
Run fonts can specify different faces for different character classes: ASCII, high ANSI, complex script, and East Asian text. The effective font depends on the Unicode characters in the run unless overridden by related properties.
Font markup
<w:rPr>
<w:rFonts w:ascii="Courier New" w:hAnsi="Courier New" w:cs="Times New Roman"/>
</w:rPr>
For a mixed English and Arabic run, for example, ASCII characters can use the ASCII font while Arabic characters use the complex-script font.
Rust workflow
#![allow(unused)] fn main() { pub fn set_first_run_font( path: &Path, font_name: &str, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); let updated_xml = set_first_run_fonts(xml, font_name)?; main_part.set_data(&mut document, updated_xml.into_bytes())?; let mut buffer = Cursor::new(Vec::new()); document.save(&mut buffer)?; Ok(buffer.into_inner()) } }
The listing updates the first run. If the selected run has no run properties, it creates rPr and prepends it before run content. If it already has run properties, a complete writer should update or add only the rFonts child so other properties such as bold, italic, color, and style remain intact.
In ooxmlsdk, generated schema types include RunProperties, RunFonts, Run, and Text.
Validate a word processing document
Validation can mean different things: package relationships resolve, required parts exist, XML is well-formed, and the content follows WordprocessingML schema rules. In ooxmlsdk 0.10.2, schema validation is available through the optional validators feature.
The upstream Open XML SDK sample separates two cases: validating a normal document and validating a deliberately corrupted document that contains schema-invalid content. In Rust, keep the same distinction: a valid document should return an empty diagnostics list, and an invalid document should return structured ValidationErrorInfo values that the caller can inspect or print.
Validate the package
Enable the feature in Cargo.toml:
[dependencies]
ooxmlsdk = { version = "0.10.2", features = ["validators"] }
Then open the package and call validate. The package-level validator loads the known root elements for the document and reports schema diagnostics with part context when available.
#![allow(unused)] fn main() { pub fn validate_word_document( path: &Path, ) -> Result<Vec<ValidationErrorInfo>, Box<dyn std::error::Error>> { let mut document = WordprocessingDocument::new_from_file(path)?; Ok(document.validate()?) } }
An empty vector means no validation errors were reported by the generated validators:
#![allow(unused)] fn main() { let errors = validate_word_document(path)?; if errors.is_empty() { println!("document is valid"); } }
Each ValidationErrorInfo includes an error category, a readable description, and optional fields such as id, type_name, field_name, and part_uri. Treat those diagnostics as runtime validation data; do not rely on opening a package alone as a substitute for schema validation.
Working with paragraphs
Paragraphs are stored as <w:p/> elements in the main document body, comments, headers, footers, footnotes, and other WordprocessingML parts.
A paragraph is the basic block-level unit in WordprocessingML. It begins on a new line and can contain optional paragraph properties, inline content such as runs, and optional revision ids used by compare/merge workflows.
Paragraph markup
<w:p>
<w:pPr>
<w:pStyle w:val="Heading1"/>
</w:pPr>
<w:r><w:t>Heading text</w:t></w:r>
</w:p>
Paragraph properties are stored in <w:pPr/>. Text content is usually stored in runs under the paragraph.
Common paragraph properties include alignment, borders, hyphenation override, indentation, line spacing, shading, text direction, and widow/orphan control. A paragraph can exist without visible text, for example as an empty paragraph or as a cell placeholder.
Rust workflow
Use the main document part to read paragraph text:
#![allow(unused)] fn main() { pub fn get_document_text(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_text_values(xml)) } }
If you need paragraph boundaries or properties, parse the main document XML and inspect <w:p/> nodes instead of flattening all text.
In ooxmlsdk, generated schema types include Paragraph, ParagraphProperties, Run, Text, Justification, ParagraphBorders, Indentation, SpacingBetweenLines, and Shading.
Working with runs
Runs are inline text containers inside paragraphs. A run can carry formatting such as bold, italic, color, font, or field-related markup.
A run defines a region of content with a common set of properties. Besides text, runs can contain breaks, tabs, drawings, field-related markup, comments references, and other inline content.
Run markup
<w:r>
<w:rPr>
<w:b/>
</w:rPr>
<w:t>Bold text</w:t>
</w:r>
Run properties are stored in <w:rPr/>; visible text is stored in <w:t/>. Text for a sentence can be split across many runs, especially after editing in Word.
If present, rPr must appear before the run content it formats. Common run properties include bold, border, character style, color, font, font size, italic, kerning, spelling/grammar suppression, shading, small caps, strikethrough, text direction, and underline.
Rust workflow
The text helper extracts <w:t/> values from the main document:
#![allow(unused)] fn main() { pub fn get_document_text(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_text_values(xml)) } }
For formatting-sensitive tasks, preserve run boundaries and update only the target run or run property subtree.
In ooxmlsdk, generated schema types include Run, RunProperties, Text, Bold, Italic, Color, RunFonts, FontSize, Underline, Strike, and SmallCaps.
Working with WordprocessingML tables
Tables are stored in the main document XML as <w:tbl/>. Rows are <w:tr/>, cells are <w:tc/>, and cell content is usually paragraphs.
Tables are block-level content, arranged as rows and columns. A table can contain paragraph content and other block-level content inside cells.
Table markup
<w:tbl>
<w:tblPr>
<w:tblStyle w:val="TableGrid"/>
<w:tblW w:w="5000" w:type="pct"/>
</w:tblPr>
<w:tblGrid>
<w:gridCol/>
<w:gridCol/>
<w:gridCol/>
</w:tblGrid>
<w:tr>
<w:tc>
<w:tcPr/>
<w:p><w:r><w:t>1</w:t></w:r></w:p>
</w:tc>
<w:tc>
<w:tcPr/>
<w:p><w:r><w:t>2</w:t></w:r></w:p>
</w:tc>
<w:tc>
<w:tcPr/>
<w:p><w:r><w:t>3</w:t></w:r></w:p>
</w:tc>
</w:tr>
</w:tbl>
w:tblPr is required for a w:tbl in the generated schema and defines table-wide properties, such as style and width. w:tblGrid is optional and defines grid layout through w:gridCol children. Each w:tr can have row properties, and each w:tc can optionally include w:tcPr for cell-specific settings such as width, borders, margins, and vertical alignment.
Rust workflow
The document text helper includes text found inside table cells:
#![allow(unused)] fn main() { pub fn get_document_text(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> { let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?; let main_part = document.main_document_part()?; let xml = main_part.data_as_str(&document)?.unwrap_or_default(); Ok(extract_text_values(xml)) } }
For table-specific edits, parse <w:tbl/>, <w:tr/>, and <w:tc/> boundaries so updates do not affect unrelated body paragraphs.
In ooxmlsdk, generated schema types include Table, TableProperties, TableGrid, GridColumn, TableRow, TableRowProperties, TableCell, TableCellProperties, TableStyle, TableWidth, and TableBorders.