use docparser_core::error::{DocError, Result};
use docparser_core::plugin::{ParseCapability, RawDoc, RawPage};
use quick_xml::events::Event;
use quick_xml::Reader;
use crate::parse_rels;
use std::collections::HashMap;
pub struct PptxRawBackend;
impl ParseCapability for PptxRawBackend {
fn id(&self) -> &str {
"office-raw"
}
fn formats(&self) -> &[&str] {
&["pptx"]
}
fn parse(&self, path: &str) -> Result<RawDoc> {
let file = std::fs::File::open(path)
.map_err(|e| DocError::corrupt(format!("cannot open {path}: {e}")))?;
let mut zip = zip::ZipArchive::new(file)
.map_err(|e| DocError::corrupt(format!("ZIP open failed: {e}")))?;
let mut cache: HashMap<String, String> = HashMap::new();
for i in 0..zip.len() {
let mut f = zip
.by_index(i)
.map_err(|e| DocError::corrupt(format!("ZIP entry error at index {i}: {e}")))?;
let name = f
.name()
.map_err(|e| DocError::corrupt(format!("ZIP entry name error at index {i}: {e}")))?
.to_string();
if name.ends_with(".xml") || name.ends_with(".rels") {
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut f, &mut buf)
.map_err(|e| DocError::corrupt(format!("读取 {name} 失败: {e}")))?;
let s = String::from_utf8(buf)
.map_err(|e| DocError::corrupt(format!("{name} 不是 UTF-8: {e}")))?;
cache.insert(name, s);
}
}
let pres_xml = cache
.get("ppt/presentation.xml")
.ok_or_else(|| DocError::corrupt("missing ppt/presentation.xml"))?;
let slide_rids = parse_slide_list(pres_xml)?;
let pres_rels = cache
.get("ppt/_rels/presentation.xml.rels")
.map(|s| s.as_str())
.unwrap_or("");
let rels_map = parse_rels(pres_rels);
let slide_paths: Vec<String> = slide_rids
.iter()
.filter_map(|rid| {
let target = rels_map.get(rid)?;
let cleaned = target.trim_start_matches("./");
Some(format!("ppt/{}", cleaned))
})
.collect();
if slide_paths.is_empty() {
return Err(DocError::corrupt("no slides found in presentation.xml"));
}
let mut pages = Vec::new();
for (idx, slide_path) in slide_paths.iter().enumerate() {
let xml = cache.get(slide_path).cloned().unwrap_or_default();
let (mut pos_items, chart_positions) = extract_slide_items(&xml)?;
let slide_num: usize = slide_path
.trim_start_matches("ppt/slides/slide")
.trim_end_matches(".xml")
.parse()
.unwrap_or(idx + 1);
let notes_path = format!("ppt/notesSlides/notesSlide{}.xml", slide_num);
if let Some(notes_xml) = cache.get(¬es_path) {
if let Ok(notes_texts) = extract_slide_texts(notes_xml) {
for t in notes_texts {
let trimmed = t.trim();
if !trimmed.is_empty() && !trimmed.chars().all(|c| c.is_ascii_digit()) {
pos_items.push((i64::MAX, i64::MAX, trimmed.to_string()));
}
}
}
}
let rels_path = format!("ppt/slides/_rels/slide{}.xml.rels", slide_num);
if let Some(rels_xml) = cache.get(&rels_path) {
let chart_targets = find_chart_targets(rels_xml);
for (ci, target) in chart_targets.iter().enumerate() {
let chart_path = resolve_rels_target(slide_path, target);
if let Some(chart_xml) = cache.get(&chart_path) {
let (c_y, c_x) = chart_positions.get(ci).copied().unwrap_or((0, 0));
if let Ok(chart_texts) = extract_chart_texts(chart_xml) {
for t in chart_texts {
if !t.trim().is_empty() {
pos_items.push((c_y, c_x, t));
}
}
}
}
}
}
pos_items.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
let items: Vec<docparser_core::plugin::RawTextItem> = pos_items
.into_iter()
.map(|(_, _, text)| docparser_core::plugin::RawTextItem { text, bbox: None })
.collect();
if !items.is_empty() {
pages.push(RawPage {
name: Some(format!("Slide {}", idx + 1)),
items,
..Default::default()
});
}
}
Ok(RawDoc {
pages,
..Default::default()
})
}
}
fn parse_slide_list(xml: &str) -> Result<Vec<String>> {
let mut reader = Reader::from_str(xml);
let mut buf = Vec::new();
let mut rids = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e)) => {
if e.name().local_name().as_ref() == crate::tags::P_SLDID {
for attr in e.attributes().filter_map(|a| a.ok()) {
if attr.key.as_ref() == "r:id" {
rids.push(attr.value.to_string());
}
}
}
}
Ok(Event::End(ref e)) => {
if e.name().local_name().as_ref() == "presentation" {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(DocError::corrupt(format!("presentation parse error: {e}"))),
_ => {}
}
buf.clear();
}
Ok(rids)
}
type SlideItems = (Vec<(i64, i64, String)>, Vec<(i64, i64)>);
fn extract_slide_items(xml: &str) -> Result<SlideItems> {
let mut reader = Reader::from_str(xml);
let mut buf = Vec::new();
let mut items = Vec::new();
let mut chart_positions = Vec::new();
let mut shape_depth: u32 = 0;
let mut cur_y: i64 = 0;
let mut cur_x: i64 = 0;
let mut in_t = false;
let mut text_buf = String::new();
let mut shape_texts: Vec<String> = Vec::new();
let mut is_chart_frame = false;
let mut in_tbl = false;
let mut in_tr = false;
let mut in_tc = false;
let mut cell_text = String::new();
let mut row_cells: Vec<String> = Vec::new();
let mut table_rows: Vec<String> = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let name = e.name().local_name().as_ref().to_string();
if matches!(
name.as_str(),
crate::tags::P_SP
| crate::tags::P_GFRAME
| crate::tags::P_GRPSP
| crate::tags::P_CXNSP
) {
if shape_depth == 0 {
cur_x = 0;
cur_y = 0;
shape_texts.clear();
is_chart_frame = name == crate::tags::P_GFRAME;
}
shape_depth += 1;
}
if shape_depth > 0 {
if name == crate::tags::A_OFF || name == crate::tags::A_POS {
for attr in e.attributes().filter_map(|a| a.ok()) {
match attr.key.as_ref() {
"y" => cur_y = attr.value.parse().unwrap_or(0),
"x" => cur_x = attr.value.parse().unwrap_or(0),
_ => {}
}
}
}
if name == crate::tags::A_T {
in_t = true;
text_buf.clear();
} else if name == crate::tags::A_BR
&& (!text_buf.is_empty() || !shape_texts.is_empty())
{
shape_texts.push(std::mem::take(&mut text_buf));
} else if name == crate::tags::A_TBL {
in_tbl = true;
table_rows.clear();
} else if name == crate::tags::A_TR {
in_tr = true;
row_cells.clear();
} else if name == crate::tags::A_TC {
in_tc = true;
cell_text.clear();
}
}
}
Ok(Event::Empty(ref e)) => {
let name = e.name().local_name().as_ref().to_string();
if shape_depth > 0 {
if name == crate::tags::A_OFF || name == crate::tags::A_POS {
for attr in e.attributes().filter_map(|a| a.ok()) {
match attr.key.as_ref() {
"y" => cur_y = attr.value.parse().unwrap_or(0),
"x" => cur_x = attr.value.parse().unwrap_or(0),
_ => {}
}
}
}
if name == crate::tags::A_T {
in_t = true;
text_buf.clear();
} else if name == crate::tags::A_BR
&& (!text_buf.is_empty() || !shape_texts.is_empty())
{
shape_texts.push(std::mem::take(&mut text_buf));
}
}
}
Ok(Event::End(ref e)) => {
let name = e.name().local_name().as_ref().to_string();
if shape_depth > 0 {
if name == crate::tags::A_T {
in_t = false;
let trimmed = text_buf.trim().to_string();
if !trimmed.is_empty() {
if in_tc {
if !cell_text.is_empty() {
cell_text.push('\n');
}
cell_text.push_str(&trimmed);
} else {
shape_texts.push(trimmed);
}
}
text_buf.clear();
} else if name == crate::tags::A_TC {
if in_tc {
row_cells.push(std::mem::take(&mut cell_text));
in_tc = false;
}
} else if name == crate::tags::A_TR {
if in_tr {
let row_str = row_cells.join(",");
table_rows.push(row_str);
row_cells.clear();
in_tr = false;
}
} else if name == crate::tags::A_TBL {
in_tbl = false;
}
if matches!(
name.as_str(),
crate::tags::P_SP
| crate::tags::P_GFRAME
| crate::tags::P_GRPSP
| crate::tags::P_CXNSP
) {
shape_depth -= 1;
if shape_depth == 0 {
if is_chart_frame && !in_tbl {
chart_positions.push((cur_y, cur_x));
}
if !table_rows.is_empty() {
for t in &table_rows {
if !t.is_empty() {
items.push((cur_y, cur_x, t.clone()));
}
}
table_rows.clear();
} else {
for t in &shape_texts {
if !t.is_empty() {
items.push((cur_y, cur_x, t.clone()));
}
}
}
shape_texts.clear();
}
}
}
}
Ok(Event::Text(ref e)) => {
if in_t {
let raw = e.xml10_content();
if let Ok(t) = quick_xml::escape::unescape(&raw) {
text_buf.push_str(&t);
}
}
}
Ok(Event::GeneralRef(ref e)) => {
if in_t {
crate::push_general_ref(&mut text_buf, e);
}
}
Ok(Event::CData(ref e)) => {
if in_t {
text_buf.push_str(e.as_ref());
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(DocError::corrupt(format!("slide parse error: {e}"))),
_ => {}
}
buf.clear();
}
Ok((items, chart_positions))
}
fn extract_slide_texts(xml: &str) -> Result<Vec<String>> {
let mut reader = Reader::from_str(xml);
let mut buf = Vec::new();
let mut texts = Vec::new();
let mut in_t = false;
let mut text_buf = String::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let name = e.name().local_name().as_ref().to_string();
if name == crate::tags::A_T {
in_t = true;
text_buf.clear();
} else if name == crate::tags::A_BR && (!text_buf.is_empty() || !texts.is_empty()) {
texts.push(std::mem::take(&mut text_buf));
}
}
Ok(Event::Empty(ref e)) => {
let name = e.name().local_name().as_ref().to_string();
if name == crate::tags::A_T {
in_t = true;
text_buf.clear();
} else if name == crate::tags::A_BR && (!text_buf.is_empty() || !texts.is_empty()) {
texts.push(std::mem::take(&mut text_buf));
}
}
Ok(Event::End(ref e)) => {
let name = e.name().local_name().as_ref().to_string();
if name == crate::tags::A_T {
in_t = false;
let trimmed = text_buf.trim().to_string();
if !trimmed.is_empty() {
texts.push(trimmed);
}
text_buf.clear();
}
}
Ok(Event::Text(ref e)) => {
if in_t {
let raw = e.xml10_content();
if let Ok(t) = quick_xml::escape::unescape(&raw) {
text_buf.push_str(&t);
}
}
}
Ok(Event::GeneralRef(ref e)) => {
if in_t {
crate::push_general_ref(&mut text_buf, e);
}
}
Ok(Event::CData(ref e)) => {
if in_t {
text_buf.push_str(e.as_ref());
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(DocError::corrupt(format!("slide parse error: {e}"))),
_ => {}
}
buf.clear();
}
Ok(texts)
}
fn find_chart_targets(rels_xml: &str) -> Vec<String> {
use quick_xml::events::Event;
use quick_xml::Reader;
let mut reader = Reader::from_str(rels_xml);
let mut buf = Vec::new();
let mut targets = Vec::new();
let chart_type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e)) => {
if e.name().local_name().as_ref() == "Relationship" {
let mut target = None;
let mut type_val = None;
for attr in e.attributes().filter_map(|a| a.ok()) {
match attr.key.as_ref() {
"Type" => type_val = Some(attr.value.to_string()),
"Target" => target = Some(attr.value.to_string()),
_ => {}
}
}
if type_val.as_deref() == Some(chart_type) {
if let Some(t) = target {
targets.push(t);
}
}
}
}
Ok(Event::Eof) => break,
_ => {}
}
buf.clear();
}
targets
}
fn resolve_rels_target(source_path: &str, target: &str) -> String {
let source_dir = source_path
.rfind('/')
.map(|i| &source_path[..i])
.unwrap_or("");
let mut result = source_dir.to_string();
let mut remaining = target;
while remaining.starts_with("../") {
if let Some(idx) = result.rfind('/') {
result = result[..idx].to_string();
} else {
result.clear();
}
remaining = &remaining[3..];
}
if result.is_empty() {
remaining.to_string()
} else {
format!("{}/{}", result, remaining)
}
}
fn extract_chart_texts(xml: &str) -> Result<Vec<String>> {
let mut reader = Reader::from_str(xml);
let mut buf = Vec::new();
let mut texts = Vec::new();
let mut in_t = false;
let mut in_tx = false;
let mut in_tx_v = false;
let mut text_buf = String::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let name = e.name().local_name().as_ref().to_string();
if name == crate::tags::C_TX {
in_tx = true;
} else if name == crate::tags::C_V && in_tx {
in_tx_v = true;
text_buf.clear();
} else if name == crate::tags::A_T {
in_t = true;
text_buf.clear();
}
}
Ok(Event::End(ref e)) => {
let name = e.name().local_name().as_ref().to_string();
if name == crate::tags::C_TX {
in_tx = false;
} else if name == crate::tags::C_V && in_tx_v {
in_tx_v = false;
let t = text_buf.trim().to_string();
if !t.is_empty() {
texts.push(t);
}
text_buf.clear();
} else if name == crate::tags::A_T {
in_t = false;
let t = text_buf.trim().to_string();
if !t.is_empty() {
texts.push(t);
}
text_buf.clear();
}
}
Ok(Event::Text(ref e)) => {
if in_t || in_tx_v {
let raw = e.xml10_content();
if let Ok(t) = quick_xml::escape::unescape(&raw) {
text_buf.push_str(&t);
}
}
}
Ok(Event::GeneralRef(ref e)) => {
if in_t || in_tx_v {
crate::push_general_ref(&mut text_buf, e);
}
}
Ok(Event::CData(ref e)) => {
if in_t || in_tx_v {
text_buf.push_str(e.as_ref());
}
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
Ok(texts)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_slide_list() {
let xml = concat!(
r#"<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" "#,
r#"xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
r#" <p:sldIdLst>"#,
r#" <p:sldId id="256" r:id="rId1"/>"#,
r#" <p:sldId id="257" r:id="rId2"/>"#,
r#" </p:sldIdLst>"#,
r#"</p:presentation>"#
);
let rids = parse_slide_list(xml).unwrap();
assert_eq!(rids, vec!["rId1", "rId2"]);
}
#[test]
fn test_slide_text() {
let xml = concat!(
r#"<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" "#,
r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">"#,
r#" <p:spTree>"#,
r#" <p:sp>"#,
r#" <p:nvSpPr><p:cNvPr id="1"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>"#,
r#" <p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="100" cy="50"/></a:xfrm></p:spPr>"#,
r#" <p:txBody>"#,
r#" <a:p>"#,
r#" <a:r>"#,
r#" <a:t>Hello World</a:t>"#,
r#" </a:r>"#,
r#" </a:p>"#,
r#" </p:txBody>"#,
r#" </p:sp>"#,
r#" </p:spTree>"#,
r#"</p:sld>"#
);
let (items, chart_pos) = extract_slide_items(xml).unwrap();
assert!(chart_pos.is_empty());
assert!(items.iter().any(|(_, _, t)| t == "Hello World"));
}
}