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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 71 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,116 +26,119 @@ It also supports OpenDocument Presentation (`.odp`).
- 🪄 **Embedding:** Used to provide pptx content and meta-information in a form that is useful for embeddings
---

## 👨‍💻 Example Usage
## 📌 Quickstart

`PresentationContainer` is the recommended entry point for new code. It detects whether the input is a PowerPoint (`.pptx`) or OpenDocument Presentation (`.odp`) file and exposes the same parsing API for both formats.
`PresentationContainer` is the preferred entry point for all new code. It
detects PPTX and ODP automatically. The default configuration produces complete
Markdown with presentation metadata, slide markers, spatial reading order, and
embedded images.

```rust
use pptx_to_md::{ImageHandlingMode, ParserConfig, PresentationContainer};
use pptx_to_md::{ParserConfig, PresentationContainer, Result};
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ParserConfig::builder()
.include_presentation_metadata(true)
.compress_images(true)
.quality(80)
.image_handling_mode(ImageHandlingMode::InMarkdown)
.include_slide_number_as_comment(true)
.include_speaker_notes(false)
.include_comments(false)
.build();

let mut container = PresentationContainer::open(
Path::new("path/to/presentation.pptx"), // or .odp
config,
fn main() -> Result<()> {
let mut presentation = PresentationContainer::open(
Path::new("presentation.pptx"), // also accepts .odp
ParserConfig::default(),
)?;

let markdown = container.convert_to_md()?;
let markdown = presentation.convert_to_md()?;
std::fs::write("output.md", markdown)?;

Ok(())
}
```

`PresentationContainer::convert_to_md()` is the preferred API for converting
a complete PPTX or ODP document. It emits presentation metadata once at the
start and then appends all slide Markdown in order. The existing `parse_all()`,
streaming, and per-slide APIs remain available for structured processing.
See [`examples/basic_usage.rs`](examples/basic_usage.rs) for the corresponding
command-line example.

### Semantic document model
## 🎯 Choosing the right API

`PresentationContainer::parse_document()` returns a **semantic** `Presentation`
containing metadata, slides, and non-fatal parser diagnostics. Each slide exposes
ordered `blocks` with bounds and source order. Text blocks preserve title/body
roles, individual paragraphs, list metadata, run formatting, and hyperlinks;
tables preserve merged-cell information and images retain alternative text.
Start with `PresentationContainer` and select the operation based on what the
application needs:

For custom Markdown behavior, call `Slide::to_markdown(&MarkdownOptions)`.
`ReadingOrder::Spatial` is the default and groups content by visual columns;
`ReadingOrder::Source` preserves the package's source order. Per-slide
`convert_to_md()` now returns `Result<String>` so image and filesystem failures
cannot be discarded silently.
| Goal | Preferred API | What it does |
| --- | --- | --- |
| Convert one complete presentation | `convert_to_md()` | Parses every slide and returns one Markdown document, including optional presentation metadata |
| Convert a large PPTX faster | `convert_to_md_multi_threaded()` | Uses parallel slide parsing for PPTX; ODP transparently uses its normal document parser |
| Inspect or transform structured content | `parse_document()` | Returns metadata, semantic slides and blocks, and aggregated diagnostics |
| Work with all slides directly | `parse_all()` | Returns `Vec<Slide>` without creating presentation-level Markdown |
| Process one slide at a time | `iter_slides()` | Streams `Result<Slide>` values and avoids retaining every parsed slide |
| Customize one slide's Markdown | `Slide::to_markdown(&MarkdownOptions)` | Controls reading order, slide marker, notes, comments, and unsupported-content comments |
| Read only document properties | `metadata()` | Returns parsed presentation metadata without parsing the slides |

See [Semantic presentation-to-Markdown conversion](docs/SEMANTIC_MARKDOWN_CONVERSION.md)
for the architecture, migration notes, test coverage, and known limitations of
the pull-parser and semantic-model changes.
`ParserConfig` controls parsing, image handling, and the defaults used by
`convert_to_md()`. `MarkdownOptions` is only needed when rendering an individual
parsed slide differently.

To access metadata as structured Rust values without converting slides, see
[`examples/presentation_metadata.rs`](https://github.com/nilskruthoff/pptx-parser/tree/master/examples/presentation_metadata.rs).
Important behavior differences:

For PPTX-only code, `PptxContainer` remains available for backwards compatibility and for callers that explicitly want the old PowerPoint-only entry point. New code that may handle both formats should prefer `PresentationContainer`.
For more usage examples, refer to the [examples](https://github.com/nilskruthoff/pptx-parser/tree/master/examples) directory.
- Presentation-level `convert_to_md*()` methods parse and render the slides and
emit presentation metadata once. Per-slide methods never emit presentation
metadata.
- `parse_document()` and `parse_all*()` retain all returned slides. The former
additionally packages metadata and aggregates slide diagnostics.
- `Slide::convert_to_md()` uses the rendering flags copied from `ParserConfig`.
`Slide::to_markdown()` accepts explicit `MarkdownOptions` for that one call;
image loading and image output mode still come from the slide's
`ParserConfig`.
- `PresentationContainer::open()` performs format detection. Use `format()` to
inspect the result. `open_as()` is only necessary when the caller explicitly
wants to force `PresentationFormat::Pptx` or `PresentationFormat::Odp`.

### PPTX and ODP auto-detection
### Structured content and custom Markdown

Use `PresentationContainer` when the input may be either `.pptx` or `.odp`:
Use `parse_document()` when Markdown is not the only desired output or parser
diagnostics need to be inspected:

```rust
use pptx_to_md::{ParserConfig, PresentationContainer, PresentationFormat};
use pptx_to_md::{MarkdownOptions, ParserConfig, PresentationContainer, ReadingOrder, Result};
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
fn main() -> Result<()> {
let mut presentation = PresentationContainer::open(
Path::new("path/to/presentation.odp"),
Path::new("presentation.odp"),
ParserConfig::default(),
)?;

assert_eq!(presentation.format(), PresentationFormat::Odp);
let slides = presentation.parse_all()?;
let document = presentation.parse_document()?;

for diagnostic in &document.diagnostics {
eprintln!("{:?}: {}", diagnostic.severity, diagnostic.message);
}

let options = MarkdownOptions {
reading_order: ReadingOrder::Source,
include_speaker_notes: true,
..MarkdownOptions::default()
};
let first_slide_markdown = document.slides[0].to_markdown(&options)?;
println!("{first_slide_markdown}");
Ok(())
}
```

If the format is already known, use `open_as` to skip auto-detection:
`ReadingOrder::Spatial` is the default and orders visual columns heuristically.
`ReadingOrder::Source` preserves the order in the source XML.

```rust
use pptx_to_md::{ParserConfig, PresentationContainer, PresentationFormat};
use std::path::Path;
### Streaming and parallel parsing

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut presentation = PresentationContainer::open_as(
Path::new("path/to/presentation.pptx"),
ParserConfig::default(),
PresentationFormat::Pptx,
)?;

let slides = presentation.parse_all()?;
Ok(())
}
```
Use `iter_slides()` for bounded-memory processing. Each slide is parsed when the
iterator advances. Use `parse_all_multi_threaded()` or
`convert_to_md_multi_threaded()` for CPU-parallel PPTX parsing. ODP stores its
pages in one `content.xml`, so its implementation remains sequential.

---

## Config Parameters
## ⚙️ Config Parameters

| Parameter | Type | Default | Description |
|--------------------------|-----------------------|---------------|-----------------------------------------------------------------------------------------------------------|
| `extract_images` | `bool` | `true` | Whether images are extracted from slides or not. If false, images can not be extracted manually either. |
| `compress_images` | `bool` | `true` | Whether images are compressed before encoding or not. Effects manually extracted images too. |
| `image_quality` | `u8` | `80` | Defines the image compression quality `(0-100)`. Higher values mean better quality but larger file sizes. |
| `quality` | `u8` | `80` | Defines the image compression quality `(0-100)`. Higher values mean better quality but larger file sizes. |
| `image_handling_mode` | `ImageHandlingMode` | `InMarkdown` | Determines how images are handled during content export |
| `image_output_path` | `Option<PathBuf>` | `None` | Output directory path for `ImageHandlingMode::Save` (mandatory for saving mode) |
| `include_slide_number_as_comment` | `bool` | `true` | Weather the slide number comment is included or not (`<!-- Slide [n] -->`) |
| `include_slide_number_as_comment` | `bool` | `true` | Whether the slide number comment is included (`<!-- Slide [n] -->`) |
| `include_speaker_notes` | `bool` | `false` | Whether speaker notes are appended to Markdown as blockquotes |
| `include_comments` | `bool` | `false` | Whether presentation comments are appended to Markdown as blockquotes |
| `include_presentation_metadata` | `bool` | `true` | Whether complete-presentation Markdown starts with a metadata HTML comment |
Expand All @@ -146,7 +149,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|---------------|---------------------------------------------------------------------------------------------------------------------------------|
| `InMarkdown` | Images are embedded directly in the Markdown output using standard syntax as `base64` data (`![]()`) |
| `Manually` | Image handling is delegated to the user, requiring manual copying or referencing (as `base64`) |
| `Save` | Images will be saved in a provided output directory and integrated using `<a>` tag syntax (`<a href="file:///<abs_path>"></a>`) |
| `Save` | Images are saved in the configured output directory and referenced with Markdown image syntax and a `file://` URL |

---

Expand Down
35 changes: 35 additions & 0 deletions examples/legacy_pptx_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Legacy PPTX-only API retained for existing integrations.
//!
//! New code should use `PresentationContainer`; see `basic_usage.rs`.
//!
//! Run with:
//! cargo run --example legacy_pptx_api <presentation.pptx> [output.md]

use pptx_to_md::{ParserConfig, PptxContainer, Result};
use std::env;
use std::fs;
use std::path::Path;

fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
let Some(input_path) = args.get(1) else {
eprintln!("Usage: cargo run --example legacy_pptx_api <presentation.pptx> [output.md]");
return Ok(());
};
let output_path = args.get(2).map(String::as_str).unwrap_or("output.md");

// This is the pre-PresentationContainer flow: open a PPTX-specific
// container, parse every slide, and render each slide separately. It does
// not add the presentation-level metadata header.
let mut container = PptxContainer::open(Path::new(input_path), ParserConfig::default())?;
let markdown = container
.parse_all()?
.into_iter()
.map(|slide| slide.convert_to_md())
.collect::<Result<Vec<_>>>()?
.join("\n");
fs::write(output_path, markdown)?;

println!("Converted PPTX with the legacy entry point to {output_path}");
Ok(())
}
31 changes: 13 additions & 18 deletions examples/manual_image_extraction.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
//! Basic usage example for the pptx-to-md crate
//! Manually extract images while parsing either a PPTX or ODP presentation.
//!
//! This example demonstrates how to open a PPTX file and convert all slides to Markdown.
//!
//! Run with: cargo run --example manual_image_extraction <path/to/your/presentation.pptx>
//! Run with:
//! cargo run --example manual_image_extraction <presentation.pptx|presentation.odp>

use base64::Engine;
use base64::engine::general_purpose;
use pptx_to_md::{ImageHandlingMode, ParserConfig, PptxContainer, Result};
use pptx_to_md::{ImageHandlingMode, ParserConfig, PresentationContainer, Result};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::{env, fs};

fn main() -> Result<()> {
// Get the PPTX file path from command line arguments
let args: Vec<String> = env::args().collect();
let pptx_path = if args.len() > 1 {
&args[1]
} else {
eprintln!("Usage: cargo run --example manual_image_extraction <path/to/presentation.pptx>");
let Some(input_path) = args.get(1) else {
eprintln!(
"Usage: cargo run --example manual_image_extraction <presentation.pptx|presentation.odp>"
);
return Ok(());
};

println!("Processing PPTX file: {}", pptx_path);
println!("Processing presentation: {input_path}");

// Use the config builder to build your config
let config = ParserConfig::builder()
Expand All @@ -32,8 +30,7 @@ fn main() -> Result<()> {
.image_handling_mode(ImageHandlingMode::Manually)
.build();

// Open the PPTX file
let mut container = PptxContainer::open(Path::new(pptx_path), config)?;
let mut container = PresentationContainer::open(Path::new(input_path), config)?;

// Parse all slides
let slides = container.parse_all()?;
Expand All @@ -47,20 +44,19 @@ fn main() -> Result<()> {
let output_dir = "extracted_images";
fs::create_dir_all(output_dir)?;

// Process slides one by one using the iterator
let mut image_count = 1;

// Convert each slide to Markdown and save
for slide in slides {
writeln!(md_file, "{}", slide.convert_to_md()?).expect("Couldn't write to file");
writeln!(md_file, "{}", slide.convert_to_md()?)?;

// Manually load the base64 encoded image strings from the slide
if let Some(images) = slide.load_images_manually() {
for image in images {
// Decode the base64 strings back to raw image data
let image_data = general_purpose::STANDARD
.decode(image.base64_content.clone())
.unwrap();
.expect("parser returned invalid base64 image data");

// Extract image extension if the image is not compressed, otherwise its always `.jpg`
let ext = if slide.config.compress_images {
Expand All @@ -85,8 +81,7 @@ fn main() -> Result<()> {
md_file,
"![{}](data:image/{};base64,{})",
file_name, ext, image.base64_content
)
.expect("Couldn't write to file");
)?;

image_count += 1;
}
Expand Down
59 changes: 18 additions & 41 deletions examples/memory_efficient_streaming.rs
Original file line number Diff line number Diff line change
@@ -1,61 +1,38 @@
//! Memory-efficient streaming example for the pptx-to-md crate
//! Process one PPTX or ODP slide at a time instead of retaining all slides.
//!
//! This example demonstrates how to use the streaming API to process large
//! PPTX files with minimal memory usage.
//!
//! Run with: cargo run --example memory_efficient_streaming <path/to/your/presentation.pptx>
//! Run with:
//! cargo run --example memory_efficient_streaming <presentation.pptx|presentation.odp>

use pptx_to_md::{ParserConfig, PptxContainer, Result};
use pptx_to_md::{ParserConfig, PresentationContainer, Result};
use std::env;
use std::fs;
use std::path::Path;

fn main() -> Result<()> {
// Get the PPTX file path from command line arguments
let args: Vec<String> = env::args().collect();
let pptx_path = if args.len() > 1 {
&args[1]
} else {
let Some(input_path) = args.get(1) else {
eprintln!(
"Usage: cargo run --example memory_efficient_streaming <path/to/presentation.pptx>"
"Usage: cargo run --example memory_efficient_streaming <presentation.pptx|presentation.odp>"
);
return Ok(());
};

println!("Processing PPTX file: {}", pptx_path);

// Use the config builder to build your config
let config = ParserConfig::builder().extract_images(true).build();

// Open the PPTX file with the streaming API
let mut streamer = PptxContainer::open(Path::new(pptx_path), config)?;

// Create output directory
let mut presentation =
PresentationContainer::open(Path::new(input_path), ParserConfig::default())?;
let output_dir = "output_streaming";
fs::create_dir_all(output_dir)?;

// Process slides one by one using the iterator
for slide_result in streamer.iter_slides() {
match slide_result {
Ok(slide) => {
println!(
"Processing slide {} ({} elements)",
slide.slide_number,
slide.elements.len()
);

let md_content = slide.convert_to_md()?;
let output_path = format!("{}/slide_{}.md", output_dir, slide.slide_number);
fs::write(&output_path, md_content)?;
println!("Saved slide {} to {}", slide.slide_number, output_path);
}
Err(e) => {
eprintln!("Error processing slide: {:?}", e);
}
}
// Unlike parse_document(), the iterator only retains the current slide.
for slide_result in presentation.iter_slides() {
let slide = slide_result?;
let output_path = format!("{output_dir}/slide_{}.md", slide.slide_number);
fs::write(&output_path, slide.convert_to_md()?)?;
println!(
"Saved slide {} ({} semantic blocks) to {output_path}",
slide.slide_number,
slide.blocks.len()
);
}

println!("All slides processed successfully!");

Ok(())
}
Loading
Loading