Skip to main content

ooxml_pml/
ext.rs

1//! Extension traits for generated PML types.
2//!
3//! These traits provide convenient accessor methods for the generated types,
4//! similar to the handwritten API but working with the schema-generated structures.
5
6use crate::generated::*;
7use ooxml_dml::ext::{TextBodyExt, TextParagraphExt};
8use ooxml_dml::types::TextParagraph;
9
10/// Extension trait for Shape (p:sp element).
11pub trait ShapeExt {
12    /// Get the shape name from cNvPr.
13    fn name(&self) -> &str;
14
15    /// Get the shape description/alt text from cNvPr.
16    fn description(&self) -> Option<&str>;
17
18    /// Get the text body if present.
19    fn text_body(&self) -> Option<&ooxml_dml::types::TextBody>;
20
21    /// Get the text paragraphs from the text body.
22    fn paragraphs(&self) -> &[TextParagraph];
23
24    /// Get all text content joined by newlines.
25    fn text(&self) -> Option<String>;
26
27    /// Check if the shape has text content.
28    fn has_text(&self) -> bool;
29
30    /// Get the drawing element ID (`cNvPr@id`).
31    fn shape_id(&self) -> u32;
32
33    /// Check if this shape is a placeholder (has a `ph` element).
34    fn is_placeholder(&self) -> bool;
35
36    /// Get the placeholder type (title, body, etc.) if this is a placeholder.
37    fn placeholder_type(&self) -> Option<&STPlaceholderType>;
38
39    /// Get the placeholder index if this is a placeholder.
40    fn placeholder_index(&self) -> Option<u32>;
41
42    /// Get the position offset in EMU as (x, y).
43    fn offset_emu(&self) -> Option<(i64, i64)>;
44
45    /// Get the extent (width, height) in EMU.
46    fn extent_emu(&self) -> Option<(i64, i64)>;
47
48    /// Get the rotation angle in degrees.
49    fn rotation_angle_deg(&self) -> Option<f64>;
50}
51
52impl ShapeExt for Shape {
53    fn name(&self) -> &str {
54        &self.non_visual_properties.c_nv_pr.name
55    }
56
57    fn description(&self) -> Option<&str> {
58        self.non_visual_properties.c_nv_pr.descr.as_deref()
59    }
60
61    fn text_body(&self) -> Option<&ooxml_dml::types::TextBody> {
62        self.text_body.as_deref()
63    }
64
65    fn paragraphs(&self) -> &[TextParagraph] {
66        static EMPTY: &[TextParagraph] = &[];
67        self.text_body
68            .as_ref()
69            .map(|tb| tb.paragraphs())
70            .unwrap_or(EMPTY)
71    }
72
73    fn text(&self) -> Option<String> {
74        self.text_body.as_ref().map(|tb| {
75            tb.paragraphs()
76                .iter()
77                .map(|p| p.text())
78                .collect::<Vec<_>>()
79                .join("\n")
80        })
81    }
82
83    fn has_text(&self) -> bool {
84        self.text_body
85            .as_ref()
86            .is_some_and(|tb| !tb.paragraphs().is_empty())
87    }
88
89    fn shape_id(&self) -> u32 {
90        self.non_visual_properties.c_nv_pr.id
91    }
92
93    fn is_placeholder(&self) -> bool {
94        self.non_visual_properties.nv_pr.ph.is_some()
95    }
96
97    fn placeholder_type(&self) -> Option<&STPlaceholderType> {
98        self.non_visual_properties
99            .nv_pr
100            .ph
101            .as_deref()
102            .and_then(|p| p.r#type.as_ref())
103    }
104
105    fn placeholder_index(&self) -> Option<u32> {
106        self.non_visual_properties
107            .nv_pr
108            .ph
109            .as_deref()
110            .and_then(|p| p.idx)
111    }
112
113    fn offset_emu(&self) -> Option<(i64, i64)> {
114        use ooxml_dml::ext::ShapePropertiesExt;
115        self.shape_properties.offset_emu()
116    }
117
118    fn extent_emu(&self) -> Option<(i64, i64)> {
119        use ooxml_dml::ext::ShapePropertiesExt;
120        self.shape_properties.extent_emu()
121    }
122
123    fn rotation_angle_deg(&self) -> Option<f64> {
124        use ooxml_dml::ext::ShapePropertiesExt;
125        self.shape_properties.rotation_angle_deg()
126    }
127}
128
129/// Extension trait for Picture (p:pic element).
130pub trait PictureExt {
131    /// Get the picture name from cNvPr.
132    fn name(&self) -> &str;
133
134    /// Get the picture description/alt text from cNvPr.
135    fn description(&self) -> Option<&str>;
136
137    /// Get the relationship ID for the embedded image (from blipFill/blip@r:embed).
138    fn embed_rel_id(&self) -> Option<&str>;
139
140    /// Get the position offset in EMU as (x, y).
141    fn offset_emu(&self) -> Option<(i64, i64)>;
142
143    /// Get the extent (width, height) in EMU.
144    fn extent_emu(&self) -> Option<(i64, i64)>;
145
146    /// Get the crop rectangle, if any.
147    ///
148    /// Specifies what portion of the image is displayed.
149    /// Requires the `dml-fills` feature.
150    #[cfg(feature = "dml-fills")]
151    fn crop_rect(&self) -> Option<&ooxml_dml::types::CTRelativeRect>;
152}
153
154impl PictureExt for Picture {
155    fn name(&self) -> &str {
156        &self.non_visual_picture_properties.c_nv_pr.name
157    }
158
159    fn description(&self) -> Option<&str> {
160        self.non_visual_picture_properties.c_nv_pr.descr.as_deref()
161    }
162
163    fn embed_rel_id(&self) -> Option<&str> {
164        self.blip_fill
165            .blip
166            .as_ref()
167            .and_then(|b| b.embed.as_deref())
168    }
169
170    fn offset_emu(&self) -> Option<(i64, i64)> {
171        use ooxml_dml::ext::ShapePropertiesExt;
172        self.shape_properties.offset_emu()
173    }
174
175    fn extent_emu(&self) -> Option<(i64, i64)> {
176        use ooxml_dml::ext::ShapePropertiesExt;
177        self.shape_properties.extent_emu()
178    }
179
180    #[cfg(feature = "dml-fills")]
181    fn crop_rect(&self) -> Option<&ooxml_dml::types::CTRelativeRect> {
182        self.blip_fill.src_rect.as_deref()
183    }
184}
185
186/// Extension trait for Connector (p:cxnSp element).
187pub trait ConnectorExt {
188    /// Get the connector name from cNvPr.
189    fn name(&self) -> &str;
190
191    /// Get the connector description/alt text from cNvPr.
192    fn description(&self) -> Option<&str>;
193}
194
195impl ConnectorExt for Connector {
196    fn name(&self) -> &str {
197        &self.non_visual_connector_properties.c_nv_pr.name
198    }
199
200    fn description(&self) -> Option<&str> {
201        self.non_visual_connector_properties
202            .c_nv_pr
203            .descr
204            .as_deref()
205    }
206}
207
208/// Extension trait for GraphicalObjectFrame (p:graphicFrame element).
209pub trait GraphicalObjectFrameExt {
210    /// Get the frame name from cNvPr.
211    fn name(&self) -> &str;
212
213    /// Get the frame description/alt text from cNvPr.
214    fn description(&self) -> Option<&str>;
215}
216
217impl GraphicalObjectFrameExt for GraphicalObjectFrame {
218    fn name(&self) -> &str {
219        &self.nv_graphic_frame_pr.c_nv_pr.name
220    }
221
222    fn description(&self) -> Option<&str> {
223        self.nv_graphic_frame_pr.c_nv_pr.descr.as_deref()
224    }
225}
226
227/// Extension trait for GroupShape (p:grpSp and p:spTree elements).
228///
229/// This trait works for both group shapes and the root shape tree,
230/// since p:spTree is defined as a GroupShape in the schema.
231pub trait GroupShapeExt {
232    /// Get the group name from cNvPr.
233    fn name(&self) -> &str;
234
235    /// Get the group description/alt text from cNvPr.
236    fn description(&self) -> Option<&str>;
237
238    /// Get all shapes in this group.
239    fn shapes(&self) -> &[Shape];
240
241    /// Get all pictures in this group.
242    fn pictures(&self) -> &[Picture];
243
244    /// Get all connectors in this group.
245    fn connectors(&self) -> &[Connector];
246
247    /// Get all nested group shapes.
248    fn group_shapes(&self) -> &[GroupShape];
249
250    /// Get all graphical object frames (charts, tables, etc.).
251    fn graphic_frames(&self) -> &[GraphicalObjectFrame];
252
253    /// Get all text from shapes in this group (recursively).
254    fn text(&self) -> String;
255
256    /// Collect all shapes recursively (including from nested group shapes).
257    fn all_shapes_recursive(&self) -> Vec<&Shape>;
258
259    /// Collect all text from all shapes recursively, joined by newlines.
260    fn all_text_recursive(&self) -> String;
261}
262
263impl GroupShapeExt for GroupShape {
264    fn name(&self) -> &str {
265        &self.non_visual_group_properties.c_nv_pr.name
266    }
267
268    fn description(&self) -> Option<&str> {
269        self.non_visual_group_properties.c_nv_pr.descr.as_deref()
270    }
271
272    fn shapes(&self) -> &[Shape] {
273        &self.shape
274    }
275
276    fn pictures(&self) -> &[Picture] {
277        &self.picture
278    }
279
280    fn connectors(&self) -> &[Connector] {
281        &self.connector
282    }
283
284    fn group_shapes(&self) -> &[GroupShape] {
285        &self.group_shape
286    }
287
288    fn graphic_frames(&self) -> &[GraphicalObjectFrame] {
289        &self.graphic_frame
290    }
291
292    fn text(&self) -> String {
293        let mut texts = Vec::new();
294
295        // Collect text from shapes
296        for shape in &self.shape {
297            if let Some(t) = shape.text() {
298                texts.push(t);
299            }
300        }
301
302        // Recursively collect from nested groups
303        for group in &self.group_shape {
304            let t = group.text();
305            if !t.is_empty() {
306                texts.push(t);
307            }
308        }
309
310        texts.join("\n")
311    }
312
313    fn all_shapes_recursive(&self) -> Vec<&Shape> {
314        let mut shapes: Vec<&Shape> = self.shape.iter().collect();
315        for group in &self.group_shape {
316            shapes.extend(group.all_shapes_recursive());
317        }
318        shapes
319    }
320
321    fn all_text_recursive(&self) -> String {
322        self.all_shapes_recursive()
323            .iter()
324            .filter_map(|s| s.text())
325            .filter(|t| !t.is_empty())
326            .collect::<Vec<_>>()
327            .join("\n")
328    }
329}
330
331/// Extension trait for CommonSlideData (p:cSld element).
332pub trait CommonSlideDataExt {
333    /// Get the shape tree (which is a GroupShape).
334    fn shape_tree(&self) -> &GroupShape;
335
336    /// Get all shapes from the shape tree.
337    fn shapes(&self) -> &[Shape];
338
339    /// Get all pictures from the shape tree.
340    fn pictures(&self) -> &[Picture];
341
342    /// Get all text content from all shapes.
343    fn text(&self) -> String;
344}
345
346impl CommonSlideDataExt for CommonSlideData {
347    fn shape_tree(&self) -> &GroupShape {
348        &self.shape_tree
349    }
350
351    fn shapes(&self) -> &[Shape] {
352        self.shape_tree.shapes()
353    }
354
355    fn pictures(&self) -> &[Picture] {
356        self.shape_tree.pictures()
357    }
358
359    fn text(&self) -> String {
360        self.shape_tree.text()
361    }
362}
363
364/// Extension trait for Slide (p:sld element).
365pub trait SlideExt {
366    /// Get the common slide data.
367    fn common_slide_data(&self) -> &CommonSlideData;
368
369    /// Get the shape tree (which is a GroupShape).
370    fn shape_tree(&self) -> &GroupShape;
371
372    /// Get all shapes on the slide.
373    fn shapes(&self) -> &[Shape];
374
375    /// Get all pictures on the slide.
376    fn pictures(&self) -> &[Picture];
377
378    /// Get all text content from the slide.
379    fn text(&self) -> String;
380
381    /// Get the slide transition (if any).
382    #[cfg(feature = "pml-transitions")]
383    fn transition(&self) -> Option<&SlideTransition>;
384
385    /// Get the slide background, if one is explicitly set.
386    ///
387    /// Requires the `pml-styling` feature.
388    #[cfg(feature = "pml-styling")]
389    fn background(&self) -> Option<&CTBackground>;
390
391    /// Check if the slide is hidden (show=false).
392    fn is_hidden(&self) -> bool;
393}
394
395impl SlideExt for Slide {
396    fn common_slide_data(&self) -> &CommonSlideData {
397        &self.common_slide_data
398    }
399
400    fn shape_tree(&self) -> &GroupShape {
401        self.common_slide_data.shape_tree()
402    }
403
404    fn shapes(&self) -> &[Shape] {
405        self.common_slide_data.shapes()
406    }
407
408    fn pictures(&self) -> &[Picture] {
409        self.common_slide_data.pictures()
410    }
411
412    fn text(&self) -> String {
413        self.common_slide_data.text()
414    }
415
416    #[cfg(feature = "pml-transitions")]
417    fn transition(&self) -> Option<&SlideTransition> {
418        self.transition.as_deref()
419    }
420
421    #[cfg(feature = "pml-styling")]
422    fn background(&self) -> Option<&CTBackground> {
423        self.common_slide_data.bg.as_deref()
424    }
425
426    fn is_hidden(&self) -> bool {
427        !self.show.unwrap_or(true)
428    }
429}
430
431/// Extension trait for SlideLayout (p:sldLayout element).
432pub trait SlideLayoutExt {
433    /// Get the common slide data.
434    fn common_slide_data(&self) -> &CommonSlideData;
435
436    /// Get the layout type (if specified).
437    fn layout_type(&self) -> Option<&STSlideLayoutType>;
438
439    /// Check if this layout should show master shapes.
440    fn show_master_shapes(&self) -> bool;
441}
442
443impl SlideLayoutExt for SlideLayout {
444    fn common_slide_data(&self) -> &CommonSlideData {
445        &self.common_slide_data
446    }
447
448    #[cfg(feature = "pml-masters")]
449    fn layout_type(&self) -> Option<&STSlideLayoutType> {
450        self.r#type.as_ref()
451    }
452
453    #[cfg(not(feature = "pml-masters"))]
454    fn layout_type(&self) -> Option<&STSlideLayoutType> {
455        None
456    }
457
458    #[cfg(feature = "pml-masters")]
459    fn show_master_shapes(&self) -> bool {
460        self.show_master_sp.unwrap_or(true)
461    }
462
463    #[cfg(not(feature = "pml-masters"))]
464    fn show_master_shapes(&self) -> bool {
465        true
466    }
467}
468
469/// Extension trait for SlideMaster (p:sldMaster element).
470pub trait SlideMasterExt {
471    /// Get the common slide data.
472    fn common_slide_data(&self) -> &CommonSlideData;
473
474    /// Check if this master should preserve content.
475    fn preserve(&self) -> bool;
476}
477
478impl SlideMasterExt for SlideMaster {
479    fn common_slide_data(&self) -> &CommonSlideData {
480        &self.common_slide_data
481    }
482
483    #[cfg(feature = "pml-masters")]
484    fn preserve(&self) -> bool {
485        self.preserve.unwrap_or(false)
486    }
487
488    #[cfg(not(feature = "pml-masters"))]
489    fn preserve(&self) -> bool {
490        false
491    }
492}
493
494/// Extension trait for NotesSlide (p:notes element).
495#[cfg(feature = "pml-notes")]
496pub trait NotesSlideExt {
497    /// Get the common slide data containing the notes content.
498    fn common_slide_data(&self) -> &CommonSlideData;
499
500    /// Get all text from the notes.
501    fn text(&self) -> String;
502}
503
504#[cfg(feature = "pml-notes")]
505impl NotesSlideExt for NotesSlide {
506    fn common_slide_data(&self) -> &CommonSlideData {
507        &self.common_slide_data
508    }
509
510    fn text(&self) -> String {
511        self.common_slide_data.text()
512    }
513}