Skip to main content

ooxml_pml/
presentation.rs

1//! Presentation API for reading and writing PowerPoint files.
2//!
3//! This module provides the main entry point for working with PPTX files.
4
5use crate::error::{Error, Result};
6use crate::ext::{CommonSlideDataExt, GroupShapeExt, PictureExt, ShapeExt};
7use crate::parsers::FromXml;
8use crate::types;
9use ooxml_dml::ext::{TextBodyExt, TextParagraphExt, TextRunExt};
10use ooxml_opc::{Package, Relationships};
11use quick_xml::Reader;
12use quick_xml::events::Event;
13use std::fs::File;
14use std::io::{BufReader, Cursor, Read, Seek};
15use std::path::Path;
16
17// Relationship types (ECMA-376 Part 1)
18const REL_OFFICE_DOCUMENT: &str =
19    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
20const REL_SLIDE: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
21const REL_NOTES_SLIDE: &str =
22    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
23const REL_SLIDE_MASTER: &str =
24    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
25const REL_SLIDE_LAYOUT: &str =
26    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
27
28/// A PowerPoint presentation.
29///
30/// This is the main entry point for reading PPTX files.
31pub struct Presentation<R: Read + Seek> {
32    package: Package<R>,
33    /// Path to the presentation part.
34    #[allow(dead_code)]
35    presentation_path: String,
36    /// Presentation-level relationships.
37    #[allow(dead_code)]
38    pres_rels: Relationships,
39    /// Slide metadata (relationship ID, path).
40    slide_info: Vec<SlideInfo>,
41    /// Slide masters in the presentation.
42    slide_masters: Vec<SlideMaster>,
43    /// Slide layouts in the presentation.
44    slide_layouts: Vec<SlideLayout>,
45}
46
47/// Metadata about a slide.
48#[derive(Debug, Clone)]
49struct SlideInfo {
50    #[allow(dead_code)]
51    rel_id: String,
52    path: String,
53    index: usize,
54    /// Relationship ID to the slide layout.
55    layout_rel_id: Option<String>,
56}
57
58/// Image data loaded from the presentation.
59#[derive(Debug, Clone)]
60pub struct ImageData {
61    /// The raw image data.
62    pub data: Vec<u8>,
63    /// The content type (MIME type) of the image.
64    pub content_type: String,
65}
66
67/// A slide master in the presentation.
68///
69/// Slide masters define the overall theme and formatting for slides.
70/// ECMA-376 Part 1, Section 19.3.1.42 (sldMaster).
71#[derive(Debug, Clone)]
72pub struct SlideMaster {
73    /// Path to the slide master part.
74    path: String,
75    /// Name of the slide master (if specified).
76    pub name: Option<String>,
77    /// Relationship IDs of layouts using this master.
78    layout_ids: Vec<String>,
79    /// Color scheme name.
80    pub color_scheme: Option<String>,
81    /// Background color (ARGB).
82    pub background_color: Option<String>,
83}
84
85impl SlideMaster {
86    /// Get the path to this slide master.
87    pub fn path(&self) -> &str {
88        &self.path
89    }
90
91    /// Get the number of layouts using this master.
92    pub fn layout_count(&self) -> usize {
93        self.layout_ids.len()
94    }
95}
96
97/// A slide layout in the presentation.
98///
99/// Slide layouts define the arrangement of content placeholders.
100/// ECMA-376 Part 1, Section 19.3.1.39 (sldLayout).
101#[derive(Debug, Clone)]
102pub struct SlideLayout {
103    /// Path to the slide layout part.
104    path: String,
105    /// Name of the layout (e.g., "Title Slide", "Title and Content").
106    pub name: Option<String>,
107    /// Layout type.
108    pub layout_type: SlideLayoutType,
109    /// Relationship ID to the slide master.
110    #[allow(dead_code)]
111    master_rel_id: Option<String>,
112    /// Whether to match slide names.
113    pub match_name: bool,
114    /// Whether to show master shapes.
115    pub show_master_shapes: bool,
116}
117
118impl SlideLayout {
119    /// Get the path to this slide layout.
120    pub fn path(&self) -> &str {
121        &self.path
122    }
123}
124
125/// Type of slide layout.
126///
127/// ECMA-376 Part 1, Section 19.7.15 (ST_SlideLayoutType).
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
129pub enum SlideLayoutType {
130    /// Blank slide.
131    Blank,
132    /// Title slide.
133    #[default]
134    Title,
135    /// Title and content.
136    TitleAndContent,
137    /// Section header.
138    SectionHeader,
139    /// Two content.
140    TwoContent,
141    /// Two content and text.
142    TwoContentAndText,
143    /// Title only.
144    TitleOnly,
145    /// Content with caption.
146    ContentWithCaption,
147    /// Picture with caption.
148    PictureWithCaption,
149    /// Vertical title and text.
150    VerticalTitleAndText,
151    /// Vertical text.
152    VerticalText,
153    /// Custom layout.
154    Custom,
155    /// Unknown layout type.
156    Unknown,
157}
158
159impl SlideLayoutType {
160    /// Parse from the slideLayout type attribute.
161    fn parse(s: &str) -> Self {
162        match s {
163            "blank" => Self::Blank,
164            "title" | "tx" => Self::Title,
165            "obj" | "objTx" | "twoObj" | "twoObjAndTx" => Self::TitleAndContent,
166            "secHead" => Self::SectionHeader,
167            "twoTxTwoObj" => Self::TwoContent,
168            "objAndTx" => Self::TwoContentAndText,
169            "titleOnly" => Self::TitleOnly,
170            "objOnly" => Self::ContentWithCaption,
171            "picTx" => Self::PictureWithCaption,
172            "vertTx" => Self::VerticalText,
173            "vertTitleAndTx" => Self::VerticalTitleAndText,
174            "cust" => Self::Custom,
175            _ => Self::Unknown,
176        }
177    }
178}
179
180impl Presentation<BufReader<File>> {
181    /// Open a presentation from a file path.
182    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
183        let file = File::open(path)?;
184        Self::from_reader(BufReader::new(file))
185    }
186}
187
188impl<R: Read + Seek> Presentation<R> {
189    /// Open a presentation from a reader.
190    pub fn from_reader(reader: R) -> Result<Self> {
191        let mut package = Package::open(reader)?;
192
193        // Find the presentation part via root relationships
194        let root_rels = package.read_relationships()?;
195        let pres_rel = root_rels
196            .get_by_type(REL_OFFICE_DOCUMENT)
197            .ok_or_else(|| Error::Invalid("Missing presentation relationship".into()))?;
198        let presentation_path = pres_rel.target.clone();
199
200        // Load presentation relationships
201        let pres_rels = package
202            .read_part_relationships(&presentation_path)
203            .unwrap_or_default();
204
205        // Parse presentation.xml to get slide list
206        let pres_xml = package.read_part(&presentation_path)?;
207        let slide_order = parse_presentation_slides(&pres_xml)?;
208
209        // Load slide masters
210        let mut slide_masters: Vec<SlideMaster> = Vec::new();
211        let mut slide_layouts: Vec<SlideLayout> = Vec::new();
212
213        for rel in pres_rels.iter() {
214            if rel.relationship_type == REL_SLIDE_MASTER {
215                let path = resolve_path(&presentation_path, &rel.target);
216                if let Ok(master_xml) = package.read_part(&path) {
217                    let master = parse_slide_master(&master_xml, &path);
218                    let master_path = path.clone();
219
220                    // Load layouts for this master
221                    if let Ok(master_rels) = package.read_part_relationships(&path) {
222                        for layout_rel in master_rels.iter() {
223                            if layout_rel.relationship_type == REL_SLIDE_LAYOUT {
224                                let layout_path = resolve_path(&master_path, &layout_rel.target);
225                                if let Ok(layout_xml) = package.read_part(&layout_path) {
226                                    let layout = parse_slide_layout(
227                                        &layout_xml,
228                                        &layout_path,
229                                        Some(layout_rel.id.clone()),
230                                    );
231                                    slide_layouts.push(layout);
232                                }
233                            }
234                        }
235                    }
236
237                    slide_masters.push(master);
238                }
239            }
240        }
241
242        // Build slide info from relationships, getting layout references from slide XML
243        let mut slide_info: Vec<SlideInfo> = Vec::new();
244        for rel in pres_rels.iter() {
245            if rel.relationship_type == REL_SLIDE {
246                let path = resolve_path(&presentation_path, &rel.target);
247                // Find index from slide order
248                let index = slide_order
249                    .iter()
250                    .position(|id| id == &rel.id)
251                    .unwrap_or(slide_info.len());
252
253                // Get layout relationship from slide
254                let layout_rel_id = if let Ok(slide_rels) = package.read_part_relationships(&path) {
255                    slide_rels
256                        .get_by_type(REL_SLIDE_LAYOUT)
257                        .map(|r| r.id.clone())
258                } else {
259                    None
260                };
261
262                slide_info.push(SlideInfo {
263                    rel_id: rel.id.clone(),
264                    path,
265                    index,
266                    layout_rel_id,
267                });
268            }
269        }
270
271        // Sort by index
272        slide_info.sort_by_key(|s| s.index);
273
274        Ok(Self {
275            package,
276            presentation_path,
277            pres_rels,
278            slide_info,
279            slide_masters,
280            slide_layouts,
281        })
282    }
283
284    /// Get the number of slides in the presentation.
285    pub fn slide_count(&self) -> usize {
286        self.slide_info.len()
287    }
288
289    /// Get all slide masters in the presentation.
290    pub fn slide_masters(&self) -> &[SlideMaster] {
291        &self.slide_masters
292    }
293
294    /// Get all slide layouts in the presentation.
295    pub fn slide_layouts(&self) -> &[SlideLayout] {
296        &self.slide_layouts
297    }
298
299    /// Get a slide layout by name.
300    pub fn layout_by_name(&self, name: &str) -> Option<&SlideLayout> {
301        self.slide_layouts
302            .iter()
303            .find(|l| l.name.as_deref() == Some(name))
304    }
305
306    /// Get a slide by index (0-based).
307    pub fn slide(&mut self, index: usize) -> Result<Slide> {
308        let info = self
309            .slide_info
310            .get(index)
311            .ok_or_else(|| Error::Invalid(format!("Slide index {} out of range", index)))?
312            .clone();
313
314        self.load_slide(&info)
315    }
316
317    /// Load all slides.
318    pub fn slides(&mut self) -> Result<Vec<Slide>> {
319        let infos: Vec<_> = self.slide_info.clone();
320        infos.iter().map(|info| self.load_slide(info)).collect()
321    }
322
323    /// Load a slide's data.
324    fn load_slide(&mut self, info: &SlideInfo) -> Result<Slide> {
325        let data = self.package.read_part(&info.path)?;
326
327        // Parse slide using generated FromXml parser
328        let inner = parse_slide_xml(&data)?;
329
330        // Extract tables from graphic frames
331        let tables = extract_tables_from_slide(&inner);
332
333        // Extract chart and SmartArt relationship IDs from graphic frames
334        let (chart_rel_ids, smartart_rel_ids) = extract_charts_and_smartart_from_slide(&inner);
335
336        // Build the slide wrapper
337        let mut slide = Slide {
338            inner,
339            index: info.index,
340            slide_path: info.path.clone(),
341            notes: None,
342            layout_rel_id: info.layout_rel_id.clone(),
343            tables,
344            chart_rel_ids,
345            smartart_rel_ids,
346        };
347
348        // Try to load speaker notes
349        if let Ok(slide_rels) = self.package.read_part_relationships(&info.path)
350            && let Some(notes_rel) = slide_rels.get_by_type(REL_NOTES_SLIDE)
351        {
352            let notes_path = resolve_path(&info.path, &notes_rel.target);
353            if let Ok(notes_data) = self.package.read_part(&notes_path) {
354                slide.notes = parse_notes_slide(&notes_data);
355            }
356        }
357
358        Ok(slide)
359    }
360
361    /// Get image data for a picture from a specific slide.
362    ///
363    /// Loads the image data from the package using the picture's relationship ID.
364    pub fn get_image_data(&mut self, slide: &Slide, picture: &types::Picture) -> Result<ImageData> {
365        // Get the embed relationship ID using the extension trait
366        let rel_id = picture
367            .embed_rel_id()
368            .ok_or_else(|| Error::Invalid("Picture has no embed relationship ID".into()))?;
369
370        // Get slide relationships
371        let slide_rels = self
372            .package
373            .read_part_relationships(slide.slide_path())
374            .map_err(|_| Error::Invalid("Failed to read slide relationships".into()))?;
375
376        // Find the image relationship
377        let rel = slide_rels
378            .get(rel_id)
379            .ok_or_else(|| Error::Invalid(format!("Image relationship {} not found", rel_id)))?;
380
381        // Resolve the image path
382        let image_path = resolve_path(slide.slide_path(), &rel.target);
383
384        // Read image data
385        let data = self.package.read_part(&image_path)?;
386
387        // Determine content type from extension
388        let content_type = content_type_from_path(&image_path);
389
390        Ok(ImageData { data, content_type })
391    }
392
393    /// Resolve a hyperlink relationship ID to its target URL.
394    ///
395    /// # Arguments
396    /// * `slide` - The slide containing the hyperlink
397    /// * `rel_id` - The relationship ID from the hyperlink
398    ///
399    /// # Returns
400    /// The target URL/path of the hyperlink, or an error if not found.
401    pub fn resolve_hyperlink(&mut self, slide: &Slide, rel_id: &str) -> Result<String> {
402        // Get slide relationships
403        let slide_rels = self
404            .package
405            .read_part_relationships(slide.slide_path())
406            .map_err(|_| Error::Invalid("Failed to read slide relationships".into()))?;
407
408        // Find the hyperlink relationship
409        let rel = slide_rels.get(rel_id).ok_or_else(|| {
410            Error::Invalid(format!("Hyperlink relationship {} not found", rel_id))
411        })?;
412
413        Ok(rel.target.clone())
414    }
415
416    /// Get all hyperlinks from a slide with their resolved URLs.
417    ///
418    /// Returns a list of (text, url) pairs for all hyperlinks on the slide.
419    pub fn get_hyperlinks_with_urls(&mut self, slide: &Slide) -> Result<Vec<(String, String)>> {
420        let hyperlinks = slide.hyperlinks();
421        let mut results = Vec::new();
422
423        for link in hyperlinks {
424            if let Ok(url) = self.resolve_hyperlink(slide, &link.rel_id) {
425                results.push((link.text, url));
426            }
427        }
428
429        Ok(results)
430    }
431
432    /// Load and parse a chart by its relationship ID.
433    ///
434    /// Use [`Slide::chart_rel_ids`] to get the relationship IDs for all charts
435    /// on a given slide, then pass each ID here to load the full chart definition.
436    ///
437    /// Requires the `pml-charts` feature.
438    ///
439    /// ECMA-376 Part 1, §21.2.2.29 (chartSpace).
440    #[cfg(feature = "pml-charts")]
441    pub fn get_chart(
442        &mut self,
443        slide: &Slide,
444        rel_id: &str,
445    ) -> Result<ooxml_dml::types::ChartSpace> {
446        // Resolve the chart part path via slide relationships
447        let slide_rels = self
448            .package
449            .read_part_relationships(slide.slide_path())
450            .map_err(|_| Error::Invalid("Failed to read slide relationships".into()))?;
451
452        let rel = slide_rels
453            .get(rel_id)
454            .ok_or_else(|| Error::Invalid(format!("Chart relationship {} not found", rel_id)))?;
455
456        let chart_path = resolve_path(slide.slide_path(), &rel.target);
457        let chart_xml = self.package.read_part(&chart_path)?;
458
459        parse_chart(&chart_xml)
460    }
461
462    /// Load all four SmartArt parts for a diagram.
463    ///
464    /// Use [`Slide::smartart_rel_ids`] to get the relationship ID sets for all
465    /// SmartArt diagrams on a given slide, then pass each [`DiagramRelIds`] here.
466    ///
467    /// The data model is always loaded (returns an error if it fails). The layout,
468    /// colors, and style parts are loaded gracefully — failures produce `None` rather
469    /// than propagating an error, since callers typically only need the data model.
470    ///
471    /// Requires the `pml-charts` feature.
472    ///
473    /// ECMA-376 Part 1, §21.4 (DrawingML — Diagrams).
474    #[cfg(feature = "pml-charts")]
475    pub fn get_smartart(
476        &mut self,
477        slide: &Slide,
478        rel_ids: &DiagramRelIds,
479    ) -> Result<SmartArtParts> {
480        let slide_rels = self
481            .package
482            .read_part_relationships(slide.slide_path())
483            .map_err(|_| Error::Invalid("Failed to read slide relationships".into()))?;
484
485        // Helper: resolve a relationship ID to a path.
486        let resolve_rel = |rel_id: &str| -> Option<String> {
487            slide_rels
488                .get(rel_id)
489                .map(|r| resolve_path(slide.slide_path(), &r.target))
490        };
491
492        // Data model (dm) — required.
493        let dm_path = resolve_rel(&rel_ids.dm).ok_or_else(|| {
494            Error::Invalid(format!(
495                "SmartArt data model relationship {} not found",
496                rel_ids.dm
497            ))
498        })?;
499        let dm_xml = self.package.read_part(&dm_path)?;
500        let data = parse_data_model(&dm_xml)?;
501
502        // Layout definition (lo) — optional/graceful.
503        let layout = resolve_rel(&rel_ids.lo)
504            .and_then(|path| self.package.read_part(&path).ok())
505            .and_then(|xml| parse_diagram_definition(&xml).ok());
506
507        // Colors (cs) — optional/graceful.
508        let colors = resolve_rel(&rel_ids.cs)
509            .and_then(|path| self.package.read_part(&path).ok())
510            .and_then(|xml| parse_diagram_colors(&xml).ok());
511
512        // Quick style (qs) — optional/graceful.
513        let style = resolve_rel(&rel_ids.qs)
514            .and_then(|path| self.package.read_part(&path).ok())
515            .and_then(|xml| parse_diagram_style(&xml).ok());
516
517        Ok(SmartArtParts {
518            data,
519            layout,
520            colors,
521            style,
522        })
523    }
524}
525
526/// Relationship IDs for a SmartArt diagram's four constituent parts.
527///
528/// SmartArt in a slide is represented by four separate XML parts:
529/// data model, layout definition, quick style, and colors.
530/// The `dgm:relIds` element in the slide XML contains the relationship IDs
531/// pointing to each part.
532///
533/// ECMA-376 Part 1, §21.4.2.20 (relIds).
534#[derive(Debug, Clone)]
535pub struct DiagramRelIds {
536    /// Relationship ID for the diagram data model part (`dgm:dataModel`).
537    pub dm: String,
538    /// Relationship ID for the diagram layout definition part (`dgm:layoutDef`).
539    pub lo: String,
540    /// Relationship ID for the diagram quick style part (`dgm:styleDef`).
541    pub qs: String,
542    /// Relationship ID for the diagram colors part (`dgm:colorsDef`).
543    pub cs: String,
544}
545
546/// The four constituent parts of a SmartArt diagram, loaded and parsed.
547///
548/// Obtained from [`Presentation::get_smartart`].
549#[cfg(feature = "pml-charts")]
550pub struct SmartArtParts {
551    /// The diagram data model (always present — contains the actual content nodes).
552    pub data: ooxml_dml::types::DataModel,
553    /// The layout definition (may be absent or fail to parse gracefully).
554    pub layout: Option<ooxml_dml::types::DiagramDefinition>,
555    /// The color transform definition (may be absent or fail to parse gracefully).
556    pub colors: Option<ooxml_dml::types::DiagramColorTransform>,
557    /// The style definition (may be absent or fail to parse gracefully).
558    pub style: Option<ooxml_dml::types::DiagramStyleDefinition>,
559}
560
561/// A slide in the presentation.
562///
563/// This wraps the generated `types::Slide` and provides additional context
564/// (index, path, notes) that comes from the package structure rather than
565/// the slide XML itself.
566#[derive(Debug, Clone)]
567pub struct Slide {
568    /// The parsed slide content (generated from PML schema).
569    inner: types::Slide,
570    /// Slide index (0-based).
571    index: usize,
572    /// Path to this slide part (for resolving relationships).
573    slide_path: String,
574    /// Speaker notes for this slide (parsed from separate notes part).
575    notes: Option<String>,
576    /// Relationship ID to the slide layout.
577    layout_rel_id: Option<String>,
578    /// Tables extracted from graphic frames.
579    tables: Vec<Table>,
580    /// Relationship IDs for charts embedded in this slide (via `c:chart r:id`).
581    chart_rel_ids: Vec<String>,
582    /// Relationship ID sets for SmartArt diagrams embedded in this slide (via `dgm:relIds`).
583    smartart_rel_ids: Vec<DiagramRelIds>,
584}
585
586/// Slide transition effect.
587///
588/// Represents the animation effect when advancing to this slide.
589#[derive(Debug, Clone, Default)]
590pub struct Transition {
591    /// Transition type (fade, push, wipe, etc.)
592    pub transition_type: Option<TransitionType>,
593    /// Transition speed.
594    pub speed: TransitionSpeed,
595    /// Advance on mouse click.
596    pub advance_on_click: bool,
597    /// Auto-advance time in milliseconds (if set).
598    pub advance_time_ms: Option<u32>,
599}
600
601/// Type of slide transition effect.
602#[derive(Debug, Clone, PartialEq, Eq)]
603pub enum TransitionType {
604    /// Fade transition.
605    Fade,
606    /// Push transition.
607    Push,
608    /// Wipe transition.
609    Wipe,
610    /// Split transition.
611    Split,
612    /// Blinds transition.
613    Blinds,
614    /// Checker transition.
615    Checker,
616    /// Circle transition.
617    Circle,
618    /// Dissolve transition.
619    Dissolve,
620    /// Comb transition.
621    Comb,
622    /// Cover transition.
623    Cover,
624    /// Cut transition.
625    Cut,
626    /// Diamond transition.
627    Diamond,
628    /// Plus transition.
629    Plus,
630    /// Random transition.
631    Random,
632    /// Strips transition.
633    Strips,
634    /// Wedge transition.
635    Wedge,
636    /// Wheel transition.
637    Wheel,
638    /// Zoom transition.
639    Zoom,
640    /// Unknown/unsupported transition type.
641    Other(String),
642}
643
644impl TransitionType {
645    /// Convert to XML element name.
646    pub fn to_xml_value(&self) -> &str {
647        match self {
648            TransitionType::Fade => "fade",
649            TransitionType::Push => "push",
650            TransitionType::Wipe => "wipe",
651            TransitionType::Split => "split",
652            TransitionType::Blinds => "blinds",
653            TransitionType::Checker => "checker",
654            TransitionType::Circle => "circle",
655            TransitionType::Dissolve => "dissolve",
656            TransitionType::Comb => "comb",
657            TransitionType::Cover => "cover",
658            TransitionType::Cut => "cut",
659            TransitionType::Diamond => "diamond",
660            TransitionType::Plus => "plus",
661            TransitionType::Random => "random",
662            TransitionType::Strips => "strips",
663            TransitionType::Wedge => "wedge",
664            TransitionType::Wheel => "wheel",
665            TransitionType::Zoom => "zoom",
666            TransitionType::Other(name) => name.as_str(),
667        }
668    }
669}
670
671/// Speed of the slide transition.
672#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
673pub enum TransitionSpeed {
674    /// Slow transition.
675    Slow,
676    /// Medium transition (default).
677    #[default]
678    Medium,
679    /// Fast transition.
680    Fast,
681}
682
683impl TransitionSpeed {
684    /// Convert to XML attribute value.
685    pub fn to_xml_value(self) -> &'static str {
686        match self {
687            TransitionSpeed::Slow => "slow",
688            TransitionSpeed::Medium => "med",
689            TransitionSpeed::Fast => "fast",
690        }
691    }
692}
693
694impl Slide {
695    /// Get the slide index (0-based).
696    pub fn index(&self) -> usize {
697        self.index
698    }
699
700    /// Get all shapes on the slide.
701    ///
702    /// Returns the generated `types::Shape` structs. Use the `ShapeExt` trait
703    /// for convenient accessor methods like `name()`, `text()`, `paragraphs()`.
704    pub fn shapes(&self) -> &[types::Shape] {
705        self.inner.common_slide_data.shape_tree.shapes()
706    }
707
708    /// Extract all text from the slide.
709    pub fn text(&self) -> String {
710        self.inner.common_slide_data.text()
711    }
712
713    /// Get the speaker notes for this slide.
714    pub fn notes(&self) -> Option<&str> {
715        self.notes.as_deref()
716    }
717
718    /// Check if this slide has speaker notes.
719    pub fn has_notes(&self) -> bool {
720        self.notes.as_ref().is_some_and(|n| !n.is_empty())
721    }
722
723    /// Get all pictures on the slide.
724    ///
725    /// Returns the generated `types::Picture` structs. Use the `PictureExt` trait
726    /// for convenient accessor methods like `name()`, `description()`, `embed_rel_id()`.
727    pub fn pictures(&self) -> &[types::Picture] {
728        self.inner.common_slide_data.pictures()
729    }
730
731    /// Get the path to this slide part (for resolving image relationships).
732    pub(crate) fn slide_path(&self) -> &str {
733        &self.slide_path
734    }
735
736    /// Get the slide transition effect (if any).
737    #[cfg(feature = "pml-transitions")]
738    pub fn transition(&self) -> Option<Transition> {
739        self.inner
740            .transition
741            .as_ref()
742            .map(|t| convert_transition(t))
743    }
744
745    /// Get the slide transition effect (if any).
746    #[cfg(not(feature = "pml-transitions"))]
747    pub fn transition(&self) -> Option<Transition> {
748        None
749    }
750
751    /// Check if this slide has a transition effect.
752    #[cfg(feature = "pml-transitions")]
753    pub fn has_transition(&self) -> bool {
754        self.inner.transition.is_some()
755    }
756
757    /// Check if this slide has a transition effect.
758    #[cfg(not(feature = "pml-transitions"))]
759    pub fn has_transition(&self) -> bool {
760        false
761    }
762
763    /// Get the relationship ID to the slide layout used by this slide.
764    ///
765    /// This can be used to look up the layout in the presentation's slide_layouts.
766    pub fn layout_rel_id(&self) -> Option<&str> {
767        self.layout_rel_id.as_deref()
768    }
769
770    /// Get all hyperlinks from all shapes on this slide.
771    ///
772    /// Returns hyperlinks with their text and relationship ID.
773    pub fn hyperlinks(&self) -> Vec<Hyperlink> {
774        let mut links = Vec::new();
775        for shape in self.shapes() {
776            if let Some(text_body) = shape.text_body() {
777                for para in text_body.paragraphs() {
778                    for run in para.runs() {
779                        if let Some(rel_id) = run.hyperlink_rel_id() {
780                            links.push(Hyperlink {
781                                text: run.text().to_string(),
782                                rel_id: rel_id.to_string(),
783                            });
784                        }
785                    }
786                }
787            }
788        }
789        links
790    }
791
792    /// Check if this slide contains any hyperlinks.
793    pub fn has_hyperlinks(&self) -> bool {
794        self.shapes().iter().any(|s| {
795            s.text_body().is_some_and(|tb| {
796                tb.paragraphs()
797                    .iter()
798                    .any(|p| p.runs().iter().any(|r| r.has_hyperlink()))
799            })
800        })
801    }
802
803    /// Get all tables on the slide.
804    pub fn tables(&self) -> &[Table] {
805        &self.tables
806    }
807
808    /// Check if this slide contains any tables.
809    pub fn has_tables(&self) -> bool {
810        !self.tables.is_empty()
811    }
812
813    /// Get the number of tables on this slide.
814    pub fn table_count(&self) -> usize {
815        self.tables.len()
816    }
817
818    /// Get the underlying generated slide type.
819    ///
820    /// Use this for full access to all parsed fields.
821    pub fn inner(&self) -> &types::Slide {
822        &self.inner
823    }
824
825    /// Get a table by index (0-based).
826    pub fn table(&self, index: usize) -> Option<&Table> {
827        self.tables.get(index)
828    }
829
830    /// Get the relationship IDs of all charts embedded in this slide.
831    ///
832    /// Each ID can be passed to [`Presentation::get_chart`] to load and parse
833    /// the corresponding `ChartSpace`.
834    pub fn chart_rel_ids(&self) -> &[String] {
835        &self.chart_rel_ids
836    }
837
838    /// Get the relationship ID sets for all SmartArt diagrams on this slide.
839    ///
840    /// Each entry can be passed to [`Presentation::get_smartart`] to load and
841    /// parse all four SmartArt parts.
842    pub fn smartart_rel_ids(&self) -> &[DiagramRelIds] {
843        &self.smartart_rel_ids
844    }
845}
846
847/// A table on a slide.
848///
849/// Represents a table embedded via DrawingML `a:tbl` element inside a `p:graphicFrame`.
850/// This is a thin wrapper around [`ooxml_dml::types::CTTable`] that adds the frame name.
851///
852/// Use the [`ooxml_dml::TableExt`], [`ooxml_dml::TableRowExt`], and [`ooxml_dml::TableCellExt`]
853/// traits for convenient access to rows, cells, and text content.
854#[derive(Debug, Clone)]
855pub struct Table {
856    /// Table name (from graphic frame's cNvPr).
857    name: Option<String>,
858    /// The underlying DrawingML table.
859    inner: ooxml_dml::types::CTTable,
860}
861
862impl Table {
863    /// Get the table name (from the containing graphic frame).
864    pub fn name(&self) -> Option<&str> {
865        self.name.as_deref()
866    }
867
868    /// Get a reference to the underlying DrawingML table.
869    pub fn inner(&self) -> &ooxml_dml::types::CTTable {
870        &self.inner
871    }
872
873    /// Get a mutable reference to the underlying DrawingML table.
874    pub fn inner_mut(&mut self) -> &mut ooxml_dml::types::CTTable {
875        &mut self.inner
876    }
877
878    /// Consume the wrapper and return the underlying DrawingML table.
879    pub fn into_inner(self) -> ooxml_dml::types::CTTable {
880        self.inner
881    }
882
883    /// Get all rows in the table.
884    pub fn rows(&self) -> &[ooxml_dml::types::CTTableRow] {
885        use ooxml_dml::TableExt;
886        self.inner.rows()
887    }
888
889    /// Get the number of rows.
890    pub fn row_count(&self) -> usize {
891        use ooxml_dml::TableExt;
892        self.inner.row_count()
893    }
894
895    /// Get the number of columns.
896    pub fn col_count(&self) -> usize {
897        use ooxml_dml::TableExt;
898        self.inner.col_count()
899    }
900
901    /// Get a cell by row and column index (0-based).
902    pub fn cell(&self, row: usize, col: usize) -> Option<&ooxml_dml::types::CTTableCell> {
903        use ooxml_dml::TableExt;
904        self.inner.cell(row, col)
905    }
906
907    /// Get all cell text as a 2D vector.
908    pub fn to_text_grid(&self) -> Vec<Vec<String>> {
909        use ooxml_dml::TableExt;
910        self.inner.to_text_grid()
911    }
912
913    /// Get plain text representation (tab-separated values).
914    pub fn text(&self) -> String {
915        use ooxml_dml::TableExt;
916        self.inner.text()
917    }
918}
919
920/// A hyperlink extracted from a text run.
921#[derive(Debug, Clone)]
922pub struct Hyperlink {
923    /// The text that is hyperlinked.
924    pub text: String,
925    /// The relationship ID (use with slide relationships to get the URL).
926    pub rel_id: String,
927}
928
929// ============================================================================
930// Parsing
931// ============================================================================
932
933/// Parse presentation.xml to get slide relationship IDs in order.
934fn parse_presentation_slides(xml: &[u8]) -> Result<Vec<String>> {
935    let mut reader = Reader::from_reader(Cursor::new(xml));
936    let mut buf = Vec::new();
937    let mut slide_ids = Vec::new();
938    let mut in_sld_id_lst = false;
939
940    loop {
941        match reader.read_event_into(&mut buf) {
942            Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
943                let name = e.name();
944                let name = name.as_ref();
945                if name == b"p:sldIdLst" {
946                    in_sld_id_lst = true;
947                } else if in_sld_id_lst && name == b"p:sldId" {
948                    // Get r:id attribute
949                    for attr in e.attributes().filter_map(|a| a.ok()) {
950                        if attr.key.as_ref() == b"r:id" {
951                            slide_ids.push(String::from_utf8_lossy(&attr.value).into_owned());
952                        }
953                    }
954                }
955            }
956            Ok(Event::End(e)) => {
957                let name = e.name();
958                if name.as_ref() == b"p:sldIdLst" {
959                    in_sld_id_lst = false;
960                }
961            }
962            Ok(Event::Eof) => break,
963            Err(e) => return Err(Error::Xml(e)),
964            _ => {}
965        }
966        buf.clear();
967    }
968
969    Ok(slide_ids)
970}
971
972/// Parse a notes slide XML file and extract the text content.
973fn parse_notes_slide(xml: &[u8]) -> Option<String> {
974    use ooxml_dml::parsers::FromXml as DmlFromXml;
975    use ooxml_dml::types::TextBody;
976
977    let mut reader = Reader::from_reader(Cursor::new(xml));
978    let mut buf = Vec::new();
979    let mut all_text = Vec::new();
980
981    loop {
982        match reader.read_event_into(&mut buf) {
983            Ok(Event::Start(e)) => {
984                let local = e.local_name();
985                let local = local.as_ref();
986                // Notes text is in p:txBody elements - use generated parser
987                if local == b"txBody"
988                    && let Ok(text_body) = TextBody::from_xml(&mut reader, &e, false)
989                {
990                    for para in &text_body.p {
991                        let text = para.text();
992                        if !text.is_empty() {
993                            all_text.push(text);
994                        }
995                    }
996                }
997            }
998            Ok(Event::Eof) => break,
999            Err(_) => break,
1000            _ => {}
1001        }
1002        buf.clear();
1003    }
1004
1005    if all_text.is_empty() {
1006        None
1007    } else {
1008        Some(all_text.join("\n"))
1009    }
1010}
1011
1012// ============================================================================
1013// Utilities
1014// ============================================================================
1015
1016/// Resolve a relative path against a base path, normalizing `..` segments.
1017fn resolve_path(base: &str, target: &str) -> String {
1018    if target.starts_with('/') {
1019        // Absolute target — return as-is (preserve leading slash per OPC spec).
1020        return target.to_string();
1021    }
1022
1023    // Get the directory of the base path
1024    let base_dir = if let Some(idx) = base.rfind('/') {
1025        &base[..idx + 1]
1026    } else {
1027        ""
1028    };
1029
1030    normalize_path(&format!("{}{}", base_dir, target))
1031}
1032
1033/// Normalize a path by resolving `..` and `.` segments.
1034fn normalize_path(path: &str) -> String {
1035    let mut parts: Vec<&str> = Vec::new();
1036    for segment in path.split('/') {
1037        match segment {
1038            ".." => {
1039                parts.pop();
1040            }
1041            "." | "" => {}
1042            _ => parts.push(segment),
1043        }
1044    }
1045    parts.join("/")
1046}
1047
1048/// Determine content type from file path extension.
1049fn content_type_from_path(path: &str) -> String {
1050    let ext = path.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
1051
1052    match ext.as_str() {
1053        "png" => "image/png",
1054        "jpg" | "jpeg" => "image/jpeg",
1055        "gif" => "image/gif",
1056        "bmp" => "image/bmp",
1057        "tiff" | "tif" => "image/tiff",
1058        "webp" => "image/webp",
1059        "svg" => "image/svg+xml",
1060        "emf" => "image/x-emf",
1061        "wmf" => "image/x-wmf",
1062        _ => "application/octet-stream",
1063    }
1064    .to_string()
1065}
1066
1067/// Parse a slide master XML file.
1068fn parse_slide_master(xml: &[u8], path: &str) -> SlideMaster {
1069    let mut reader = Reader::from_reader(Cursor::new(xml));
1070    let mut buf = Vec::new();
1071    let mut name = None;
1072    let mut color_scheme = None;
1073    let mut background_color = None;
1074    let mut layout_ids = Vec::new();
1075
1076    loop {
1077        match reader.read_event_into(&mut buf) {
1078            Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
1079                let tag = e.name();
1080                let tag = tag.as_ref();
1081                match tag {
1082                    b"p:cSld" => {
1083                        for attr in e.attributes().filter_map(|a| a.ok()) {
1084                            if attr.key.as_ref() == b"name" {
1085                                name = Some(String::from_utf8_lossy(&attr.value).into_owned());
1086                            }
1087                        }
1088                    }
1089                    b"p:sldLayoutId" => {
1090                        for attr in e.attributes().filter_map(|a| a.ok()) {
1091                            if attr.key.as_ref() == b"r:id" {
1092                                layout_ids.push(String::from_utf8_lossy(&attr.value).into_owned());
1093                            }
1094                        }
1095                    }
1096                    b"a:clrScheme" => {
1097                        for attr in e.attributes().filter_map(|a| a.ok()) {
1098                            if attr.key.as_ref() == b"name" {
1099                                color_scheme =
1100                                    Some(String::from_utf8_lossy(&attr.value).into_owned());
1101                            }
1102                        }
1103                    }
1104                    b"a:srgbClr" => {
1105                        if background_color.is_none() {
1106                            for attr in e.attributes().filter_map(|a| a.ok()) {
1107                                if attr.key.as_ref() == b"val" {
1108                                    background_color =
1109                                        Some(String::from_utf8_lossy(&attr.value).into_owned());
1110                                }
1111                            }
1112                        }
1113                    }
1114                    _ => {}
1115                }
1116            }
1117            Ok(Event::Eof) => break,
1118            _ => {}
1119        }
1120        buf.clear();
1121    }
1122
1123    SlideMaster {
1124        path: path.to_string(),
1125        name,
1126        layout_ids,
1127        color_scheme,
1128        background_color,
1129    }
1130}
1131
1132/// Parse a slide layout XML file.
1133fn parse_slide_layout(xml: &[u8], path: &str, master_rel_id: Option<String>) -> SlideLayout {
1134    let mut reader = Reader::from_reader(Cursor::new(xml));
1135    let mut buf = Vec::new();
1136    let mut name = None;
1137    let mut layout_type = SlideLayoutType::Unknown;
1138    let mut match_name = false;
1139    let mut show_master_shapes = true;
1140
1141    loop {
1142        match reader.read_event_into(&mut buf) {
1143            Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
1144                let tag = e.name();
1145                let tag = tag.as_ref();
1146                match tag {
1147                    b"p:sldLayout" => {
1148                        for attr in e.attributes().filter_map(|a| a.ok()) {
1149                            let val = String::from_utf8_lossy(&attr.value);
1150                            match attr.key.as_ref() {
1151                                b"type" => layout_type = SlideLayoutType::parse(&val),
1152                                b"matchingName" => match_name = val == "1" || val == "true",
1153                                b"showMasterSp" => {
1154                                    show_master_shapes = val != "0" && val != "false"
1155                                }
1156                                _ => {}
1157                            }
1158                        }
1159                    }
1160                    b"p:cSld" => {
1161                        for attr in e.attributes().filter_map(|a| a.ok()) {
1162                            if attr.key.as_ref() == b"name" {
1163                                name = Some(String::from_utf8_lossy(&attr.value).into_owned());
1164                            }
1165                        }
1166                    }
1167                    _ => {}
1168                }
1169            }
1170            Ok(Event::Eof) => break,
1171            _ => {}
1172        }
1173        buf.clear();
1174    }
1175
1176    SlideLayout {
1177        path: path.to_string(),
1178        name,
1179        layout_type,
1180        master_rel_id,
1181        match_name,
1182        show_master_shapes,
1183    }
1184}
1185
1186// ============================================================================
1187// Generated parser helpers
1188// ============================================================================
1189
1190/// Parse a slide using the generated FromXml parser.
1191fn parse_slide_xml(xml: &[u8]) -> Result<types::Slide> {
1192    let mut reader = Reader::from_reader(Cursor::new(xml));
1193    let mut buf = Vec::new();
1194
1195    // Find the root p:sld element and parse it
1196    loop {
1197        match reader.read_event_into(&mut buf) {
1198            Ok(Event::Start(e)) => {
1199                let local_name = e.local_name();
1200                if local_name.as_ref() == b"sld" {
1201                    return types::Slide::from_xml(&mut reader, &e, false)
1202                        .map_err(|e| Error::Invalid(format!("Failed to parse slide: {}", e)));
1203                }
1204            }
1205            Ok(Event::Eof) => break,
1206            Err(e) => return Err(Error::Xml(e)),
1207            _ => {}
1208        }
1209        buf.clear();
1210    }
1211
1212    Err(Error::Invalid("No p:sld element found in slide XML".into()))
1213}
1214
1215/// Convert a generated SlideTransition to the handwritten Transition type.
1216#[cfg(feature = "pml-transitions")]
1217fn convert_transition(trans: &types::SlideTransition) -> Transition {
1218    // Get speed
1219    let speed = trans
1220        .spd
1221        .as_ref()
1222        .map_or(TransitionSpeed::Medium, |s| match s {
1223            types::STTransitionSpeed::Slow => TransitionSpeed::Slow,
1224            types::STTransitionSpeed::Med => TransitionSpeed::Medium,
1225            types::STTransitionSpeed::Fast => TransitionSpeed::Fast,
1226        });
1227
1228    // Determine transition type by checking which field is Some
1229    let transition_type = if trans.fade.is_some() {
1230        Some(TransitionType::Fade)
1231    } else if trans.push.is_some() {
1232        Some(TransitionType::Push)
1233    } else if trans.wipe.is_some() {
1234        Some(TransitionType::Wipe)
1235    } else if trans.split.is_some() {
1236        Some(TransitionType::Split)
1237    } else if trans.blinds.is_some() {
1238        Some(TransitionType::Blinds)
1239    } else if trans.checker.is_some() {
1240        Some(TransitionType::Checker)
1241    } else if trans.circle.is_some() {
1242        Some(TransitionType::Circle)
1243    } else if trans.dissolve.is_some() {
1244        Some(TransitionType::Dissolve)
1245    } else if trans.comb.is_some() {
1246        Some(TransitionType::Comb)
1247    } else if trans.cover.is_some() {
1248        Some(TransitionType::Cover)
1249    } else if trans.cut.is_some() {
1250        Some(TransitionType::Cut)
1251    } else if trans.diamond.is_some() {
1252        Some(TransitionType::Diamond)
1253    } else if trans.plus.is_some() {
1254        Some(TransitionType::Plus)
1255    } else if trans.random.is_some() {
1256        Some(TransitionType::Random)
1257    } else if trans.strips.is_some() {
1258        Some(TransitionType::Strips)
1259    } else if trans.wedge.is_some() {
1260        Some(TransitionType::Wedge)
1261    } else if trans.wheel.is_some() {
1262        Some(TransitionType::Wheel)
1263    } else if trans.zoom.is_some() {
1264        Some(TransitionType::Zoom)
1265    } else {
1266        None
1267    };
1268
1269    Transition {
1270        transition_type,
1271        speed,
1272        advance_on_click: trans.adv_click.unwrap_or(true),
1273        advance_time_ms: trans.adv_tm,
1274    }
1275}
1276
1277/// Extract tables from graphic frames in a slide.
1278///
1279/// Tables are embedded in `p:graphicFrame` elements containing DrawingML `a:tbl`.
1280/// The graphic frame structure is: p:graphicFrame/a:graphic/a:graphicData/a:tbl
1281#[cfg(feature = "extra-children")]
1282fn extract_tables_from_slide(slide: &types::Slide) -> Vec<Table> {
1283    use crate::ext::GraphicalObjectFrameExt;
1284
1285    let mut tables = Vec::new();
1286
1287    // Tables are in graphic frames in the shape tree
1288    for frame in &slide.common_slide_data.shape_tree.graphic_frame {
1289        // Get the frame name for the table
1290        let frame_name = Some(frame.name().to_string()).filter(|s| !s.is_empty());
1291
1292        // Look through extra_children for the a:graphic element
1293        for node in &frame.extra_children {
1294            if let Some(table) = find_table_in_node(&node.node, frame_name.clone()) {
1295                tables.push(table);
1296            }
1297        }
1298    }
1299
1300    tables
1301}
1302
1303/// Stub for when extra-children feature is disabled.
1304#[cfg(not(feature = "extra-children"))]
1305fn extract_tables_from_slide(_slide: &types::Slide) -> Vec<Table> {
1306    Vec::new()
1307}
1308
1309/// Recursively search for a table element in an XML node tree.
1310#[cfg(feature = "extra-children")]
1311fn find_table_in_node(node: &ooxml_xml::RawXmlNode, frame_name: Option<String>) -> Option<Table> {
1312    use ooxml_xml::RawXmlNode;
1313
1314    match node {
1315        RawXmlNode::Element(elem) => {
1316            // Check if this is a table element (a:tbl or just tbl)
1317            let local_name = elem.name.split(':').next_back().unwrap_or(&elem.name);
1318            if local_name == "tbl" {
1319                return parse_table_element(elem, frame_name);
1320            }
1321
1322            // Recursively search children
1323            for child in &elem.children {
1324                if let Some(table) = find_table_in_node(child, frame_name.clone()) {
1325                    return Some(table);
1326                }
1327            }
1328            None
1329        }
1330        _ => None,
1331    }
1332}
1333
1334/// Parse a table from a RawXmlElement representing a:tbl.
1335#[cfg(feature = "extra-children")]
1336fn parse_table_element(
1337    elem: &ooxml_xml::RawXmlElement,
1338    frame_name: Option<String>,
1339) -> Option<Table> {
1340    use ooxml_dml::types::CTTable;
1341
1342    // Use the RawXmlElement::parse_as helper to convert to typed struct
1343    elem.parse_as::<CTTable>()
1344        .ok()
1345        .map(|ct_table| wrap_ct_table(ct_table, frame_name))
1346}
1347
1348/// Wrap a DML CTTable in our Table type with the frame name.
1349#[cfg(feature = "extra-children")]
1350fn wrap_ct_table(ct_table: ooxml_dml::types::CTTable, name: Option<String>) -> Table {
1351    Table {
1352        name,
1353        inner: ct_table,
1354    }
1355}
1356
1357// ============================================================================
1358// Chart and SmartArt extraction
1359// ============================================================================
1360
1361/// Extract chart rel IDs and SmartArt rel ID sets from graphic frames in a slide.
1362///
1363/// Charts live in `p:graphicFrame/a:graphic/a:graphicData[@uri=".../chart"]/c:chart[@r:id="..."]`.
1364/// SmartArt lives in `p:graphicFrame/a:graphic/a:graphicData[@uri=".../diagram"]/dgm:relIds`.
1365#[cfg(feature = "extra-children")]
1366fn extract_charts_and_smartart_from_slide(
1367    slide: &types::Slide,
1368) -> (Vec<String>, Vec<DiagramRelIds>) {
1369    let mut chart_ids = Vec::new();
1370    let mut smartart_ids = Vec::new();
1371
1372    for frame in &slide.common_slide_data.shape_tree.graphic_frame {
1373        for node in &frame.extra_children {
1374            collect_chart_and_smartart_ids(&node.node, &mut chart_ids, &mut smartart_ids);
1375        }
1376    }
1377
1378    (chart_ids, smartart_ids)
1379}
1380
1381/// Stub when extra-children feature is disabled.
1382#[cfg(not(feature = "extra-children"))]
1383fn extract_charts_and_smartart_from_slide(
1384    _slide: &types::Slide,
1385) -> (Vec<String>, Vec<DiagramRelIds>) {
1386    (Vec::new(), Vec::new())
1387}
1388
1389/// Recursively walk a raw XML node tree collecting chart `r:id` values and
1390/// SmartArt `dgm:relIds` attribute sets.
1391#[cfg(feature = "extra-children")]
1392fn collect_chart_and_smartart_ids(
1393    node: &ooxml_xml::RawXmlNode,
1394    chart_ids: &mut Vec<String>,
1395    smartart_ids: &mut Vec<DiagramRelIds>,
1396) {
1397    use ooxml_xml::RawXmlNode;
1398
1399    if let RawXmlNode::Element(elem) = node {
1400        let local = elem.name.split(':').next_back().unwrap_or(&elem.name);
1401        match local {
1402            // <c:chart r:id="rIdN"/> — chart reference
1403            "chart" => {
1404                for (attr_name, attr_val) in &elem.attributes {
1405                    let attr_local = attr_name.split(':').next_back().unwrap_or(attr_name);
1406                    if attr_local == "id" {
1407                        chart_ids.push(attr_val.clone());
1408                    }
1409                }
1410            }
1411            // <dgm:relIds r:dm="..." r:lo="..." r:qs="..." r:cs="..."/>
1412            "relIds" => {
1413                let mut dm = None;
1414                let mut lo = None;
1415                let mut qs = None;
1416                let mut cs = None;
1417                for (attr_name, attr_val) in &elem.attributes {
1418                    let attr_local = attr_name.split(':').next_back().unwrap_or(attr_name);
1419                    match attr_local {
1420                        "dm" => dm = Some(attr_val.clone()),
1421                        "lo" => lo = Some(attr_val.clone()),
1422                        "qs" => qs = Some(attr_val.clone()),
1423                        "cs" => cs = Some(attr_val.clone()),
1424                        _ => {}
1425                    }
1426                }
1427                if let (Some(dm), Some(lo), Some(qs), Some(cs)) = (dm, lo, qs, cs) {
1428                    smartart_ids.push(DiagramRelIds { dm, lo, qs, cs });
1429                }
1430            }
1431            // Any other element — recurse into children
1432            _ => {
1433                for child in &elem.children {
1434                    collect_chart_and_smartart_ids(child, chart_ids, smartart_ids);
1435                }
1436            }
1437        }
1438    }
1439}
1440
1441// ============================================================================
1442// Chart and SmartArt part parsers
1443// ============================================================================
1444
1445/// Parse a chart XML part into a `ChartSpace`.
1446///
1447/// Requires the `pml-charts` feature (which enables `ooxml-dml/dml-charts`).
1448/// ECMA-376 Part 1, §21.2.2.29 (CT_ChartSpace).
1449#[cfg(feature = "pml-charts")]
1450fn parse_chart(xml: &[u8]) -> Result<ooxml_dml::types::ChartSpace> {
1451    use ooxml_dml::parsers::FromXml as DmlFromXml;
1452    let mut reader = Reader::from_reader(Cursor::new(xml));
1453    let mut buf = Vec::new();
1454
1455    loop {
1456        match reader.read_event_into(&mut buf) {
1457            Ok(Event::Start(e)) => {
1458                return ooxml_dml::types::ChartSpace::from_xml(&mut reader, &e, false)
1459                    .map_err(|e| Error::Invalid(format!("Failed to parse chartSpace: {}", e)));
1460            }
1461            Ok(Event::Empty(e)) => {
1462                return ooxml_dml::types::ChartSpace::from_xml(&mut reader, &e, true)
1463                    .map_err(|e| Error::Invalid(format!("Failed to parse chartSpace: {}", e)));
1464            }
1465            Ok(Event::Eof) => break,
1466            Err(e) => return Err(Error::Xml(e)),
1467            _ => {}
1468        }
1469        buf.clear();
1470    }
1471    Err(Error::Invalid("No chartSpace element found".into()))
1472}
1473
1474/// Parse a SmartArt data model XML part into a `DataModel`.
1475///
1476/// ECMA-376 Part 1, §21.4.2.8 (CT_DataModel).
1477#[cfg(feature = "pml-charts")]
1478fn parse_data_model(xml: &[u8]) -> Result<ooxml_dml::types::DataModel> {
1479    use ooxml_dml::parsers::FromXml as DmlFromXml;
1480    let mut reader = Reader::from_reader(Cursor::new(xml));
1481    let mut buf = Vec::new();
1482
1483    loop {
1484        match reader.read_event_into(&mut buf) {
1485            Ok(Event::Start(e)) => {
1486                return ooxml_dml::types::DataModel::from_xml(&mut reader, &e, false)
1487                    .map_err(|e| Error::Invalid(format!("Failed to parse dataModel: {}", e)));
1488            }
1489            Ok(Event::Empty(e)) => {
1490                return ooxml_dml::types::DataModel::from_xml(&mut reader, &e, true)
1491                    .map_err(|e| Error::Invalid(format!("Failed to parse dataModel: {}", e)));
1492            }
1493            Ok(Event::Eof) => break,
1494            Err(e) => return Err(Error::Xml(e)),
1495            _ => {}
1496        }
1497        buf.clear();
1498    }
1499    Err(Error::Invalid("No dataModel element found".into()))
1500}
1501
1502/// Parse a SmartArt layout definition XML part into a `DiagramDefinition`.
1503///
1504/// ECMA-376 Part 1, §21.4.3 (layoutDef).
1505#[cfg(feature = "pml-charts")]
1506fn parse_diagram_definition(xml: &[u8]) -> Result<ooxml_dml::types::DiagramDefinition> {
1507    use ooxml_dml::parsers::FromXml as DmlFromXml;
1508    let mut reader = Reader::from_reader(Cursor::new(xml));
1509    let mut buf = Vec::new();
1510
1511    loop {
1512        match reader.read_event_into(&mut buf) {
1513            Ok(Event::Start(e)) => {
1514                return ooxml_dml::types::DiagramDefinition::from_xml(&mut reader, &e, false)
1515                    .map_err(|e| Error::Invalid(format!("Failed to parse layoutDef: {}", e)));
1516            }
1517            Ok(Event::Empty(e)) => {
1518                return ooxml_dml::types::DiagramDefinition::from_xml(&mut reader, &e, true)
1519                    .map_err(|e| Error::Invalid(format!("Failed to parse layoutDef: {}", e)));
1520            }
1521            Ok(Event::Eof) => break,
1522            Err(e) => return Err(Error::Xml(e)),
1523            _ => {}
1524        }
1525        buf.clear();
1526    }
1527    Err(Error::Invalid("No layoutDef element found".into()))
1528}
1529
1530/// Parse a SmartArt colors XML part into a `DiagramColorTransform`.
1531///
1532/// ECMA-376 Part 1, §21.4.4 (colorsDef).
1533#[cfg(feature = "pml-charts")]
1534fn parse_diagram_colors(xml: &[u8]) -> Result<ooxml_dml::types::DiagramColorTransform> {
1535    use ooxml_dml::parsers::FromXml as DmlFromXml;
1536    let mut reader = Reader::from_reader(Cursor::new(xml));
1537    let mut buf = Vec::new();
1538
1539    loop {
1540        match reader.read_event_into(&mut buf) {
1541            Ok(Event::Start(e)) => {
1542                return ooxml_dml::types::DiagramColorTransform::from_xml(&mut reader, &e, false)
1543                    .map_err(|e| Error::Invalid(format!("Failed to parse colorsDef: {}", e)));
1544            }
1545            Ok(Event::Empty(e)) => {
1546                return ooxml_dml::types::DiagramColorTransform::from_xml(&mut reader, &e, true)
1547                    .map_err(|e| Error::Invalid(format!("Failed to parse colorsDef: {}", e)));
1548            }
1549            Ok(Event::Eof) => break,
1550            Err(e) => return Err(Error::Xml(e)),
1551            _ => {}
1552        }
1553        buf.clear();
1554    }
1555    Err(Error::Invalid("No colorsDef element found".into()))
1556}
1557
1558/// Parse a SmartArt style definition XML part into a `DiagramStyleDefinition`.
1559///
1560/// ECMA-376 Part 1, §21.4.5 (styleDef).
1561#[cfg(feature = "pml-charts")]
1562fn parse_diagram_style(xml: &[u8]) -> Result<ooxml_dml::types::DiagramStyleDefinition> {
1563    use ooxml_dml::parsers::FromXml as DmlFromXml;
1564    let mut reader = Reader::from_reader(Cursor::new(xml));
1565    let mut buf = Vec::new();
1566
1567    loop {
1568        match reader.read_event_into(&mut buf) {
1569            Ok(Event::Start(e)) => {
1570                return ooxml_dml::types::DiagramStyleDefinition::from_xml(&mut reader, &e, false)
1571                    .map_err(|e| Error::Invalid(format!("Failed to parse styleDef: {}", e)));
1572            }
1573            Ok(Event::Empty(e)) => {
1574                return ooxml_dml::types::DiagramStyleDefinition::from_xml(&mut reader, &e, true)
1575                    .map_err(|e| Error::Invalid(format!("Failed to parse styleDef: {}", e)));
1576            }
1577            Ok(Event::Eof) => break,
1578            Err(e) => return Err(Error::Xml(e)),
1579            _ => {}
1580        }
1581        buf.clear();
1582    }
1583    Err(Error::Invalid("No styleDef element found".into()))
1584}
1585
1586#[cfg(test)]
1587mod tests {
1588    use super::*;
1589
1590    #[test]
1591    fn test_resolve_path() {
1592        assert_eq!(
1593            resolve_path("ppt/presentation.xml", "slides/slide1.xml"),
1594            "ppt/slides/slide1.xml"
1595        );
1596        assert_eq!(
1597            resolve_path("ppt/presentation.xml", "/ppt/slides/slide1.xml"),
1598            "/ppt/slides/slide1.xml"
1599        );
1600        // Relative path with parent navigation (../ used in master → layout refs)
1601        assert_eq!(
1602            resolve_path(
1603                "ppt/slideMasters/slideMaster1.xml",
1604                "../slideLayouts/slideLayout1.xml"
1605            ),
1606            "ppt/slideLayouts/slideLayout1.xml"
1607        );
1608        assert_eq!(
1609            resolve_path(
1610                "ppt/slideLayouts/slideLayout1.xml",
1611                "../slideMasters/slideMaster1.xml"
1612            ),
1613            "ppt/slideMasters/slideMaster1.xml"
1614        );
1615    }
1616
1617    /// Build a `RawXmlElement` by parsing a simple XML string using quick-xml.
1618    ///
1619    /// This is a test helper only — it handles single top-level elements without
1620    /// nested namespaced children by walking quick-xml events.
1621    #[cfg(feature = "extra-children")]
1622    fn build_raw_element_from_xml(xml: &str) -> ooxml_xml::RawXmlElement {
1623        use ooxml_xml::{RawXmlElement, RawXmlNode};
1624
1625        let mut reader = Reader::from_reader(Cursor::new(xml.as_bytes()));
1626        reader.config_mut().trim_text(false);
1627        let mut buf = Vec::new();
1628        let mut stack: Vec<RawXmlElement> = Vec::new();
1629        let mut root: Option<RawXmlElement> = None;
1630
1631        loop {
1632            match reader.read_event_into(&mut buf) {
1633                Ok(Event::Start(e)) => {
1634                    let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
1635                    let attrs: Vec<(String, String)> = e
1636                        .attributes()
1637                        .filter_map(|a| a.ok())
1638                        .map(|attr| {
1639                            let k = String::from_utf8_lossy(attr.key.as_ref()).into_owned();
1640                            let v = String::from_utf8_lossy(&attr.value).into_owned();
1641                            (k, v)
1642                        })
1643                        .collect();
1644                    stack.push(RawXmlElement {
1645                        name,
1646                        attributes: attrs,
1647                        children: Vec::new(),
1648                        self_closing: false,
1649                    });
1650                }
1651                Ok(Event::Empty(e)) => {
1652                    let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
1653                    let attrs: Vec<(String, String)> = e
1654                        .attributes()
1655                        .filter_map(|a| a.ok())
1656                        .map(|attr| {
1657                            let k = String::from_utf8_lossy(attr.key.as_ref()).into_owned();
1658                            let v = String::from_utf8_lossy(&attr.value).into_owned();
1659                            (k, v)
1660                        })
1661                        .collect();
1662                    let elem = RawXmlElement {
1663                        name,
1664                        attributes: attrs,
1665                        children: Vec::new(),
1666                        self_closing: true,
1667                    };
1668                    if let Some(parent) = stack.last_mut() {
1669                        parent.children.push(RawXmlNode::Element(elem));
1670                    } else {
1671                        root = Some(elem);
1672                    }
1673                }
1674                Ok(Event::End(_)) => {
1675                    if let Some(finished) = stack.pop() {
1676                        if let Some(parent) = stack.last_mut() {
1677                            parent.children.push(RawXmlNode::Element(finished));
1678                        } else {
1679                            root = Some(finished);
1680                        }
1681                    }
1682                }
1683                Ok(Event::Eof) => break,
1684                _ => {}
1685            }
1686            buf.clear();
1687        }
1688
1689        root.expect("test XML must have a root element")
1690    }
1691
1692    /// Verify that chart r:id values are extracted from an XML node tree containing
1693    /// `<a:graphic>/<a:graphicData>/<c:chart r:id="rId5"/>`.
1694    #[cfg(feature = "extra-children")]
1695    #[test]
1696    fn test_extract_chart_rel_id() {
1697        use ooxml_xml::RawXmlNode;
1698
1699        let xml = r#"<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart r:id="rId5"/></a:graphicData></a:graphic>"#;
1700        let elem = build_raw_element_from_xml(xml);
1701        let node = RawXmlNode::Element(elem);
1702
1703        let mut chart_ids = Vec::new();
1704        let mut smartart_ids = Vec::new();
1705        collect_chart_and_smartart_ids(&node, &mut chart_ids, &mut smartart_ids);
1706
1707        assert_eq!(chart_ids, vec!["rId5".to_string()]);
1708        assert!(smartart_ids.is_empty());
1709    }
1710
1711    /// Verify that SmartArt dgm:relIds attribute sets are extracted from an XML
1712    /// node tree containing `<a:graphic>/<a:graphicData>/<dgm:relIds r:dm="..." .../>`.
1713    #[cfg(feature = "extra-children")]
1714    #[test]
1715    fn test_extract_smartart_rel_ids() {
1716        use ooxml_xml::RawXmlNode;
1717
1718        let xml = r#"<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/diagram"><dgm:relIds r:dm="rId4" r:lo="rId5" r:qs="rId6" r:cs="rId7"/></a:graphicData></a:graphic>"#;
1719        let elem = build_raw_element_from_xml(xml);
1720        let node = RawXmlNode::Element(elem);
1721
1722        let mut chart_ids = Vec::new();
1723        let mut smartart_ids = Vec::new();
1724        collect_chart_and_smartart_ids(&node, &mut chart_ids, &mut smartart_ids);
1725
1726        assert!(chart_ids.is_empty());
1727        assert_eq!(smartart_ids.len(), 1);
1728        assert_eq!(smartart_ids[0].dm, "rId4");
1729        assert_eq!(smartart_ids[0].lo, "rId5");
1730        assert_eq!(smartart_ids[0].qs, "rId6");
1731        assert_eq!(smartart_ids[0].cs, "rId7");
1732    }
1733}