#![allow(clippy::print_stdout, clippy::print_stderr, clippy::dbg_macro)] #![cfg(feature = "office")]
mod helpers;
use helpers::extract_uri_document;
use std::io::Write;
use tempfile::NamedTempFile;
use xberg::{ExtractionConfig, ImageExtractionConfig};
use zip::CompressionMethod;
use zip::write::{FileOptions, ZipWriter};
#[tokio::test]
async fn test_ppsx_slideshow_extraction() {
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Operation failed")
.parent()
.expect("Operation failed");
let test_file = workspace_root.join("test_documents/pptx/sample.ppsx");
if !test_file.exists() {
println!("Skipping test: PPSX test file not found at {:?}", test_file);
return;
}
let result = extract_uri_document(&test_file, None, &ExtractionConfig::default()).await;
match result {
Ok(extraction) => {
assert!(!extraction.content.is_empty(), "PPSX content should not be empty");
println!("✅ PPSX extraction succeeded!");
println!(" Content length: {} chars", extraction.content.len());
println!(
" Content preview: {}",
&extraction.content[..extraction.content.len().min(200)]
);
}
Err(e) => {
panic!(
"PPSX extraction failed with error: {:?}\n\
This is GitHub Issue #321 Bug 2: PPSX files should be supported.\n\
PPSX MIME type (application/vnd.openxmlformats-officedocument.presentationml.slideshow) \
needs to be added to extension-to-MIME mapping.",
e
);
}
}
}
#[tokio::test]
async fn test_ppsx_with_explicit_mime_type() {
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Operation failed")
.parent()
.expect("Operation failed");
let test_file = workspace_root.join("test_documents/pptx/sample.ppsx");
if !test_file.exists() {
println!("Skipping test: PPSX test file not found at {:?}", test_file);
return;
}
let result = extract_uri_document(
&test_file,
Some("application/vnd.openxmlformats-officedocument.presentationml.slideshow"),
&ExtractionConfig::default(),
)
.await;
match result {
Ok(extraction) => {
assert!(!extraction.content.is_empty(), "PPSX content should not be empty");
println!("✅ PPSX extraction with explicit MIME type succeeded!");
}
Err(e) => {
panic!(
"PPSX extraction with explicit MIME type failed: {:?}\n\
The PPTX extractor should handle PPSX content (identical XML structure).",
e
);
}
}
}
#[tokio::test]
async fn test_pptx_with_image_placeholder_no_txbody() {
let mut temp_file = NamedTempFile::with_suffix(".pptx").expect("Failed to create temp file");
{
let mut zip = ZipWriter::new(&mut temp_file);
let options: FileOptions<()> = FileOptions::default().compression_method(CompressionMethod::Stored);
zip.start_file("[Content_Types].xml", options)
.expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
</Types>"#).expect("Operation failed");
zip.start_file("_rels/.rels", options).expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<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>"#).expect("Operation failed");
zip.start_file("ppt/presentation.xml", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<p:sldIdLst>
<p:sldId id="256" r:id="rId2"/>
</p:sldIdLst>
</p:presentation>"#,
)
.expect("Operation failed");
zip.start_file("ppt/_rels/presentation.xml.rels", options)
.expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
</Relationships>"#).expect("Operation failed");
zip.start_file("ppt/slides/slide1.xml", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr>
<p:cNvPr id="1" name=""/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="0" cy="0"/>
<a:chOff x="0" y="0"/>
<a:chExt cx="0" cy="0"/>
</a:xfrm>
</p:grpSpPr>
<!-- Normal text shape WITH txBody - this should be extracted -->
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Title"/>
<p:cNvSpPr/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="100000" cy="100000"/>
</a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
</p:spPr>
<p:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p>
<a:r>
<a:rPr lang="en-US"/>
<a:t>This is the title text</a:t>
</a:r>
</a:p>
</p:txBody>
</p:sp>
<!-- IMAGE PLACEHOLDER shape WITHOUT txBody - this caused the "No txBody found" error -->
<!-- This is a valid PPTX structure - image placeholders don't contain text -->
<p:sp>
<p:nvSpPr>
<p:cNvPr id="99" name="Image Placeholder"/>
<p:cNvSpPr>
<a:spLocks noGrp="1"/>
</p:cNvSpPr>
<p:nvPr>
<p:ph type="pic" idx="1"/>
</p:nvPr>
</p:nvSpPr>
<p:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="100000" cy="100000"/>
</a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
</p:spPr>
<!-- NOTE: No <p:txBody> here - this is valid for image placeholders -->
</p:sp>
<!-- Another normal text shape - should also be extracted -->
<p:sp>
<p:nvSpPr>
<p:cNvPr id="3" name="Content"/>
<p:cNvSpPr/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr>
<a:xfrm>
<a:off x="0" y="200000"/>
<a:ext cx="100000" cy="100000"/>
</a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
</p:spPr>
<p:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p>
<a:r>
<a:rPr lang="en-US"/>
<a:t>Content after image placeholder</a:t>
</a:r>
</a:p>
</p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
</p:sld>"#,
)
.expect("Operation failed");
zip.start_file("ppt/slides/_rels/slide1.xml.rels", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>"#,
)
.expect("Operation failed");
zip.finish().expect("Operation failed");
}
let result = extract_uri_document(
temp_file.path(),
Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
&ExtractionConfig::default(),
)
.await;
match result {
Ok(extraction) => {
assert!(!extraction.content.is_empty(), "Content should not be empty");
assert!(
extraction.content.contains("title text"),
"Should extract text from first shape with txBody. Got: {}",
extraction.content
);
assert!(
extraction.content.contains("Content after"),
"Should extract text from shape after image placeholder. Got: {}",
extraction.content
);
println!("✅ PPTX with image placeholder (no txBody) extraction succeeded!");
println!(" Content: {}", extraction.content);
}
Err(e) => {
let error_msg = format!("{:?}", e);
if error_msg.contains("No txBody found") {
panic!(
"PPTX extraction failed with 'No txBody found' error!\n\
This is GitHub Issue #321 Bug 1.\n\
The parser should skip shapes without txBody (image placeholders) \
instead of failing.\n\
Error: {:?}",
e
);
} else {
panic!("PPTX extraction failed with unexpected error: {:?}", e);
}
}
}
}
#[tokio::test]
async fn test_pptx_mixed_shapes_extraction() {
let mut temp_file = NamedTempFile::with_suffix(".pptx").expect("Failed to create temp file");
{
let mut zip = ZipWriter::new(&mut temp_file);
let options: FileOptions<()> = FileOptions::default().compression_method(CompressionMethod::Stored);
zip.start_file("[Content_Types].xml", options)
.expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
</Types>"#).expect("Operation failed");
zip.start_file("_rels/.rels", options).expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<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>"#).expect("Operation failed");
zip.start_file("ppt/presentation.xml", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<p:sldIdLst>
<p:sldId id="256" r:id="rId2"/>
</p:sldIdLst>
</p:presentation>"#,
)
.expect("Operation failed");
zip.start_file("ppt/_rels/presentation.xml.rels", options)
.expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
</Relationships>"#).expect("Operation failed");
zip.start_file("ppt/slides/slide1.xml", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr>
<p:cNvPr id="1" name=""/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr/>
<!-- Shape 1: Normal text -->
<p:sp>
<p:nvSpPr><p:cNvPr id="2" name="Title"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
<p:spPr/>
<p:txBody>
<a:bodyPr/><a:lstStyle/>
<a:p><a:r><a:t>First Text Shape</a:t></a:r></a:p>
</p:txBody>
</p:sp>
<!-- Shape 2: Image placeholder (NO txBody) -->
<p:sp>
<p:nvSpPr>
<p:cNvPr id="10" name="Picture Placeholder"/>
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
<p:nvPr><p:ph type="pic"/></p:nvPr>
</p:nvSpPr>
<p:spPr/>
</p:sp>
<!-- Shape 3: Another text shape -->
<p:sp>
<p:nvSpPr><p:cNvPr id="3" name="Body"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
<p:spPr/>
<p:txBody>
<a:bodyPr/><a:lstStyle/>
<a:p><a:r><a:t>Second Text Shape</a:t></a:r></a:p>
</p:txBody>
</p:sp>
<!-- Shape 4: Chart placeholder (NO txBody) -->
<p:sp>
<p:nvSpPr>
<p:cNvPr id="11" name="Chart Placeholder"/>
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
<p:nvPr><p:ph type="chart"/></p:nvPr>
</p:nvSpPr>
<p:spPr/>
</p:sp>
<!-- Shape 5: Content placeholder (NO txBody - empty) -->
<p:sp>
<p:nvSpPr>
<p:cNvPr id="12" name="Content Placeholder"/>
<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
<p:nvPr><p:ph type="body"/></p:nvPr>
</p:nvSpPr>
<p:spPr/>
</p:sp>
<!-- Shape 6: Final text shape -->
<p:sp>
<p:nvSpPr><p:cNvPr id="4" name="Footer"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
<p:spPr/>
<p:txBody>
<a:bodyPr/><a:lstStyle/>
<a:p><a:r><a:t>Third Text Shape</a:t></a:r></a:p>
</p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
</p:sld>"#,
)
.expect("Operation failed");
zip.start_file("ppt/slides/_rels/slide1.xml.rels", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>"#,
)
.expect("Operation failed");
zip.finish().expect("Operation failed");
}
let result = extract_uri_document(
temp_file.path(),
Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
&ExtractionConfig::default(),
)
.await;
match result {
Ok(extraction) => {
assert!(
extraction.content.contains("First Text Shape"),
"Should extract first text shape"
);
assert!(
extraction.content.contains("Second Text Shape"),
"Should extract second text shape (after image placeholder)"
);
assert!(
extraction.content.contains("Third Text Shape"),
"Should extract third text shape (after multiple placeholders)"
);
println!("✅ PPTX mixed shapes extraction succeeded!");
println!(" All text shapes extracted despite image/chart/content placeholders without txBody");
}
Err(e) => {
panic!(
"PPTX extraction failed: {:?}\n\
Shapes without txBody should be skipped gracefully.",
e
);
}
}
}
#[tokio::test]
async fn test_pptx_image_page_numbers_not_reversed() {
let mut temp_file = NamedTempFile::with_suffix(".pptx").expect("Failed to create temp file");
let png_image: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x18,
0xDD, 0x8D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
];
{
let mut zip = ZipWriter::new(&mut temp_file);
let options: FileOptions<()> = FileOptions::default().compression_method(CompressionMethod::Stored);
zip.start_file("[Content_Types].xml", options)
.expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Default Extension="png" ContentType="image/png"/>
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
<Override PartName="/ppt/slides/slide2.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
</Types>"#).expect("Operation failed");
zip.start_file("_rels/.rels", options).expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<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>"#).expect("Operation failed");
zip.start_file("ppt/presentation.xml", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<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="rId2"/>
<p:sldId id="257" r:id="rId3"/>
</p:sldIdLst>
</p:presentation>"#,
)
.expect("Operation failed");
zip.start_file("ppt/_rels/presentation.xml.rels", options)
.expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide2.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
</Relationships>"#).expect("Operation failed");
zip.start_file("ppt/media/image1.png", options)
.expect("Operation failed");
zip.write_all(png_image).expect("Operation failed");
zip.start_file("ppt/slides/slide1.xml", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<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"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
<p:spPr/>
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>Slide 1 - Has Image</a:t></a:r></a:p></p:txBody>
</p:sp>
<p:pic>
<p:nvPicPr>
<p:cNvPr id="3" name="Picture 1"/>
<p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr>
<p:nvPr/>
</p:nvPicPr>
<p:blipFill>
<a:blip r:embed="rId2"/>
<a:stretch><a:fillRect/></a:stretch>
</p:blipFill>
<p:spPr>
<a:xfrm><a:off x="0" y="0"/><a:ext cx="100000" cy="100000"/></a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
</p:spPr>
</p:pic>
</p:spTree>
</p:cSld>
</p:sld>"#,
)
.expect("Operation failed");
zip.start_file("ppt/slides/_rels/slide1.xml.rels", options)
.expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image1.png"/>
</Relationships>"#).expect("Operation failed");
zip.start_file("ppt/slides/slide2.xml", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<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"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
<p:spPr/>
<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>Slide 2 - No Image</a:t></a:r></a:p></p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
</p:sld>"#,
)
.expect("Operation failed");
zip.start_file("ppt/slides/_rels/slide2.xml.rels", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>"#,
)
.expect("Operation failed");
zip.finish().expect("Operation failed");
}
let config = ExtractionConfig {
images: Some(ImageExtractionConfig {
extract_images: true,
..Default::default()
}),
..Default::default()
};
let result = extract_uri_document(
temp_file.path(),
Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
&config,
)
.await;
match result {
Ok(extraction) => {
assert!(extraction.content.contains("Slide 1"), "Should extract slide 1 text");
assert!(extraction.content.contains("Slide 2"), "Should extract slide 2 text");
let images = extraction.images.as_ref().expect("Images should be present");
assert!(!images.is_empty(), "Should extract at least one image");
let image = &images[0];
assert_eq!(
image.page_number,
Some(1),
"GitHub Issue #329: Image on slide 1 should have page_number=1, but got {:?}. \
The page numbers are reversed!",
image.page_number
);
println!("✅ PPTX image page numbers are correct!");
println!(" Image on slide 1 has page_number={:?}", image.page_number);
}
Err(e) => {
panic!("PPTX extraction failed: {:?}", e);
}
}
}
#[tokio::test]
async fn test_pptx_image_page_numbers_issue329_user_file() {
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Operation failed")
.parent()
.expect("Operation failed");
let test_file = workspace_root.join("test_documents/pptx/pptx_reversed_slide_order_issue329.pptx");
if !test_file.exists() {
println!("Skipping test: User file not found at {:?}", test_file);
return;
}
let config = ExtractionConfig {
images: Some(ImageExtractionConfig {
extract_images: true,
..Default::default()
}),
..Default::default()
};
let result = extract_uri_document(&test_file, None, &config).await;
match result {
Ok(extraction) => {
let images = extraction.images.as_ref().expect("Images should be extracted");
if images.is_empty() {
println!("No images extracted from user file (may not have embedded images)");
return;
}
for (idx, image) in images.iter().enumerate() {
assert_eq!(
image.page_number,
Some(1),
"GitHub Issue #329: Image {} should have page_number=1, but got {:?}",
idx,
image.page_number
);
}
println!("✅ User file from Issue #329 - image page numbers correct!");
println!(" Found {} images, all with page_number=1", images.len());
}
Err(e) => {
panic!("Failed to extract user file: {:?}", e);
}
}
}
#[tokio::test]
async fn test_pptx_broken_image_blip_missing_embed_skipped_gracefully() {
let mut temp_file = NamedTempFile::with_suffix(".pptx").expect("Failed to create temp file");
{
let mut zip = ZipWriter::new(&mut temp_file);
let options: FileOptions<()> = FileOptions::default().compression_method(CompressionMethod::Stored);
zip.start_file("[Content_Types].xml", options)
.expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
</Types>"#).expect("Operation failed");
zip.start_file("_rels/.rels", options).expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<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>"#).expect("Operation failed");
zip.start_file("ppt/presentation.xml", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<p:sldIdLst>
<p:sldId id="256" r:id="rId2"/>
</p:sldIdLst>
</p:presentation>"#,
)
.expect("Operation failed");
zip.start_file("ppt/_rels/presentation.xml.rels", options)
.expect("Operation failed");
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
</Relationships>"#).expect("Operation failed");
zip.start_file("ppt/slides/slide1.xml", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr>
<p:cNvPr id="1" name=""/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="0" cy="0"/>
<a:chOff x="0" y="0"/>
<a:chExt cx="0" cy="0"/>
</a:xfrm>
</p:grpSpPr>
<!-- Normal text shape BEFORE the broken image - must be preserved -->
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Title"/>
<p:cNvSpPr/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="100000" cy="100000"/>
</a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
</p:spPr>
<p:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p>
<a:r>
<a:rPr lang="en-US"/>
<a:t>Text before broken image</a:t>
</a:r>
</a:p>
</p:txBody>
</p:sp>
<!-- BROKEN IMAGE: <a:blip> has NO r:embed attribute -->
<!-- Before the fix, this would abort the entire page with:
"Image embed attribute not found" -->
<p:pic>
<p:nvPicPr>
<p:cNvPr id="4" name="Broken Image"/>
<p:cNvPicPr>
<a:picLocks noChangeAspect="1"/>
</p:cNvPicPr>
<p:nvPr/>
</p:nvPicPr>
<p:blipFill>
<a:blip/>
<a:stretch>
<a:fillRect/>
</a:stretch>
</p:blipFill>
<p:spPr>
<a:xfrm>
<a:off x="0" y="200000"/>
<a:ext cx="100000" cy="100000"/>
</a:xfrm>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
</p:spPr>
</p:pic>
<!-- Normal text shape AFTER the broken image - must be preserved -->
<p:sp>
<p:nvSpPr>
<p:cNvPr id="3" name="Content"/>
<p:cNvSpPr/>
<p:nvPr/>
</p:nvSpPr>
<p:spPr>
<a:xfrm>
<a:off x="0" y="400000"/>
<a:ext cx="100000" cy="100000"/>
</a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
</p:spPr>
<p:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p>
<a:r>
<a:rPr lang="en-US"/>
<a:t>Text after broken image</a:t>
</a:r>
</a:p>
</p:txBody>
</p:sp>
</p:spTree>
</p:cSld>
</p:sld>"#,
)
.expect("Operation failed");
zip.start_file("ppt/slides/_rels/slide1.xml.rels", options)
.expect("Operation failed");
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>"#,
)
.expect("Operation failed");
zip.finish().expect("Operation failed");
}
let result = extract_uri_document(
temp_file.path(),
Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
&ExtractionConfig::default(),
)
.await;
match result {
Ok(extraction) => {
assert!(!extraction.content.is_empty(), "Content should not be empty");
assert!(
extraction.content.contains("Text before broken image"),
"Should preserve text before broken image. Got: {}",
extraction.content
);
assert!(
extraction.content.contains("Text after broken image"),
"Should preserve text after broken image. Got: {}",
extraction.content
);
println!("✅ PPTX with broken image (blip missing r:embed) extraction succeeded!");
println!(" Content: {}", extraction.content);
}
Err(e) => {
let error_msg = format!("{:?}", e);
if error_msg.contains("Image embed attribute not found") {
panic!(
"PPTX extraction failed with 'Image embed attribute not found' error!\n\
This is the regression for PR #1016.\n\
The parser should skip <pic> elements whose <a:blip> lacks r:embed\n\
instead of failing the entire page.\n\
Error: {:?}",
e
);
} else {
panic!("PPTX extraction failed with unexpected error: {:?}", e);
}
}
}
}