1use docparser_core::error::{DocError, Result};
7use docparser_core::plugin::{ParseCapability, RawDoc, RawPage};
8use quick_xml::events::Event;
9use quick_xml::Reader;
10
11use crate::parse_rels;
12use std::collections::HashMap;
13
14pub struct PptxRawBackend;
16
17impl ParseCapability for PptxRawBackend {
18 fn id(&self) -> &str {
19 "office-raw"
20 }
21 fn formats(&self) -> &[&str] {
22 &["pptx"]
23 }
24 fn parse(&self, path: &str) -> Result<RawDoc> {
25 let file = std::fs::File::open(path)
26 .map_err(|e| DocError::corrupt(format!("cannot open {path}: {e}")))?;
27 let mut zip = zip::ZipArchive::new(file)
28 .map_err(|e| DocError::corrupt(format!("ZIP open failed: {e}")))?;
29
30 let mut cache: HashMap<String, String> = HashMap::new();
32 for i in 0..zip.len() {
33 let mut f = zip
34 .by_index(i)
35 .map_err(|e| DocError::corrupt(format!("ZIP entry error at index {i}: {e}")))?;
36 let name = f
38 .name()
39 .map_err(|e| DocError::corrupt(format!("ZIP entry name error at index {i}: {e}")))?
40 .to_string();
41 if name.ends_with(".xml") || name.ends_with(".rels") {
42 let mut buf = Vec::new();
43 std::io::Read::read_to_end(&mut f, &mut buf)
44 .map_err(|e| DocError::corrupt(format!("读取 {name} 失败: {e}")))?;
45 let s = String::from_utf8(buf)
46 .map_err(|e| DocError::corrupt(format!("{name} 不是 UTF-8: {e}")))?;
47 cache.insert(name, s);
48 }
49 }
50
51 let pres_xml = cache
53 .get("ppt/presentation.xml")
54 .ok_or_else(|| DocError::corrupt("missing ppt/presentation.xml"))?;
55 let slide_rids = parse_slide_list(pres_xml)?;
56
57 let pres_rels = cache
59 .get("ppt/_rels/presentation.xml.rels")
60 .map(|s| s.as_str())
61 .unwrap_or("");
62 let rels_map = parse_rels(pres_rels);
63
64 let slide_paths: Vec<String> = slide_rids
66 .iter()
67 .filter_map(|rid| {
68 let target = rels_map.get(rid)?;
69 let cleaned = target.trim_start_matches("./");
71 Some(format!("ppt/{}", cleaned))
72 })
73 .collect();
74
75 if slide_paths.is_empty() {
76 return Err(DocError::corrupt("no slides found in presentation.xml"));
77 }
78
79 let mut pages = Vec::new();
81 for (idx, slide_path) in slide_paths.iter().enumerate() {
82 let xml = cache.get(slide_path).cloned().unwrap_or_default();
83
84 let (mut pos_items, chart_positions) = extract_slide_items(&xml)?;
86
87 let slide_num: usize = slide_path
89 .trim_start_matches("ppt/slides/slide")
90 .trim_end_matches(".xml")
91 .parse()
92 .unwrap_or(idx + 1);
93
94 let notes_path = format!("ppt/notesSlides/notesSlide{}.xml", slide_num);
96 if let Some(notes_xml) = cache.get(¬es_path) {
97 if let Ok(notes_texts) = extract_slide_texts(notes_xml) {
98 for t in notes_texts {
99 let trimmed = t.trim();
100 if !trimmed.is_empty() && !trimmed.chars().all(|c| c.is_ascii_digit()) {
101 pos_items.push((i64::MAX, i64::MAX, trimmed.to_string()));
102 }
103 }
104 }
105 }
106
107 let rels_path = format!("ppt/slides/_rels/slide{}.xml.rels", slide_num);
109 if let Some(rels_xml) = cache.get(&rels_path) {
110 let chart_targets = find_chart_targets(rels_xml);
111 for (ci, target) in chart_targets.iter().enumerate() {
113 let chart_path = resolve_rels_target(slide_path, target);
114 if let Some(chart_xml) = cache.get(&chart_path) {
115 let (c_y, c_x) = chart_positions.get(ci).copied().unwrap_or((0, 0));
116 if let Ok(chart_texts) = extract_chart_texts(chart_xml) {
117 for t in chart_texts {
118 if !t.trim().is_empty() {
119 pos_items.push((c_y, c_x, t));
120 }
121 }
122 }
123 }
124 }
125 }
126
127 pos_items.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
129
130 let items: Vec<docparser_core::plugin::RawTextItem> = pos_items
131 .into_iter()
132 .map(|(_, _, text)| docparser_core::plugin::RawTextItem { text, bbox: None })
133 .collect();
134
135 if !items.is_empty() {
136 pages.push(RawPage {
137 name: Some(format!("Slide {}", idx + 1)),
138 items,
139 ..Default::default()
140 });
141 }
142 }
143
144 Ok(RawDoc {
145 pages,
146 ..Default::default()
147 })
148 }
149}
150
151fn parse_slide_list(xml: &str) -> Result<Vec<String>> {
153 let mut reader = Reader::from_str(xml);
154 let mut buf = Vec::new();
156 let mut rids = Vec::new();
157
158 loop {
159 match reader.read_event_into(&mut buf) {
160 Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e)) => {
161 if e.name().local_name().as_ref() == crate::tags::P_SLDID {
162 for attr in e.attributes().filter_map(|a| a.ok()) {
163 if attr.key.as_ref() == "r:id" {
165 rids.push(attr.value.to_string());
166 }
167 }
168 }
169 }
170 Ok(Event::End(ref e)) => {
171 if e.name().local_name().as_ref() == "presentation" {
172 break;
173 }
174 }
175 Ok(Event::Eof) => break,
176 Err(e) => return Err(DocError::corrupt(format!("presentation parse error: {e}"))),
177 _ => {}
178 }
179 buf.clear();
180 }
181 Ok(rids)
182}
183
184type SlideItems = (Vec<(i64, i64, String)>, Vec<(i64, i64)>);
186
187fn extract_slide_items(xml: &str) -> Result<SlideItems> {
193 let mut reader = Reader::from_str(xml);
194 let mut buf = Vec::new();
196 let mut items = Vec::new();
197 let mut chart_positions = Vec::new();
198
199 let mut shape_depth: u32 = 0;
200 let mut cur_y: i64 = 0;
201 let mut cur_x: i64 = 0;
202 let mut in_t = false;
203 let mut text_buf = String::new();
204 let mut shape_texts: Vec<String> = Vec::new();
205 let mut is_chart_frame = false;
206 let mut in_tbl = false;
208 let mut in_tr = false;
209 let mut in_tc = false;
210 let mut cell_text = String::new();
211 let mut row_cells: Vec<String> = Vec::new();
212 let mut table_rows: Vec<String> = Vec::new();
213
214 loop {
215 match reader.read_event_into(&mut buf) {
216 Ok(Event::Start(ref e)) => {
217 let name = e.name().local_name().as_ref().to_string();
218
219 if matches!(
220 name.as_str(),
221 crate::tags::P_SP
222 | crate::tags::P_GFRAME
223 | crate::tags::P_GRPSP
224 | crate::tags::P_CXNSP
225 ) {
226 if shape_depth == 0 {
227 cur_x = 0;
228 cur_y = 0;
229 shape_texts.clear();
230 is_chart_frame = name == crate::tags::P_GFRAME;
231 }
232 shape_depth += 1;
233 }
234
235 if shape_depth > 0 {
236 if name == crate::tags::A_OFF || name == crate::tags::A_POS {
237 for attr in e.attributes().filter_map(|a| a.ok()) {
238 match attr.key.as_ref() {
239 "y" => cur_y = attr.value.parse().unwrap_or(0),
240 "x" => cur_x = attr.value.parse().unwrap_or(0),
241 _ => {}
242 }
243 }
244 }
245
246 if name == crate::tags::A_T {
247 in_t = true;
248 text_buf.clear();
249 } else if name == crate::tags::A_BR
250 && (!text_buf.is_empty() || !shape_texts.is_empty())
251 {
252 shape_texts.push(std::mem::take(&mut text_buf));
253 } else if name == crate::tags::A_TBL {
254 in_tbl = true;
255 table_rows.clear();
256 } else if name == crate::tags::A_TR {
257 in_tr = true;
258 row_cells.clear();
259 } else if name == crate::tags::A_TC {
260 in_tc = true;
261 cell_text.clear();
262 }
263 }
264 }
265 Ok(Event::Empty(ref e)) => {
266 let name = e.name().local_name().as_ref().to_string();
267 if shape_depth > 0 {
268 if name == crate::tags::A_OFF || name == crate::tags::A_POS {
269 for attr in e.attributes().filter_map(|a| a.ok()) {
270 match attr.key.as_ref() {
271 "y" => cur_y = attr.value.parse().unwrap_or(0),
272 "x" => cur_x = attr.value.parse().unwrap_or(0),
273 _ => {}
274 }
275 }
276 }
277 if name == crate::tags::A_T {
278 in_t = true;
279 text_buf.clear();
280 } else if name == crate::tags::A_BR
281 && (!text_buf.is_empty() || !shape_texts.is_empty())
282 {
283 shape_texts.push(std::mem::take(&mut text_buf));
284 }
285 }
286 }
287 Ok(Event::End(ref e)) => {
288 let name = e.name().local_name().as_ref().to_string();
289 if shape_depth > 0 {
290 if name == crate::tags::A_T {
291 in_t = false;
292 let trimmed = text_buf.trim().to_string();
293 if !trimmed.is_empty() {
294 if in_tc {
295 if !cell_text.is_empty() {
297 cell_text.push('\n');
298 }
299 cell_text.push_str(&trimmed);
300 } else {
301 shape_texts.push(trimmed);
302 }
303 }
304 text_buf.clear();
305 } else if name == crate::tags::A_TC {
306 if in_tc {
307 row_cells.push(std::mem::take(&mut cell_text));
308 in_tc = false;
309 }
310 } else if name == crate::tags::A_TR {
311 if in_tr {
312 let row_str = row_cells.join(",");
313 table_rows.push(row_str);
314 row_cells.clear();
315 in_tr = false;
316 }
317 } else if name == crate::tags::A_TBL {
318 in_tbl = false;
319 }
320
321 if matches!(
322 name.as_str(),
323 crate::tags::P_SP
324 | crate::tags::P_GFRAME
325 | crate::tags::P_GRPSP
326 | crate::tags::P_CXNSP
327 ) {
328 shape_depth -= 1;
329 if shape_depth == 0 {
330 if is_chart_frame && !in_tbl {
332 chart_positions.push((cur_y, cur_x));
333 }
334 if !table_rows.is_empty() {
335 for t in &table_rows {
337 if !t.is_empty() {
338 items.push((cur_y, cur_x, t.clone()));
339 }
340 }
341 table_rows.clear();
342 } else {
343 for t in &shape_texts {
344 if !t.is_empty() {
345 items.push((cur_y, cur_x, t.clone()));
346 }
347 }
348 }
349 shape_texts.clear();
350 }
351 }
352 }
353 }
354 Ok(Event::Text(ref e)) => {
355 if in_t {
356 let raw = e.xml10_content();
358 if let Ok(t) = quick_xml::escape::unescape(&raw) {
359 text_buf.push_str(&t);
360 }
361 }
362 }
363 Ok(Event::GeneralRef(ref e)) => {
364 if in_t {
365 crate::push_general_ref(&mut text_buf, e);
366 }
367 }
368 Ok(Event::CData(ref e)) => {
369 if in_t {
370 text_buf.push_str(e.as_ref());
371 }
372 }
373 Ok(Event::Eof) => break,
374 Err(e) => return Err(DocError::corrupt(format!("slide parse error: {e}"))),
375 _ => {}
376 }
377 buf.clear();
378 }
379
380 Ok((items, chart_positions))
381}
382
383fn extract_slide_texts(xml: &str) -> Result<Vec<String>> {
385 let mut reader = Reader::from_str(xml);
386 let mut buf = Vec::new();
388 let mut texts = Vec::new();
389 let mut in_t = false;
390 let mut text_buf = String::new();
391
392 loop {
393 match reader.read_event_into(&mut buf) {
394 Ok(Event::Start(ref e)) => {
395 let name = e.name().local_name().as_ref().to_string();
396 if name == crate::tags::A_T {
397 in_t = true;
398 text_buf.clear();
399 } else if name == crate::tags::A_BR && (!text_buf.is_empty() || !texts.is_empty()) {
400 texts.push(std::mem::take(&mut text_buf));
402 }
403 }
404 Ok(Event::Empty(ref e)) => {
405 let name = e.name().local_name().as_ref().to_string();
406 if name == crate::tags::A_T {
407 in_t = true;
408 text_buf.clear();
409 } else if name == crate::tags::A_BR && (!text_buf.is_empty() || !texts.is_empty()) {
410 texts.push(std::mem::take(&mut text_buf));
411 }
412 }
413 Ok(Event::End(ref e)) => {
414 let name = e.name().local_name().as_ref().to_string();
415 if name == crate::tags::A_T {
416 in_t = false;
417 let trimmed = text_buf.trim().to_string();
418 if !trimmed.is_empty() {
419 texts.push(trimmed);
420 }
421 text_buf.clear();
422 }
423 }
424 Ok(Event::Text(ref e)) => {
425 if in_t {
426 let raw = e.xml10_content();
428 if let Ok(t) = quick_xml::escape::unescape(&raw) {
429 text_buf.push_str(&t);
430 }
431 }
432 }
433 Ok(Event::GeneralRef(ref e)) => {
434 if in_t {
435 crate::push_general_ref(&mut text_buf, e);
436 }
437 }
438 Ok(Event::CData(ref e)) => {
439 if in_t {
440 text_buf.push_str(e.as_ref());
441 }
442 }
443 Ok(Event::Eof) => break,
444 Err(e) => return Err(DocError::corrupt(format!("slide parse error: {e}"))),
445 _ => {}
446 }
447 buf.clear();
448 }
449 Ok(texts)
450}
451
452fn find_chart_targets(rels_xml: &str) -> Vec<String> {
454 use quick_xml::events::Event;
455 use quick_xml::Reader;
456
457 let mut reader = Reader::from_str(rels_xml);
458 let mut buf = Vec::new();
460 let mut targets = Vec::new();
461 let chart_type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
462
463 loop {
464 match reader.read_event_into(&mut buf) {
465 Ok(Event::Empty(ref e)) | Ok(Event::Start(ref e)) => {
466 if e.name().local_name().as_ref() == "Relationship" {
467 let mut target = None;
468 let mut type_val = None;
469 for attr in e.attributes().filter_map(|a| a.ok()) {
470 match attr.key.as_ref() {
471 "Type" => type_val = Some(attr.value.to_string()),
472 "Target" => target = Some(attr.value.to_string()),
473 _ => {}
474 }
475 }
476 if type_val.as_deref() == Some(chart_type) {
477 if let Some(t) = target {
478 targets.push(t);
479 }
480 }
481 }
482 }
483 Ok(Event::Eof) => break,
484 _ => {}
485 }
486 buf.clear();
487 }
488 targets
489}
490
491fn resolve_rels_target(source_path: &str, target: &str) -> String {
497 let source_dir = source_path
498 .rfind('/')
499 .map(|i| &source_path[..i])
500 .unwrap_or("");
501 let mut result = source_dir.to_string();
502 let mut remaining = target;
503 while remaining.starts_with("../") {
504 if let Some(idx) = result.rfind('/') {
505 result = result[..idx].to_string();
506 } else {
507 result.clear();
508 }
509 remaining = &remaining[3..];
510 }
511 if result.is_empty() {
512 remaining.to_string()
513 } else {
514 format!("{}/{}", result, remaining)
515 }
516}
517
518fn extract_chart_texts(xml: &str) -> Result<Vec<String>> {
524 let mut reader = Reader::from_str(xml);
525 let mut buf = Vec::new();
527 let mut texts = Vec::new();
528 let mut in_t = false;
529 let mut in_tx = false;
530 let mut in_tx_v = false;
531 let mut text_buf = String::new();
532
533 loop {
534 match reader.read_event_into(&mut buf) {
535 Ok(Event::Start(ref e)) => {
536 let name = e.name().local_name().as_ref().to_string();
537 if name == crate::tags::C_TX {
538 in_tx = true;
539 } else if name == crate::tags::C_V && in_tx {
540 in_tx_v = true;
541 text_buf.clear();
542 } else if name == crate::tags::A_T {
543 in_t = true;
544 text_buf.clear();
545 }
546 }
547 Ok(Event::End(ref e)) => {
548 let name = e.name().local_name().as_ref().to_string();
549 if name == crate::tags::C_TX {
550 in_tx = false;
551 } else if name == crate::tags::C_V && in_tx_v {
552 in_tx_v = false;
553 let t = text_buf.trim().to_string();
554 if !t.is_empty() {
555 texts.push(t);
556 }
557 text_buf.clear();
558 } else if name == crate::tags::A_T {
559 in_t = false;
560 let t = text_buf.trim().to_string();
561 if !t.is_empty() {
562 texts.push(t);
563 }
564 text_buf.clear();
565 }
566 }
567 Ok(Event::Text(ref e)) => {
568 if in_t || in_tx_v {
569 let raw = e.xml10_content();
570 if let Ok(t) = quick_xml::escape::unescape(&raw) {
571 text_buf.push_str(&t);
572 }
573 }
574 }
575 Ok(Event::GeneralRef(ref e)) => {
576 if in_t || in_tx_v {
577 crate::push_general_ref(&mut text_buf, e);
578 }
579 }
580 Ok(Event::CData(ref e)) => {
581 if in_t || in_tx_v {
582 text_buf.push_str(e.as_ref());
583 }
584 }
585 Ok(Event::Eof) => break,
586 Err(_) => break,
587 _ => {}
588 }
589 buf.clear();
590 }
591 Ok(texts)
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[test]
599 fn test_slide_list() {
600 let xml = concat!(
601 r#"<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" "#,
602 r#"xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
603 r#" <p:sldIdLst>"#,
604 r#" <p:sldId id="256" r:id="rId1"/>"#,
605 r#" <p:sldId id="257" r:id="rId2"/>"#,
606 r#" </p:sldIdLst>"#,
607 r#"</p:presentation>"#
608 );
609 let rids = parse_slide_list(xml).unwrap();
610 assert_eq!(rids, vec!["rId1", "rId2"]);
611 }
612
613 #[test]
614 fn test_slide_text() {
615 let xml = concat!(
616 r#"<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" "#,
617 r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">"#,
618 r#" <p:spTree>"#,
619 r#" <p:sp>"#,
620 r#" <p:nvSpPr><p:cNvPr id="1"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>"#,
621 r#" <p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="100" cy="50"/></a:xfrm></p:spPr>"#,
622 r#" <p:txBody>"#,
623 r#" <a:p>"#,
624 r#" <a:r>"#,
625 r#" <a:t>Hello World</a:t>"#,
626 r#" </a:r>"#,
627 r#" </a:p>"#,
628 r#" </p:txBody>"#,
629 r#" </p:sp>"#,
630 r#" </p:spTree>"#,
631 r#"</p:sld>"#
632 );
633 let (items, chart_pos) = extract_slide_items(xml).unwrap();
634 assert!(chart_pos.is_empty());
635 assert!(items.iter().any(|(_, _, t)| t == "Hello World"));
636 }
637}