powerpoint_ooxml/model.rs
1//! In-memory model of a `.pptx` package.
2//!
3//! A presentation is a sequence of slides; a slide is a sequence of [`Shape`]s, an enum covering
4//! every distinct shape kind PresentationML defines: plain autoshapes with no placeholder
5//! inheritance, images (`Shape::Picture`), charts (`Shape::Chart`), placeholder metadata on
6//! autoshapes, per-slide speaker notes, shape groups (`Shape::Group`), connectors
7//! (`Shape::Connector`), tables (`Shape::Table`), per-slide comments ([`SlideComment`]), and a
8//! customizable color/font theme ([`Theme`]). A fully customizable slide layout/master model,
9//! backgrounds, and SmartArt are not modeled.
10//!
11//! [`Connector`] and [`SlideComment`] have no real fixture in this project's local corpus to ground
12//! their structure against (unlike every other shape kind, which was cross-checked against a real,
13//! non-synthetic `.pptx`; see each type's own doc comment). Their shape is instead built directly
14//! from ECMA-376's own schema text, the same fallback used whenever no fixture happens to exercise
15//! a given property (e.g. `word-ooxml`'s `CT_TcBorders` child sequence). Flagged here for extra
16//! scrutiny once opened in a real PowerPoint installation.
17//!
18//! A shape's content (position/size, outline, fill, text) reuses the `drawing` crate's
19//! [`drawing::ShapeProperties`]/[`drawing::TextBody`] directly rather than redefining them — the
20//! same shared DrawingML vocabulary already used by `word-ooxml` (`<w:drawing>`) and `excel-ooxml`
21//! (`xdr:sp`/`xdr:pic`). See `drawing`'s own top-level doc comment: it explicitly anticipated
22//! PowerPoint as the format that would lean on it "the most heavily of the three, since a slide's
23//! shapes essentially *are* its content". A chart on a slide reuses the `chart` crate's
24//! `ChartSpace` verbatim, same posture as `word-ooxml`/ `excel-ooxml`'s own embedded charts.
25
26use chart::ChartSpace;
27use drawing::{Color, Fill, Line, ShapeProperties, TextAnchor, TextBody};
28
29/// EMUs (English Metric Units) per inch — 914,400, the same conversion factor
30/// `word-ooxml`/`drawing` already use for every EMU-denominated field.
31pub const EMU_PER_INCH: i64 = 914_400;
32
33/// The default slide size this crate uses when a [`Presentation`] doesn't override it: 16:9
34/// widescreen, `12192000 x 6858000` EMU (13.333 x 7.5 inches) — real PowerPoint's own current
35/// default.
36pub const DEFAULT_SLIDE_WIDTH_EMU: i64 = 12_192_000;
37pub const DEFAULT_SLIDE_HEIGHT_EMU: i64 = 6_858_000;
38
39/// The default notes page size (4:3 portrait, `6858000 x 9144000` EMU), same posture: not
40/// customizable, just written as a fixed, schema-complete value so a generated presentation always
41/// has a valid `<p:notesSz>` (mandatory per `CT_Presentation`), matching the real fixture's own
42/// value.
43pub const DEFAULT_NOTES_WIDTH_EMU: i64 = 6_858_000;
44pub const DEFAULT_NOTES_HEIGHT_EMU: i64 = 9_144_000;
45
46/// A `.pptx` presentation: an ordered list of slides, plus the slide size.
47///
48/// A real PowerPoint package also always carries a slide master, at least one slide layout, and a
49/// theme — but this crate has no customizable model for the slide master/layout (still a single
50/// minimal, fixed pair under the hood; the reader ignores their content entirely, it only resolves
51/// each slide's own shapes). The theme, however, *is* customizable (`theme`) — unlike the
52/// master/layout, a slide master's theme relationship is mandatory per ECMA-376
53/// but its actual color/font content is the single most visible piece of a deck's visual identity,
54/// so it gets its own model rather than staying permanently fixed like `excel-ooxml`'s theme.
55#[derive(Debug, Clone, PartialEq)]
56pub struct Presentation {
57 /// The slides, in display order.
58 pub slides: Vec<Slide>,
59 /// `<p:sldSz cx=".." cy="..">`, in EMUs.
60 pub slide_width_emu: i64,
61 pub slide_height_emu: i64,
62 /// This presentation's color/font theme. `None` (the default) still always produces a complete
63 /// `ppt/theme/theme1.xml` — the part itself is mandatory, not optional, unlike e.g.
64 /// `word-ooxml::Document.theme` — just with Office's own built-in default colors/fonts
65 /// ([`Theme::office_default`]) rather than a caller-supplied palette.
66 pub theme: Option<Theme>,
67 /// Document metadata (`docProps/core.xml`/`app.xml`/`custom.xml`). Ports `excel-ooxml`'s own
68 /// [`DocumentProperties`] posture unchanged (same fields, same "each `None`/empty stays an
69 /// omitted, schema-valid element/part" behavior).
70 pub properties: DocumentProperties,
71 /// Package-wide custom table styles (`ppt/tableStyles.xml`'s own `<a:tblStyle>` entries, beyond
72 /// the single fixed built-in one this crate always declares as `def`). Referenced by id from
73 /// [`SlideTable::style_id`]. An empty `Vec` (the default) means every table in this
74 /// presentation uses the fixed built-in style, unchanged.
75 pub table_styles: Vec<SlideTableStyle>,
76 /// TrueType/OpenType fonts embedded directly in the package (`ppt/fonts/fontN.fntdata`,
77 /// `<p:embeddedFontLst>`). Lets a presentation carry a non-standard font so it renders
78 /// correctly even on a machine that doesn't have that font installed. An empty `Vec` (the
79 /// default) omits `<p:embeddedFontLst>` entirely and leaves `<p:presentation>`'s own
80 /// `embedTrueTypeFonts` attribute unset, same "only write what was actually set" convention as
81 /// every other optional part in this crate.
82 pub embedded_fonts: Vec<EmbeddedFont>,
83}
84
85impl Presentation {
86 /// Creates an empty presentation (no slides), with the default 16:9 widescreen slide size and
87 /// no custom theme (Office's own built-in default colors/fonts are used).
88 pub fn new() -> Self {
89 Self {
90 slides: Vec::new(),
91 slide_width_emu: DEFAULT_SLIDE_WIDTH_EMU,
92 slide_height_emu: DEFAULT_SLIDE_HEIGHT_EMU,
93 theme: None,
94 properties: DocumentProperties::new(),
95 table_styles: Vec::new(),
96 embedded_fonts: Vec::new(),
97 }
98 }
99
100 /// Appends a slide.
101 pub fn with_slide(mut self, slide: Slide) -> Self {
102 self.slides.push(slide);
103 self
104 }
105
106 /// Appends an empty slide and returns a mutable reference to it, to build it up in place
107 /// (`presentation.add_slide().add_shape(..)`). A `&mut self` counterpart to
108 /// [`Self::with_slide`], for building a presentation slide-by-slide in a loop without the
109 /// consuming builder's `presentation = presentation.with_slide(..)` reassignment on every
110 /// iteration.
111 pub fn add_slide(&mut self) -> &mut Slide {
112 self.slides.push(Slide::new());
113 self.slides.last_mut().expect("a slide was just pushed")
114 }
115
116 /// Overrides the slide size (default: 16:9 widescreen).
117 pub fn with_slide_size(mut self, width_emu: i64, height_emu: i64) -> Self {
118 self.slide_width_emu = width_emu;
119 self.slide_height_emu = height_emu;
120 self
121 }
122
123 /// Sets a custom color/font theme, replacing Office's own built-in default.
124 pub fn with_theme(mut self, theme: Theme) -> Self {
125 self.theme = Some(theme);
126 self
127 }
128
129 /// Sets this presentation's document metadata.
130 pub fn with_properties(mut self, properties: DocumentProperties) -> Self {
131 self.properties = properties;
132 self
133 }
134
135 /// Appends a custom table style.
136 pub fn with_table_style(mut self, table_style: SlideTableStyle) -> Self {
137 self.table_styles.push(table_style);
138 self
139 }
140
141 /// Appends an embedded font.
142 pub fn with_embedded_font(mut self, font: EmbeddedFont) -> Self {
143 self.embedded_fonts.push(font);
144 self
145 }
146}
147
148impl Default for Presentation {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154/// A single slide: an ordered list of shapes (`<p:spTree>`'s children), plus optional speaker
155/// notes.
156#[derive(Debug, Clone, PartialEq, Default)]
157pub struct Slide {
158 pub shapes: Vec<Shape>,
159 /// This slide's speaker notes (`ppt/notesSlides/notesSlideN.xml`'s own "body" placeholder
160 /// text), if any. `None` (the default) means no notes part is written for this slide at all —
161 /// real PowerPoint packages commonly have no notes for most slides, so this stays a genuinely
162 /// optional, per-slide part rather than always-present boilerplate (mirrors `word-ooxml`'s
163 /// header/footer, `styles.xml`, etc. — every optional part in this workspace follows the same
164 /// "only written when actually set" convention). See [`Presentation`]'s writer for the fixed
165 /// notes-master/notes-slide boilerplate this produces around the text itself.
166 pub notes: Option<TextBody>,
167 /// This slide's comments (`ppt/comments/commentN.xml`), if any — a genuinely optional part,
168 /// only written when non-empty, same "only written when actually set" convention as `notes`.
169 /// See [`SlideComment`]'s own doc comment for the caveat that this feature isn't grounded on a
170 /// real fixture.
171 pub comments: Vec<SlideComment>,
172 /// `<p:cSld>/<p:bg>/<p:bgPr>` — this slide's own background fill, overriding whatever the slide
173 /// layout/master would otherwise show through. `None` (the default) omits `<p:bg>` entirely —
174 /// the slide simply inherits its background from the layout/master chain, same "no override"
175 /// default every other optional field in this crate uses. Only the direct fill choice
176 /// (`<p:bgPr>`, `EG_FillProperties`) is modeled — `<p:bgRef>` (a reference into the theme's own
177 /// background style list) is not. Grounded against a real fixture (`ppt/slides/slide2.xml`),
178 /// which confirmed `<p:bg>` as `<p:cSld>`'s very first child, directly wrapping `<p:bgPr>`
179 /// before any fill.
180 pub background: Option<Fill>,
181 /// `<p:sld show="0">` (`CT_Slide`'s own `show` attribute) — hides this slide from the normal
182 /// slide-show sequence (it's still present in the deck, just skipped when presenting;
183 /// PowerPoint's own "Hide Slide" toggle). `false` (the default) omits the attribute, matching
184 /// the schema's own default of a shown slide. Grounded against a real fixture.
185 pub hidden: bool,
186 /// `<p:sld name=".">` — an optional display name for this slide (distinct from its title
187 /// placeholder's own text), shown e.g. in PowerPoint's Slide Navigator/Outline view. `None`
188 /// omits the attribute entirely. **Not grounded on a fixture** — no corpus `.pptx` file
189 /// examined actually sets it — modeled directly from `CT_Slide`'s schema anyway, the same
190 /// posture already used for e.g. [`crate::model::SlideComment`]/[`crate::model::Connector`].
191 pub name: Option<String>,
192 /// `<p:sld showMasterSp="0">` (`CT_Slide`'s own `showMasterSp` attribute, inverted here) —
193 /// hides this slide's inherited slide- master placeholder graphics/decorations (e.g. a logo or
194 /// footer bar drawn directly on the master, not through a placeholder this slide overrides).
195 /// The schema's own default is `showMasterSp="1"` (shown) when the attribute is omitted — this
196 /// field is named/stored inverted (`false` = shown, matching every other boolean "override"
197 /// field in this crate, e.g. `hidden`) so that `Slide::new`'s all-`false` default already
198 /// matches real PowerPoint's own default behavior, and the attribute is only ever written (as
199 /// `"0"`) when explicitly hidden. Grounded against real fixtures.
200 pub hide_master_graphics: bool,
201}
202
203impl Slide {
204 /// Creates an empty slide (no shapes, no notes, no comments, no background override, shown,
205 /// unnamed, showing inherited master graphics).
206 pub fn new() -> Self {
207 Self::default()
208 }
209
210 /// Appends a shape.
211 pub fn with_shape(mut self, shape: Shape) -> Self {
212 self.shapes.push(shape);
213 self
214 }
215
216 /// Appends a shape and returns a mutable reference to it. A `&mut self` counterpart to
217 /// [`Self::with_shape`], same rationale as [`Presentation::add_slide`].
218 pub fn add_shape(&mut self, shape: Shape) -> &mut Shape {
219 self.shapes.push(shape);
220 self.shapes.last_mut().expect("a shape was just pushed")
221 }
222
223 /// Hides this slide from the normal slide-show sequence (`show="0"`).
224 pub fn with_hidden(mut self, hidden: bool) -> Self {
225 self.hidden = hidden;
226 self
227 }
228
229 /// Sets this slide's display name (`<p:sld name="..">`).
230 pub fn with_name(mut self, name: impl Into<String>) -> Self {
231 self.name = Some(name.into());
232 self
233 }
234
235 /// Sets this slide's speaker notes.
236 pub fn with_notes(mut self, notes: TextBody) -> Self {
237 self.notes = Some(notes);
238 self
239 }
240
241 /// Appends a comment.
242 pub fn with_comment(mut self, comment: SlideComment) -> Self {
243 self.comments.push(comment);
244 self
245 }
246
247 /// Sets this slide's own background fill, overriding the layout/master.
248 pub fn with_background(mut self, background: Fill) -> Self {
249 self.background = Some(background);
250 self
251 }
252
253 /// Hides this slide's inherited slide-master graphics (`showMasterSp="0"`).
254 pub fn with_hide_master_graphics(mut self, hide: bool) -> Self {
255 self.hide_master_graphics = hide;
256 self
257 }
258}
259
260/// A single item in a slide's shape tree (`<p:spTree>`'s children, in document order) — an enum
261/// covering every distinct shape kind PresentationML defines.
262#[derive(Debug, Clone, PartialEq)]
263pub enum Shape {
264 /// `<p:sp>`, `CT_Shape` — an autoshape or text box, possibly a placeholder.
265 AutoShape(AutoShape),
266 /// `<p:pic>`, `CT_Picture` — an embedded image.
267 Picture(Picture),
268 /// `<p:graphicFrame>` wrapping a `<c:chart>` — an embedded chart. Boxed: a chart's own nested
269 /// `ChartSpace` dwarfs every other variant here, and clippy's `large_enum_variant` flags the
270 /// resulting gap (every `Shape`, no matter its real kind, otherwise pays for the biggest one).
271 Chart(Box<SlideChart>),
272 /// `<p:grpSp>`, `CT_GroupShape` — a group of shapes moving/resizing as one, itself containing
273 /// any of these same variants (including nested groups).
274 Group(ShapeGroup),
275 /// `<p:cxnSp>`, `CT_Connector` — a straight/elbow/curved connector line, typically linking two
276 /// other shapes.
277 Connector(Connector),
278 /// `<p:graphicFrame>` wrapping an `<a:tbl>` — an embedded table.
279 Table(SlideTable),
280 /// `<p:pic>` carrying an `<a:videoFile>`/`<a:audioFile>` reference instead of a plain image —
281 /// an embedded video or audio clip.
282 Media(SlideMedia),
283}
284
285/// An autoshape or text box (`<p:sp>`, `CT_Shape`).
286///
287/// Real PowerPoint title/content slides overwhelmingly use *placeholder* shapes
288/// (position/formatting inherited from the slide layout/master, see [`Placeholder`]) rather than
289/// freestanding ones. `placeholder` is `None` for a freestanding shape and `Some` for one that
290/// plays a placeholder role; either way, `properties`/`text_body` describe the shape's own content
291/// exactly the same way.
292#[derive(Debug, Clone, PartialEq)]
293pub struct AutoShape {
294 /// `<p:cNvPr id="..">` — must be unique within the slide, and must not collide with `1`, always
295 /// reserved by this crate's writer for the slide's own group shape (`<p:nvGrpSpPr><p:cNvPr
296 /// id="1"../>`) — mirrors `word-ooxml`'s existing convention of caller-managed numeric ids (see
297 /// e.g. `Bookmark::new(id)`/`NoteReference::new(id)`).
298 pub id: u32,
299 /// `<p:cNvPr name="..">` — a human-readable shape name (e.g. "Zone de texte 1"), purely
300 /// cosmetic (shown in PowerPoint's Selection Pane).
301 pub name: String,
302 /// `<p:spPr>`'s content — position/size, outline geometry, fill, line. Reuses
303 /// `drawing::ShapeProperties` verbatim (see this module's own doc comment). When `transform` is
304 /// unset, the writer still always emits a fixed zero-valued `<a:xfrm>` and a default `rect`
305 /// `<a:prstGeom>` — the same "always write geometry, never delegate silently" fix this project
306 /// already had to apply twice to `excel-ooxml`'s picture writing (missing geometry, then
307 /// missing `<a:xfrm>`, both making a shape invisible in real Excel) — applied here proactively
308 /// rather than rediscovered.
309 ///
310 /// Unlike `excel-ooxml`'s picture reader (which unconditionally clears `transform`/demotes a
311 /// plain `rect` `geometry` back to `None` on read, because a picture's *real* position always
312 /// comes from its separate two-cell anchor, never from `spPr`'s own `xfrm`), this crate's
313 /// reader makes no such attempt for a *freestanding* shape: `xfrm` is its only source of
314 /// position/size, so a real, non-zero transform must round-trip faithfully. This means an
315 /// [`AutoShape`] written with `properties` left at its `ShapeProperties::new()` default reads
316 /// back with an explicit zero-offset/zero-extent transform and a `rect` geometry, not `None` —
317 /// a known, deliberate round-trip asymmetry for this specific case, not a bug. A *placeholder*
318 /// shape (`<p:spPr/>` empty, position/formatting inherited) reads back with `properties` at
319 /// that same default, for the exact same textual reason — genuinely indistinguishable from the
320 /// freestanding zero-sized case by this crate, another accepted limitation of not modeling
321 /// slide layout inheritance yet.
322 pub properties: ShapeProperties,
323 /// `<p:txBody>` — this shape's text content, if any. Reuses `drawing::TextBody` verbatim.
324 pub text_body: Option<TextBody>,
325 /// `<p:nvSpPr>/<p:nvPr>/<p:ph>` — this shape's placeholder role, if any. `None` for a
326 /// freestanding shape.
327 pub placeholder: Option<Placeholder>,
328 /// `<p:nvSpPr>/<p:cNvSpPr txBox="1">` — marks this shape as a genuine text box rather than an
329 /// ordinary autoshape. Purely a hint real PowerPoint itself uses (e.g. to decide whether the
330 /// shape gets a default outline/fill when the user starts typing into an empty one) — this
331 /// crate doesn't otherwise treat a text box specially; `properties`/`text_body` describe its
332 /// content exactly the same way regardless. Grounded against a real fixture confirming the
333 /// exact `<p:cNvSpPr txBox="1"/>` shape.
334 pub is_text_box: bool,
335 /// `<p:nvSpPr>/<p:cNvPr>/<a:hlinkClick>` (including the internal-target/relative-jump
336 /// variants) — a clickable hyperlink on this whole shape (as opposed to a hyperlink on an
337 /// individual text run, `drawing::Hyperlink` on `drawing::TextRunProperties`, which this crate
338 /// does reuse for run text but doesn't yet resolve to a real relationship — see `writer.rs`'s
339 /// own doc comment on hyperlinks for why shape-level support came first). `None` for no link.
340 /// See [`SlideHyperlinkTarget`]'s own doc comment for the target kinds.
341 pub hyperlink: Option<SlideHyperlinkTarget>,
342}
343
344impl AutoShape {
345 /// Creates a freestanding shape with no explicit position/size/fill/ line, no text, no
346 /// placeholder role, not a text box, and no hyperlink (use [`AutoShape::with_properties`]/
347 /// [`AutoShape::with_text_body`]/[`AutoShape::with_placeholder`]/
348 /// [`AutoShape::with_text_box`]/[`AutoShape::with_hyperlink`]/
349 /// [`AutoShape::with_hyperlink_target`] to fill those in).
350 pub fn new(id: u32, name: impl Into<String>) -> Self {
351 Self {
352 id,
353 name: name.into(),
354 properties: ShapeProperties::new(),
355 text_body: None,
356 placeholder: None,
357 is_text_box: false,
358 hyperlink: None,
359 }
360 }
361
362 /// Sets this shape's position/size/outline/fill/line.
363 pub fn with_properties(mut self, properties: ShapeProperties) -> Self {
364 self.properties = properties;
365 self
366 }
367
368 /// Sets this shape's text content.
369 pub fn with_text_body(mut self, text_body: TextBody) -> Self {
370 self.text_body = Some(text_body);
371 self
372 }
373
374 /// Marks this shape as playing the given placeholder role.
375 pub fn with_placeholder(mut self, placeholder: Placeholder) -> Self {
376 self.placeholder = Some(placeholder);
377 self
378 }
379
380 /// Marks (or unmarks) this shape as a genuine text box (`txBox="1"`).
381 pub fn with_text_box(mut self, is_text_box: bool) -> Self {
382 self.is_text_box = is_text_box;
383 self
384 }
385
386 /// Attaches a clickable hyperlink to this whole shape, given its target URL — sugar for
387 /// `with_hyperlink_target(SlideHyperlinkTarget::External(url))`.
388 pub fn with_hyperlink(mut self, url: impl Into<String>) -> Self {
389 self.hyperlink = Some(SlideHyperlinkTarget::External(url.into()));
390 self
391 }
392
393 /// Attaches a clickable hyperlink to this whole shape, given any [`SlideHyperlinkTarget`] — the
394 /// general form of [`AutoShape::with_hyperlink`], also covering internal slide jumps.
395 pub fn with_hyperlink_target(mut self, target: SlideHyperlinkTarget) -> Self {
396 self.hyperlink = Some(target);
397 self
398 }
399}
400
401/// A clickable hyperlink's target (`<a:hlinkClick>`, `CT_Hyperlink`): either
402/// [`SlideHyperlinkTarget::External`], or a jump to another slide in the *same* presentation rather
403/// than an outside URL. `<a:hlinkClick r:id=".">`'s `r:id` resolves to a relationship — for
404/// [`SlideHyperlinkTarget::External`] that relationship's target is the URL itself
405/// (`TargetMode="External"`); for
406/// [`SlideHyperlinkTarget::Slide`] it instead targets another part *inside* the same package
407/// (`TargetMode="Internal"`, `"./slides/slideN.xml"`), paired with
408/// `action="ppaction://hlinksldjump"`. The four relative-jump variants need no relationship at all
409/// — they're expressed purely via `action="ppaction://hlinkshowjump?jump=."` with no `r:id`
410/// attribute. Grounded against a real fixture for the `r:id`+`hlinksldjump` shape
411/// ([`SlideHyperlinkTarget::Slide`]); the relative- jump actions have no fixture in this project's
412/// corpus but are modeled directly from the `ppaction://hlinkshowjump` action values ECMA-376
413/// itself documents for "next slide"/"previous slide"/"first slide"/"last slide".
414#[derive(Debug, Clone, PartialEq, Eq)]
415pub enum SlideHyperlinkTarget {
416 /// An ordinary outside URL (e.g. `"https://example.com"`), the only kind this crate supported
417 /// previously.
418 External(String),
419 /// Jumps to another slide in the same presentation, given as a 0-based index into
420 /// [`Presentation::slides`] — resolved to a relative relationship target
421 /// (`"./slides/slide{index + 1}.xml"`) purely from that index at write time, since slide
422 /// numbering is fully deterministic (no pre-built global slide-id map needed). Not validated
423 /// against the presentation's actual slide count — an out-of-range index simply produces a link
424 /// real PowerPoint would show as broken, the same best-effort posture already used elsewhere in
425 /// this crate (e.g. table cell merge spans).
426 Slide(usize),
427 /// Jumps to the next slide in presentation order.
428 NextSlide,
429 /// Jumps to the previous slide in presentation order.
430 PreviousSlide,
431 /// Jumps to the first slide in the presentation.
432 FirstSlide,
433 /// Jumps to the last slide in the presentation.
434 LastSlide,
435}
436
437/// `<p:nvPr><p:ph type=".." idx=".."/></p:nvPr>` (`CT_Placeholder`) — a shape's placeholder role:
438/// which slot of the slide layout/master it fills, so its position/formatting can be inherited from
439/// there instead of being set explicitly. This crate never resolves that inheritance (no slide
440/// layout/master model beyond a single fixed, empty one — see `Presentation`'s doc comment);
441/// recording `Placeholder` on a read-back shape is purely informational; nothing else in this crate
442/// consults it.
443#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct Placeholder {
445 /// `type` — which slot this shape fills (`ST_PlaceholderType`).
446 pub kind: PlaceholderKind,
447 /// `idx` — disambiguates multiple placeholders of the same `kind` on one layout (e.g. several
448 /// `body` placeholders). `None` omits the attribute, matching a title placeholder's own
449 /// convention (real PowerPoint never writes `idx` on `title`/`ctrTitle`).
450 pub index: Option<u32>,
451}
452
453impl Placeholder {
454 /// Creates a placeholder role with no explicit index.
455 pub fn new(kind: PlaceholderKind) -> Self {
456 Self { kind, index: None }
457 }
458
459 /// Sets this placeholder's `idx` attribute.
460 pub fn with_index(mut self, index: u32) -> Self {
461 self.index = Some(index);
462 self
463 }
464}
465
466/// A useful subset of `ST_PlaceholderType`'s values — the ones a shape (as opposed to a placeholder
467/// *picture*/*chart*/*table*, not modeled) can play. [`PlaceholderKind::Other`] preserves
468/// round-trip fidelity for any type not explicitly modeled here (`chart`/`tbl`/
469/// `clipArt`/`dgm`/`media`/`sldImg`/`pic`, all placeholder roles for a non-`<p:sp>` shape kind that
470/// this crate doesn't tag with placeholder info on the writing side, but must still be able to read
471/// back without error), same "closed enum + escape hatch" posture `drawing::PresetShape` already
472/// established.
473#[derive(Debug, Clone, PartialEq, Eq)]
474pub enum PlaceholderKind {
475 Title,
476 Body,
477 CenterTitle,
478 SubTitle,
479 DateTime,
480 SlideNumber,
481 Footer,
482 Header,
483 Object,
484 /// `sldImg` — the slide-thumbnail placeholder on a notes slide (see `Presentation`'s writer,
485 /// which emits one automatically around `Slide.notes`).
486 SlideImage,
487 Other(String),
488}
489
490impl PlaceholderKind {
491 pub(crate) fn xml_token(&self) -> &str {
492 match self {
493 PlaceholderKind::Title => "title",
494 PlaceholderKind::Body => "body",
495 PlaceholderKind::CenterTitle => "ctrTitle",
496 PlaceholderKind::SubTitle => "subTitle",
497 PlaceholderKind::DateTime => "dt",
498 PlaceholderKind::SlideNumber => "sldNum",
499 PlaceholderKind::Footer => "ftr",
500 PlaceholderKind::Header => "hdr",
501 PlaceholderKind::Object => "obj",
502 PlaceholderKind::SlideImage => "sldImg",
503 PlaceholderKind::Other(token) => token,
504 }
505 }
506
507 pub(crate) fn from_xml_token(token: &str) -> Self {
508 match token {
509 "title" => PlaceholderKind::Title,
510 "body" => PlaceholderKind::Body,
511 "ctrTitle" => PlaceholderKind::CenterTitle,
512 "subTitle" => PlaceholderKind::SubTitle,
513 "dt" => PlaceholderKind::DateTime,
514 "sldNum" => PlaceholderKind::SlideNumber,
515 "ftr" => PlaceholderKind::Footer,
516 "hdr" => PlaceholderKind::Header,
517 "obj" => PlaceholderKind::Object,
518 "sldImg" => PlaceholderKind::SlideImage,
519 other => PlaceholderKind::Other(other.to_string()),
520 }
521 }
522}
523
524/// An embedded image (`<p:pic>`, `CT_Picture`).
525///
526/// Unlike an [`AutoShape`], a picture's position/size is never optional or defaulted:
527/// `offset_emu`/`extent_emu` are plain fields (not routed through `drawing::Transform2D`'s `Option`
528/// flexibility), since a PresentationML picture's `<a:xfrm>` — unlike Excel's cell-anchored
529/// pictures — is the shape's *only* source of position/size; there is no separate anchor to fall
530/// back on, so demanding a concrete value upfront avoids ever having to invent one.
531#[derive(Debug, Clone, PartialEq)]
532pub struct Picture {
533 /// `<p:cNvPr id="..">` — same uniqueness rules as `AutoShape::id`.
534 pub id: u32,
535 /// `<p:cNvPr name="..">`.
536 pub name: String,
537 /// The raw, encoded image bytes (e.g. the bytes of a `.png` file).
538 pub data: Vec<u8>,
539 /// The image's encoding, also used to pick the media part's extension and content type.
540 pub format: PictureFormat,
541 /// `<a:off x=".." y="..">`, in EMUs.
542 pub offset_emu: (i64, i64),
543 /// `<a:ext cx=".." cy="..">`, in EMUs.
544 pub extent_emu: (i64, i64),
545 /// Alt text / description (`p:cNvPr`'s `descr` attribute). May be empty.
546 pub description: String,
547 /// Additional shape formatting (fill/line/rotation, and a non-default outline geometry — e.g.
548 /// an oval crop) for the picture's own `p:spPr`, beyond the fixed default rectangle. `None`
549 /// (the default) produces a plain rectangular picture with no extra fill/line, the same fixed
550 /// shape `word-ooxml`/`excel-ooxml`'s own pictures default to. `transform` is never consulted
551 /// here — `offset_emu`/`extent_emu` above are always authoritative for a picture's
552 /// position/size, mirroring `word_ooxml::Image::shape_properties`'s own posture.
553 pub shape_properties: Option<ShapeProperties>,
554 /// `<a:blip r:link=".">`'s own external target. When set, the writer emits *both* `r:embed`
555 /// (pointing at `data` below, embedded in the package as usual) and `r:link`
556 /// (`TargetMode="External"`, this URL/path) on the same `<a:blip>` — PowerPoint's own "embedded
557 /// preview cache + externally-linked original" dual pattern, so the picture still displays even
558 /// when the external source is unreachable. `None` (the default) omits `r:link` entirely — a
559 /// plain, fully-embedded picture, unchanged. Grounded against a real fixture.
560 pub external_link: Option<String>,
561}
562
563impl Picture {
564 /// Creates a picture at the given size, positioned at `(0, 0)` (use [`Picture::with_offset`] to
565 /// place it elsewhere), with no alt text, no extra shape formatting, and no external link.
566 pub fn new(
567 id: u32,
568 name: impl Into<String>,
569 data: impl Into<Vec<u8>>,
570 format: PictureFormat,
571 width_emu: i64,
572 height_emu: i64,
573 ) -> Self {
574 Self {
575 id,
576 name: name.into(),
577 data: data.into(),
578 format,
579 offset_emu: (0, 0),
580 extent_emu: (width_emu, height_emu),
581 description: String::new(),
582 shape_properties: None,
583 external_link: None,
584 }
585 }
586
587 /// Sets this picture's position.
588 pub fn with_offset(mut self, x_emu: i64, y_emu: i64) -> Self {
589 self.offset_emu = (x_emu, y_emu);
590 self
591 }
592
593 /// Sets this picture's alt text / description.
594 pub fn with_description(mut self, description: impl Into<String>) -> Self {
595 self.description = description.into();
596 self
597 }
598
599 /// Sets this picture's additional shape formatting (fill/line/ geometry), beyond the fixed
600 /// default rectangle.
601 pub fn with_shape_properties(mut self, shape_properties: ShapeProperties) -> Self {
602 self.shape_properties = Some(shape_properties);
603 self
604 }
605
606 /// Additionally links this picture to an external image (`r:link`), alongside its own embedded
607 /// `data` — PowerPoint's own "embedded preview cache + externally-linked original" dual
608 /// pattern.
609 pub fn with_external_link(mut self, url: impl Into<String>) -> Self {
610 self.external_link = Some(url.into());
611 self
612 }
613}
614
615/// An image's encoding — mirrors `word-ooxml::ImageFormat` by name and scope for cross-crate
616/// consistency (same real-world concept, each host crate keeps its own tiny copy rather than one
617/// depending on the other — there is no shared "media" crate, and this enum is far too small to
618/// justify creating one).
619#[derive(Debug, Clone, Copy, PartialEq, Eq)]
620pub enum PictureFormat {
621 Png,
622 Jpeg,
623 Gif,
624 Bmp,
625}
626
627impl PictureFormat {
628 pub(crate) fn extension(&self) -> &'static str {
629 match self {
630 PictureFormat::Png => "png",
631 PictureFormat::Jpeg => "jpeg",
632 PictureFormat::Gif => "gif",
633 PictureFormat::Bmp => "bmp",
634 }
635 }
636
637 pub(crate) fn content_type(&self) -> &'static str {
638 match self {
639 PictureFormat::Png => "image/png",
640 PictureFormat::Jpeg => "image/jpeg",
641 PictureFormat::Gif => "image/gif",
642 PictureFormat::Bmp => "image/bmp",
643 }
644 }
645
646 /// Guesses a format from a part name's extension (e.g. `/ppt/media/image1.jpeg"`), for reading
647 /// an existing package back.
648 pub(crate) fn from_extension(part_name: &str) -> Option<Self> {
649 let extension = part_name.rsplit('.').next()?.to_ascii_lowercase();
650 match extension.as_str() {
651 "png" => Some(PictureFormat::Png),
652 "jpeg" | "jpg" => Some(PictureFormat::Jpeg),
653 "gif" => Some(PictureFormat::Gif),
654 "bmp" => Some(PictureFormat::Bmp),
655 _ => None,
656 }
657 }
658}
659
660/// An embedded video or audio clip (`<p:pic>` carrying an `<a:videoFile r:link=".">`/`<a:audioFile
661/// r:link=".">` reference in its own `<p:nvPr>`, instead of the plain `<a:blip>` image an ordinary
662/// [`Picture`] has).
663///
664/// Real PowerPoint (2010+) also writes a Microsoft-proprietary `<p:extLst><p:ext><p14:media
665/// r:embed=".."/></p:ext></p:extLst>` alongside the core `<a:videoFile>`/`<a:audioFile>` element,
666/// referencing the same media part a second time for its own newer UI features — not modeled here
667/// (an extension outside ECMA-376 itself, same "skip Microsoft-only extensions" posture this crate
668/// already takes for `<a:extLst>` generally); the core relationship alone is schema-complete and
669/// should still play in real PowerPoint.
670///
671/// `<p:timing>` (autoplay/on-click trigger, bookmarks, trim points) lives at the *slide* level, not
672/// on the shape itself, and is a materially larger, separate feature — not modeled. Unlike
673/// [`Picture`], this type has no `shape_properties` field: the writer always produces a plain
674/// default rectangle (`<a:prstGeom prst="rect">`, no custom fill/ line), and any non-default
675/// geometry/fill/line an existing file's own `<p:pic>` might carry is simply discarded when read
676/// back — a video/audio clip's own outline styling is a rare, cosmetic refinement didn't ask for.
677#[derive(Debug, Clone, PartialEq)]
678pub struct SlideMedia {
679 /// `<p:cNvPr id="..">` — same uniqueness rules as `AutoShape::id`.
680 pub id: u32,
681 /// `<p:cNvPr name="..">`.
682 pub name: String,
683 /// The raw, encoded media bytes (e.g. the bytes of a `.mp4` file).
684 pub data: Vec<u8>,
685 /// The media's encoding, also used to pick the media part's extension/ content type and to tell
686 /// video from audio (`format.is_video()`).
687 pub format: MediaFormat,
688 /// `<a:off x=".." y="..">`, in EMUs — same "never optional, no separate anchor" posture as
689 /// [`Picture::offset_emu`].
690 pub offset_emu: (i64, i64),
691 /// `<a:ext cx=".." cy="..">`, in EMUs.
692 pub extent_emu: (i64, i64),
693 /// This clip's poster frame (`<p:blipFill>`'s own `<a:blip>`, the still image real PowerPoint
694 /// shows before playback) — the raw image bytes plus its format. `None` omits `<a:blip>` from
695 /// `<p:blipFill>` entirely (schema-valid — `blip` is `minOccurs="0"` — but the clip will render
696 /// with no visible thumbnail until played).
697 pub poster_image: Option<(Vec<u8>, PictureFormat)>,
698}
699
700impl SlideMedia {
701 /// Creates a media clip at the given size, positioned at `(0, 0)`, with no poster frame.
702 pub fn new(
703 id: u32,
704 name: impl Into<String>,
705 data: impl Into<Vec<u8>>,
706 format: MediaFormat,
707 width_emu: i64,
708 height_emu: i64,
709 ) -> Self {
710 Self {
711 id,
712 name: name.into(),
713 data: data.into(),
714 format,
715 offset_emu: (0, 0),
716 extent_emu: (width_emu, height_emu),
717 poster_image: None,
718 }
719 }
720
721 /// Sets this clip's position.
722 pub fn with_offset(mut self, x_emu: i64, y_emu: i64) -> Self {
723 self.offset_emu = (x_emu, y_emu);
724 self
725 }
726
727 /// Sets this clip's poster frame image.
728 pub fn with_poster_image(mut self, data: impl Into<Vec<u8>>, format: PictureFormat) -> Self {
729 self.poster_image = Some((data.into(), format));
730 self
731 }
732}
733
734/// A video/audio clip's encoding (`<a:videoFile>`/`<a:audioFile>`'s referenced media part) —
735/// mirrors [`PictureFormat`]'s own "closed enum, small known set" posture, covering the handful of
736/// container formats real PowerPoint itself natively embeds rather than every format a media player
737/// could theoretically open.
738#[derive(Debug, Clone, Copy, PartialEq, Eq)]
739pub enum MediaFormat {
740 Mp4,
741 Wmv,
742 Avi,
743 Mp3,
744 Wav,
745}
746
747impl MediaFormat {
748 /// Whether this format is a video (as opposed to an audio-only) clip — decides whether the
749 /// writer emits `<a:videoFile>` or `<a:audioFile>`.
750 pub(crate) fn is_video(&self) -> bool {
751 matches!(self, MediaFormat::Mp4 | MediaFormat::Wmv | MediaFormat::Avi)
752 }
753
754 pub(crate) fn extension(&self) -> &'static str {
755 match self {
756 MediaFormat::Mp4 => "mp4",
757 MediaFormat::Wmv => "wmv",
758 MediaFormat::Avi => "avi",
759 MediaFormat::Mp3 => "mp3",
760 MediaFormat::Wav => "wav",
761 }
762 }
763
764 pub(crate) fn content_type(&self) -> &'static str {
765 match self {
766 MediaFormat::Mp4 => "video/mp4",
767 MediaFormat::Wmv => "video/x-ms-wmv",
768 MediaFormat::Avi => "video/avi",
769 MediaFormat::Mp3 => "audio/mpeg",
770 MediaFormat::Wav => "audio/wav",
771 }
772 }
773
774 /// Guesses a format from a part name's extension (e.g. `/ppt/media/media1.mp4`), for reading an
775 /// existing package back.
776 pub(crate) fn from_extension(part_name: &str) -> Option<Self> {
777 let extension = part_name.rsplit('.').next()?.to_ascii_lowercase();
778 match extension.as_str() {
779 "mp4" => Some(MediaFormat::Mp4),
780 "wmv" => Some(MediaFormat::Wmv),
781 "avi" => Some(MediaFormat::Avi),
782 "mp3" => Some(MediaFormat::Mp3),
783 "wav" => Some(MediaFormat::Wav),
784 _ => None,
785 }
786 }
787}
788
789/// An embedded chart (`<p:graphicFrame>` wrapping a `<c:chart r:id="..">`).
790///
791/// Reuses the `chart` crate's `ChartSpace` verbatim, same posture as
792/// `word-ooxml::EmbeddedChart`/`excel-ooxml::SheetChart` — **cached values only, no embedded
793/// `.xlsx` workbook as a data source**, matching the scope of those two.
794#[derive(Debug, Clone, PartialEq)]
795pub struct SlideChart {
796 /// `<p:cNvPr id="..">` — same uniqueness rules as `AutoShape::id`.
797 pub id: u32,
798 /// `<p:cNvPr name="..">`.
799 pub name: String,
800 pub chart_space: ChartSpace,
801 /// `<p:xfrm><a:off x=".." y="..">`, in EMUs. Like [`Picture`], a chart's position/size is never
802 /// optional — `<p:xfrm>` is its only source of it (no separate anchor, unlike Excel's
803 /// `SheetChart`).
804 pub offset_emu: (i64, i64),
805 /// `<p:xfrm><a:ext cx=".." cy="..">`, in EMUs.
806 pub extent_emu: (i64, i64),
807}
808
809impl SlideChart {
810 /// Creates an embedded chart at the given size, positioned at `(0, 0)` (use
811 /// [`SlideChart::with_offset`] to place it elsewhere).
812 pub fn new(
813 id: u32,
814 name: impl Into<String>,
815 chart_space: ChartSpace,
816 width_emu: i64,
817 height_emu: i64,
818 ) -> Self {
819 Self {
820 id,
821 name: name.into(),
822 chart_space,
823 offset_emu: (0, 0),
824 extent_emu: (width_emu, height_emu),
825 }
826 }
827
828 /// Sets this chart's position.
829 pub fn with_offset(mut self, x_emu: i64, y_emu: i64) -> Self {
830 self.offset_emu = (x_emu, y_emu);
831 self
832 }
833}
834
835/// A group of shapes moving/resizing together (`<p:grpSp>`, `CT_GroupShape`).
836///
837/// A group's own transform is two coordinate pairs working together, not one:
838/// `offset_emu`/`extent_emu` (`<a:off>`/`<a:ext>`) place the group on the slide, exactly like any
839/// other shape; `child_offset_emu`/ `child_extent_emu` (`<a:chOff>`/`<a:chExt>`) define the
840/// coordinate space the *children*'s own `xfrm` values are expressed in — real PowerPoint scales
841/// between the two automatically whenever they differ (e.g. a group's children keep their original
842/// small-scale coordinates while the group itself is stretched much larger on the slide). Confirmed
843/// against a real fixture with nested groups, including a group whose child coordinate space
844/// genuinely differs from its own on-slide extent. Groups can nest (a `Shape::Group` inside
845/// another's own `shapes`), confirmed on the same fixture.
846#[derive(Debug, Clone, PartialEq)]
847pub struct ShapeGroup {
848 /// `<p:cNvPr id="..">` — same uniqueness rules as `AutoShape::id`.
849 pub id: u32,
850 /// `<p:cNvPr name="..">`.
851 pub name: String,
852 /// `<a:xfrm><a:off x=".." y="..">`, in EMUs.
853 pub offset_emu: (i64, i64),
854 /// `<a:xfrm><a:ext cx=".." cy="..">`, in EMUs.
855 pub extent_emu: (i64, i64),
856 /// `<a:xfrm><a:chOff x=".." y="..">`, in EMUs — the coordinate space this group's children's
857 /// own `xfrm` values are expressed in.
858 pub child_offset_emu: (i64, i64),
859 /// `<a:xfrm><a:chExt cx=".." cy="..">`, in EMUs.
860 pub child_extent_emu: (i64, i64),
861 /// `<a:xfrm rot=".">` — `ST_Angle`, in 60,000ths of a degree, positive clockwise. `0` (the
862 /// default) omits the attribute — no rotation. Symmetric to
863 /// [`drawing::Transform2D::rotation_60000ths`]. Grounded against real fixtures.
864 pub rotation_60000ths: i32,
865 /// `<a:xfrm flipH=".">` — mirrors the whole group horizontally before rotation is applied.
866 /// **Not grounded on a group specifically** in the fixture corpus — modeled by analogy to
867 /// [`drawing::Transform2D::flip_horizontal`] (identical `CT_GroupTransform2D`/`CT_Transform2D`
868 /// attribute, already grounded there).
869 pub flip_horizontal: bool,
870 /// `<a:xfrm flipV="..">` — mirrors the whole group vertically. Same grounding caveat as
871 /// `flip_horizontal`.
872 pub flip_vertical: bool,
873 /// The group's children, in document order — any shape kind, including nested groups.
874 pub shapes: Vec<Shape>,
875}
876
877impl ShapeGroup {
878 /// Creates an empty group (no children) with a zero-valued transform — use
879 /// [`ShapeGroup::with_transform`] to position it and its children's coordinate space, and
880 /// [`ShapeGroup::with_shape`] to add children.
881 pub fn new(id: u32, name: impl Into<String>) -> Self {
882 Self {
883 id,
884 name: name.into(),
885 offset_emu: (0, 0),
886 extent_emu: (0, 0),
887 child_offset_emu: (0, 0),
888 child_extent_emu: (0, 0),
889 rotation_60000ths: 0,
890 flip_horizontal: false,
891 flip_vertical: false,
892 shapes: Vec::new(),
893 }
894 }
895
896 /// Sets this group's own on-slide position/size and its children's coordinate space (often the
897 /// same values, when no scaling between the two is needed).
898 pub fn with_transform(
899 mut self,
900 offset_emu: (i64, i64),
901 extent_emu: (i64, i64),
902 child_offset_emu: (i64, i64),
903 child_extent_emu: (i64, i64),
904 ) -> Self {
905 self.offset_emu = offset_emu;
906 self.extent_emu = extent_emu;
907 self.child_offset_emu = child_offset_emu;
908 self.child_extent_emu = child_extent_emu;
909 self
910 }
911
912 /// Sets this group's rotation, given in ordinary degrees (converted to the schema's
913 /// 60,000ths-of-a-degree unit internally).
914 pub fn with_rotation_degrees(mut self, degrees: f64) -> Self {
915 self.rotation_60000ths = (degrees * 60_000.0).round() as i32;
916 self
917 }
918
919 /// Mirrors the whole group horizontally.
920 pub fn with_flip_horizontal(mut self, flip: bool) -> Self {
921 self.flip_horizontal = flip;
922 self
923 }
924
925 /// Mirrors the whole group vertically.
926 pub fn with_flip_vertical(mut self, flip: bool) -> Self {
927 self.flip_vertical = flip;
928 self
929 }
930
931 /// Appends a child shape.
932 pub fn with_shape(mut self, shape: Shape) -> Self {
933 self.shapes.push(shape);
934 self
935 }
936}
937
938/// A straight/elbow/curved connector line (`<p:cxnSp>`, `CT_Connector`), typically used to visually
939/// link two other shapes.
940///
941/// **Not grounded on a real fixture** — see this module's own top-level doc comment. Structure
942/// follows ECMA-376's `CT_Connector`/ `CT_ConnectorNonVisual` directly: a connector's own
943/// `<p:spPr>` is the exact same `CT_ShapeProperties` content type an autoshape's `<p:spPr>` uses
944/// (position/geometry/fill/line), so `properties` reuses `drawing::ShapeProperties` verbatim, same
945/// as [`AutoShape`] — only the non-visual wrapper element name (`p:cxnSp`/`p:nvCxnSpPr` instead of
946/// `p:sp`/`p:nvSpPr`) and the optional start/end shape attachments differ. When
947/// `properties.geometry` is unset, the writer defaults to `<a:prstGeom prst="line">` (a straight
948/// connector) rather than `AutoShape`'s own `rect` default — the same "always write geometry
949/// explicitly" discipline, just with a connector-appropriate default preset.
950#[derive(Debug, Clone, PartialEq)]
951pub struct Connector {
952 /// `<p:cNvPr id="..">` — same uniqueness rules as `AutoShape::id`.
953 pub id: u32,
954 /// `<p:cNvPr name="..">`.
955 pub name: String,
956 /// `<p:spPr>`'s content — position/size, outline geometry, fill, line.
957 pub properties: ShapeProperties,
958 /// `<p:nvCxnSpPr><p:cNvCxnSpPr><a:stCxn id=".." idx=".."/>` — which other shape (and which of
959 /// its connection sites) this connector's start point is attached to. `None` for a floating,
960 /// unattached start point (positioned by `properties`' own `xfrm` alone).
961 pub start_connection: Option<ShapeConnection>,
962 /// `<a:endCxn id=".." idx=".."/>` — same as `start_connection`, for the connector's end point.
963 pub end_connection: Option<ShapeConnection>,
964}
965
966impl Connector {
967 /// Creates a floating connector (no shape attachments) with no explicit position/size/line.
968 pub fn new(id: u32, name: impl Into<String>) -> Self {
969 Self {
970 id,
971 name: name.into(),
972 properties: ShapeProperties::new(),
973 start_connection: None,
974 end_connection: None,
975 }
976 }
977
978 /// Sets this connector's position/size/line/fill.
979 pub fn with_properties(mut self, properties: ShapeProperties) -> Self {
980 self.properties = properties;
981 self
982 }
983
984 /// Attaches this connector's start point to another shape's connection site.
985 pub fn with_start_connection(mut self, shape_id: u32, index: u32) -> Self {
986 self.start_connection = Some(ShapeConnection { shape_id, index });
987 self
988 }
989
990 /// Attaches this connector's end point to another shape's connection site.
991 pub fn with_end_connection(mut self, shape_id: u32, index: u32) -> Self {
992 self.end_connection = Some(ShapeConnection { shape_id, index });
993 self
994 }
995}
996
997/// One end of a [`Connector`]'s attachment to another shape (`<a:stCxn>`/`<a:endCxn>`,
998/// `CT_Connection`).
999#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1000pub struct ShapeConnection {
1001 /// `id` — the attached shape's `<p:cNvPr id="..">`.
1002 pub shape_id: u32,
1003 /// `idx` — which of that shape's connection sites (a preset-shape- specific numbered list of
1004 /// attachment points around its outline; this crate doesn't validate the index against the
1005 /// target shape's actual preset).
1006 pub index: u32,
1007}
1008
1009/// An embedded table (`<p:graphicFrame>` wrapping `<a:tbl>`, `a:graphicData
1010/// uri="../drawingml/2006/table"`) — DrawingML's own table vocabulary, distinct from
1011/// `word-ooxml::Table`'s WordprocessingML `w:tbl` (a different XML namespace/vocabulary entirely,
1012/// despite the conceptual overlap — no code is shared between the two, same posture as every other
1013/// cross-crate "same real-world concept, separate small copy" case in this workspace, e.g.
1014/// `PictureFormat`).
1015///
1016/// Structure (`p:graphicFrame`/`p:xfrm`/`a:tbl`/`a:tblGrid`/`a:tr`/`a:tc`) confirmed against real
1017/// fixtures: note the frame's own transform element is `<p:xfrm>`, not `<a:xfrm>`, same as
1018/// [`SlideChart`]'s. `<a:tableStyleId>` (a GUID referencing a table style) is written from
1019/// `style_id` when set (see [`SlideTableStyle`]), otherwise falls back to the same
1020/// fixed built-in default this crate's `tableStyles.xml` always declares, matching this crate's
1021/// existing posture on `p:xfrm`/theme boilerplate elsewhere.
1022#[derive(Debug, Clone, PartialEq)]
1023pub struct SlideTable {
1024 /// `<p:cNvPr id="..">` — same uniqueness rules as `AutoShape::id`.
1025 pub id: u32,
1026 /// `<p:cNvPr name="..">`.
1027 pub name: String,
1028 /// `<p:xfrm><a:off x=".." y="..">`, in EMUs.
1029 pub offset_emu: (i64, i64),
1030 /// `<p:xfrm><a:ext cx=".." cy="..">`, in EMUs.
1031 pub extent_emu: (i64, i64),
1032 /// `<a:tblGrid>`'s `<a:gridCol w="..">` widths, in EMUs, one per column (column count is
1033 /// derived from this, mirroring `word_ooxml::Table.column_widths`).
1034 pub column_widths_emu: Vec<i64>,
1035 pub rows: Vec<TableRow>,
1036 /// `<a:tableStyleId>` — references one of `Presentation.table_styles` by
1037 /// [`SlideTableStyle::id`]. `None` (the default) falls back to this crate's fixed built-in
1038 /// style, unchanged.
1039 pub style_id: Option<String>,
1040 /// `<a:tblPr firstRow=".">` — applies `style_id`'s `first_row` part to this table's actual
1041 /// first row (a header-row emphasis, e.g. bold text/a distinct fill). `false` (the default,
1042 /// matching `CT_TableProperties`'s own schema default) omits the attribute. **Without this, a
1043 /// referenced [`SlideTableStyle`]'s per-part formatting has no visible effect in real
1044 /// PowerPoint at all** — these six toggles are what actually turns each style part "on" for a
1045 /// given table; `style_id` alone only makes the style *available*. Grounded against a real
1046 /// fixture (`firstRow="1" bandRow="1"`).
1047 pub style_first_row: bool,
1048 /// `<a:tblPr firstCol="..">` — applies `style_id`'s `first_column` part. See
1049 /// [`SlideTable::style_first_row`]'s doc comment.
1050 pub style_first_column: bool,
1051 /// `<a:tblPr lastRow="..">` — applies `style_id`'s `last_row` part (a totals-row emphasis). See
1052 /// [`SlideTable::style_first_row`]'s doc comment.
1053 pub style_last_row: bool,
1054 /// `<a:tblPr lastCol="..">` — applies `style_id`'s `last_column` part. See
1055 /// [`SlideTable::style_first_row`]'s doc comment.
1056 pub style_last_column: bool,
1057 /// `<a:tblPr bandRow="..">` — applies `style_id`'s `band1_horizontal`/ `band2_horizontal` parts
1058 /// as alternating row stripes. See [`SlideTable::style_first_row`]'s doc comment.
1059 pub style_band_rows: bool,
1060 /// `<a:tblPr bandCol="..">` — applies `style_id`'s `band1_vertical`/ `band2_vertical` parts as
1061 /// alternating column stripes. See [`SlideTable::style_first_row`]'s doc comment.
1062 pub style_band_columns: bool,
1063}
1064
1065impl SlideTable {
1066 /// Creates an empty table (no rows/columns) at the given size, positioned at `(0, 0)`, using
1067 /// the fixed built-in table style with none of its parts (first row/column, last row/column,
1068 /// banding) actually applied.
1069 pub fn new(id: u32, name: impl Into<String>, width_emu: i64, height_emu: i64) -> Self {
1070 Self {
1071 id,
1072 name: name.into(),
1073 offset_emu: (0, 0),
1074 extent_emu: (width_emu, height_emu),
1075 column_widths_emu: Vec::new(),
1076 rows: Vec::new(),
1077 style_id: None,
1078 style_first_row: false,
1079 style_first_column: false,
1080 style_last_row: false,
1081 style_last_column: false,
1082 style_band_rows: false,
1083 style_band_columns: false,
1084 }
1085 }
1086
1087 /// Sets this table's position.
1088 pub fn with_offset(mut self, x_emu: i64, y_emu: i64) -> Self {
1089 self.offset_emu = (x_emu, y_emu);
1090 self
1091 }
1092
1093 /// Uses a custom table style (by id) instead of the fixed built-in one.
1094 pub fn with_style_id(mut self, style_id: impl Into<String>) -> Self {
1095 self.style_id = Some(style_id.into());
1096 self
1097 }
1098
1099 /// Sets the column widths (and, implicitly, the column count).
1100 pub fn with_column_widths(mut self, column_widths_emu: impl Into<Vec<i64>>) -> Self {
1101 self.column_widths_emu = column_widths_emu.into();
1102 self
1103 }
1104
1105 /// Appends a row.
1106 pub fn with_row(mut self, row: TableRow) -> Self {
1107 self.rows.push(row);
1108 self
1109 }
1110
1111 /// Applies the referenced style's first-row part to this table's actual first row (e.g. a bold
1112 /// header row).
1113 pub fn with_style_first_row(mut self, apply: bool) -> Self {
1114 self.style_first_row = apply;
1115 self
1116 }
1117
1118 /// Applies the referenced style's first-column part.
1119 pub fn with_style_first_column(mut self, apply: bool) -> Self {
1120 self.style_first_column = apply;
1121 self
1122 }
1123
1124 /// Applies the referenced style's last-row part (e.g. a totals row).
1125 pub fn with_style_last_row(mut self, apply: bool) -> Self {
1126 self.style_last_row = apply;
1127 self
1128 }
1129
1130 /// Applies the referenced style's last-column part.
1131 pub fn with_style_last_column(mut self, apply: bool) -> Self {
1132 self.style_last_column = apply;
1133 self
1134 }
1135
1136 /// Applies the referenced style's horizontal-banding parts (alternating row stripes).
1137 pub fn with_style_band_rows(mut self, apply: bool) -> Self {
1138 self.style_band_rows = apply;
1139 self
1140 }
1141
1142 /// Applies the referenced style's vertical-banding parts (alternating column stripes).
1143 pub fn with_style_band_columns(mut self, apply: bool) -> Self {
1144 self.style_band_columns = apply;
1145 self
1146 }
1147}
1148
1149/// One row of a [`SlideTable`] (`<a:tr h="..">`, `CT_TableRow`).
1150#[derive(Debug, Clone, PartialEq, Default)]
1151pub struct TableRow {
1152 /// `h` — row height, in EMUs.
1153 pub height_emu: i64,
1154 pub cells: Vec<TableCell>,
1155}
1156
1157impl TableRow {
1158 /// Creates an empty row (no cells) at the given height.
1159 pub fn new(height_emu: i64) -> Self {
1160 Self {
1161 height_emu,
1162 cells: Vec::new(),
1163 }
1164 }
1165
1166 /// Appends a cell.
1167 pub fn with_cell(mut self, cell: TableCell) -> Self {
1168 self.cells.push(cell);
1169 self
1170 }
1171}
1172
1173/// One cell of a [`TableRow`] (`<a:tc>`, `CT_TableCell`).
1174///
1175/// **The merge fields are not grounded on a real fixture** — see this module's own top-level doc
1176/// comment; none of this project's real table fixtures happens to exercise a merged cell, only the
1177/// base `graphicFrame`/`tbl`/`tblGrid`/`tr`/`tc` structure itself is fixture- confirmed. Built from
1178/// ECMA-376's own `CT_TableCell` attribute list, which is genuinely different from
1179/// `word_ooxml::TableCell`'s merge model despite the conceptual overlap: WordprocessingML lets a
1180/// spanned cell be omitted from its row entirely (`gridSpan` absorbs the columns, no placeholder
1181/// `w:tc` needed for what it covers), but `CT_TableCell` has no such shortcut — every row must
1182/// carry exactly one `<a:tc>` per `<a:gridCol>`, and a merge is expressed by marking the *absorbed*
1183/// cells with `hMerge`/`vMerge` (plain booleans, not an enum) rather than omitting them, while the
1184/// top/left cell of the merge carries `gridSpan`/`rowSpan` (the count, not a boolean).
1185/// [`SlideTable`]'s writer enforces the "one cell per grid column, every row" shape by padding a
1186/// short row with empty cells rather than silently producing an invalid `.pptx`.
1187#[derive(Debug, Clone, PartialEq, Default)]
1188pub struct TableCell {
1189 pub text_body: Option<TextBody>,
1190 /// `gridSpan` — how many grid columns this cell spans (a horizontal merge, the top/left cell
1191 /// only). `None` means `1` (no merge), matching `CT_TableCell`'s own schema default.
1192 pub horizontal_span: Option<u32>,
1193 /// `rowSpan` — how many rows this cell spans (a vertical merge, the top/left cell only). `None`
1194 /// means `1`.
1195 pub vertical_span: Option<u32>,
1196 /// `hMerge` — `true` marks this cell as absorbed by a horizontal merge from the cell to its
1197 /// left (this cell must still be present in the row, typically with no text of its own).
1198 pub horizontal_merge: bool,
1199 /// `vMerge` — `true` marks this cell as absorbed by a vertical merge from the cell above it.
1200 pub vertical_merge: bool,
1201 /// `<a:tcPr>` (`CT_TableCellProperties`) — this cell's own margins, vertical anchor, borders,
1202 /// and background fill, overriding whatever the table's style would otherwise apply. `None`
1203 /// (the default) omits `<a:tcPr>` entirely — the cell simply inherits everything from the table
1204 /// style, same "no override" default every other optional field in this crate uses.
1205 pub properties: Option<TableCellProperties>,
1206}
1207
1208impl TableCell {
1209 /// Creates an empty cell (no text, no merge, no property overrides).
1210 pub fn new() -> Self {
1211 Self::default()
1212 }
1213
1214 /// Sets this cell's text content.
1215 pub fn with_text_body(mut self, text_body: TextBody) -> Self {
1216 self.text_body = Some(text_body);
1217 self
1218 }
1219
1220 /// Sets this cell's own margin/anchor/border/fill overrides (`<a:tcPr>`).
1221 pub fn with_properties(mut self, properties: TableCellProperties) -> Self {
1222 self.properties = Some(properties);
1223 self
1224 }
1225
1226 /// Marks this cell as the top/left cell of a horizontal merge spanning `span` grid columns —
1227 /// the `span - 1` cells to its right still need to be present in the row, each created via
1228 /// [`TableCell::horizontally_merged`].
1229 pub fn with_horizontal_span(mut self, span: u32) -> Self {
1230 self.horizontal_span = Some(span);
1231 self
1232 }
1233
1234 /// Marks this cell as the top/left cell of a vertical merge spanning `span` rows — the `span -
1235 /// 1` cells below it (same column, following rows) still need to be present, each created via
1236 /// [`TableCell::vertically_merged`].
1237 pub fn with_vertical_span(mut self, span: u32) -> Self {
1238 self.vertical_span = Some(span);
1239 self
1240 }
1241
1242 /// An empty placeholder cell absorbed by a horizontal merge from its left neighbor
1243 /// (`hMerge="1"`).
1244 pub fn horizontally_merged() -> Self {
1245 Self {
1246 horizontal_merge: true,
1247 ..Self::default()
1248 }
1249 }
1250
1251 /// An empty placeholder cell absorbed by a vertical merge from the cell above it
1252 /// (`vMerge="1"`).
1253 pub fn vertically_merged() -> Self {
1254 Self {
1255 vertical_merge: true,
1256 ..Self::default()
1257 }
1258 }
1259}
1260
1261/// A [`TableCell`]'s own property overrides (`<a:tcPr>`, `CT_TableCellProperties`). Not modeled:
1262/// `anchorCtr`/`horzOverflow` attributes, the diagonal borders (`<a:lnTlToBr>`/`<a:lnBlToTr>`), and
1263/// `<a:cell3D>` (bevel/lighting effects) — all secondary refinements on top of margins, vertical
1264/// anchor, four straight borders, and background fill. Grounded against a real fixture, which
1265/// confirmed the attribute set and the
1266/// `lnL`/`lnR`/`lnT`/`lnB` then fill-choice element order (`CT_TableCellProperties`'s own
1267/// sequence).
1268#[derive(Debug, Clone, PartialEq, Default)]
1269pub struct TableCellProperties {
1270 /// `marL`, in EMUs.
1271 pub margin_left_emu: Option<i64>,
1272 /// `marT`, in EMUs.
1273 pub margin_top_emu: Option<i64>,
1274 /// `marR`, in EMUs.
1275 pub margin_right_emu: Option<i64>,
1276 /// `marB`, in EMUs.
1277 pub margin_bottom_emu: Option<i64>,
1278 /// `anchor` — vertical alignment of this cell's text within its own height. Reuses
1279 /// [`drawing::TextAnchor`] directly, the same type `<a:bodyPr anchor="..">` uses
1280 /// (`CT_TableCellProperties` and `CT_TextBodyProperties` both reference
1281 /// `ST_TextAnchoringType`).
1282 pub anchor: Option<TextAnchor>,
1283 /// `<a:lnL>` — this cell's left border.
1284 pub border_left: Option<Line>,
1285 /// `<a:lnR>` — this cell's right border.
1286 pub border_right: Option<Line>,
1287 /// `<a:lnT>` — this cell's top border.
1288 pub border_top: Option<Line>,
1289 /// `<a:lnB>` — this cell's bottom border.
1290 pub border_bottom: Option<Line>,
1291 /// `EG_FillProperties` — this cell's own background fill, overriding the table style's own cell
1292 /// fill.
1293 pub fill: Option<Fill>,
1294}
1295
1296impl TableCellProperties {
1297 pub fn new() -> Self {
1298 Self::default()
1299 }
1300
1301 /// Sets all four margins at once, in EMUs.
1302 pub fn with_margins_emu(mut self, left: i64, top: i64, right: i64, bottom: i64) -> Self {
1303 self.margin_left_emu = Some(left);
1304 self.margin_top_emu = Some(top);
1305 self.margin_right_emu = Some(right);
1306 self.margin_bottom_emu = Some(bottom);
1307 self
1308 }
1309
1310 /// Sets this cell's vertical text anchor.
1311 pub fn with_anchor(mut self, anchor: TextAnchor) -> Self {
1312 self.anchor = Some(anchor);
1313 self
1314 }
1315
1316 /// Sets this cell's left border.
1317 pub fn with_border_left(mut self, border: Line) -> Self {
1318 self.border_left = Some(border);
1319 self
1320 }
1321
1322 /// Sets this cell's right border.
1323 pub fn with_border_right(mut self, border: Line) -> Self {
1324 self.border_right = Some(border);
1325 self
1326 }
1327
1328 /// Sets this cell's top border.
1329 pub fn with_border_top(mut self, border: Line) -> Self {
1330 self.border_top = Some(border);
1331 self
1332 }
1333
1334 /// Sets this cell's bottom border.
1335 pub fn with_border_bottom(mut self, border: Line) -> Self {
1336 self.border_bottom = Some(border);
1337 self
1338 }
1339
1340 /// Sets this cell's own background fill.
1341 pub fn with_fill(mut self, fill: Fill) -> Self {
1342 self.fill = Some(fill);
1343 self
1344 }
1345}
1346
1347/// A slide comment (`ppt/comments/commentN.xml`'s own `<p:cm>`, `CT_Comment`) — the legacy comment
1348/// mechanism ECMA-376 itself defines (pre-2016 "modern threaded comments", which PowerPoint now
1349/// stores in a different, Microsoft-proprietary way this crate doesn't model).
1350///
1351/// **Not grounded on a real fixture** — see this module's own top-level doc comment. Structure
1352/// follows ECMA-376's `CT_Comment`/`CT_CommentList`/ `CT_CommentAuthor`/`CT_CommentAuthorList`
1353/// directly (§19.4): a `<p:cm>`'s `authorId` attribute references a `<p:cmAuthor>` entry in the
1354/// package-wide `ppt/commentAuthors.xml` by numeric id — this crate hides that indirection
1355/// entirely, storing the author's name/initials directly on each `SlideComment` and having the
1356/// writer deduplicate/number authors automatically (an author appearing on several comments becomes
1357/// one `<p:cmAuthor>` entry, matching how real PowerPoint itself avoids duplicate author entries).
1358#[derive(Debug, Clone, PartialEq)]
1359pub struct SlideComment {
1360 /// `<p:cmAuthor name="..">` — this comment's author's display name.
1361 pub author: String,
1362 /// `<p:cmAuthor initials="..">`.
1363 pub initials: String,
1364 /// `<p:cm dt="..">` (`ST_DateTime`) — an ISO-8601 date/time string (e.g.
1365 /// `"2024-01-15T10:30:00.000"`). Stored as a plain string, same posture as every other
1366 /// date/time field in this workspace (no dedicated date/time type anywhere).
1367 pub date: String,
1368 /// `<p:pos x=".." y="..">`, in EMUs — where the comment's marker sits on the slide.
1369 pub position_emu: (i64, i64),
1370 /// `<p:text>` — the comment's own body text (plain text only, unlike e.g.
1371 /// `word_ooxml::Comment`'s block-level content — `CT_Comment`'s `text` is a plain `xsd:string`,
1372 /// no rich formatting at all).
1373 pub text: String,
1374}
1375
1376impl SlideComment {
1377 /// Creates a comment at position `(0, 0)`.
1378 pub fn new(
1379 author: impl Into<String>,
1380 initials: impl Into<String>,
1381 date: impl Into<String>,
1382 text: impl Into<String>,
1383 ) -> Self {
1384 Self {
1385 author: author.into(),
1386 initials: initials.into(),
1387 date: date.into(),
1388 position_emu: (0, 0),
1389 text: text.into(),
1390 }
1391 }
1392
1393 /// Sets this comment's marker position.
1394 pub fn with_position(mut self, x_emu: i64, y_emu: i64) -> Self {
1395 self.position_emu = (x_emu, y_emu);
1396 self
1397 }
1398}
1399
1400/// A presentation's customizable color/font theme (`ppt/theme/theme1.xml`'s own
1401/// `<a:clrScheme>`/`<a:fontScheme>`).
1402///
1403/// Deliberately mirrors `word_ooxml::Theme`/`ColorScheme`/`FontScheme` by name and shape, not by
1404/// crate dependency — this workspace's established posture for a shared real-world concept with no
1405/// shared crate backing it (see `PictureFormat`'s own doc comment for the same reasoning). Unlike
1406/// `word_ooxml::Theme` (genuinely optional — `word/theme/theme1.xml` is only written when
1407/// `Document.theme` is set), a PowerPoint theme part is *mandatory*: `Presentation.theme: None`
1408/// still always produces a full `theme1.xml`, just with [`Theme::office_default`]'s colors/fonts
1409/// rather than a caller-supplied palette — see `Presentation`'s own doc comment.
1410///
1411/// Real PowerPoint files can and do carry more than one distinct theme (a real fixture has both
1412/// `theme1.xml` and `theme2.xml`, the latter the notes master's own); this crate, like
1413/// `word-ooxml`, still only exposes a single customizable theme for the slide master (the notes
1414/// master's own separate theme part stays fixed at `Theme::office_default`'s content regardless).
1415///
1416/// `fmtScheme` (`CT_BaseStyles` requires it alongside `clrScheme`/ `fontScheme`) stays a fixed,
1417/// non-configurable copy of Office's own built-in default, same scope limit as
1418/// `word_ooxml::Theme`'s own `fmtScheme` — irrelevant to `word-ooxml` there, but genuinely relevant
1419/// to a slide's shapes here; still out of scope.
1420#[derive(Debug, Clone, PartialEq, Eq)]
1421pub struct Theme {
1422 /// The theme's name (`<a:theme name="..">`), also reused unchanged as the nested
1423 /// `<a:clrScheme>`/`<a:fontScheme>` elements' own `name` attribute, same convention as
1424 /// `word_ooxml::Theme`.
1425 pub name: String,
1426 pub colors: ColorScheme,
1427 pub fonts: FontScheme,
1428}
1429
1430/// A theme's 12-color palette (`<a:clrScheme>`, `CT_ColorScheme`) — mirrors
1431/// `word_ooxml::ColorScheme` by name/shape (see that type's own doc comment for the full rationale,
1432/// including why every slot is a plain RGB hex string rather than modeling `<a:sysClr>`'s live
1433/// system-color binding).
1434#[derive(Debug, Clone, PartialEq, Eq)]
1435pub struct ColorScheme {
1436 pub dark1: String,
1437 pub light1: String,
1438 pub dark2: String,
1439 pub light2: String,
1440 pub accent1: String,
1441 pub accent2: String,
1442 pub accent3: String,
1443 pub accent4: String,
1444 pub accent5: String,
1445 pub accent6: String,
1446 pub hyperlink: String,
1447 pub followed_hyperlink: String,
1448}
1449
1450/// A theme's font scheme (`<a:fontScheme>`, `CT_FontScheme`) — mirrors `word_ooxml::FontScheme` by
1451/// name/shape (Latin typeface only, same scope limit).
1452#[derive(Debug, Clone, PartialEq, Eq)]
1453pub struct FontScheme {
1454 pub major_latin: String,
1455 pub minor_latin: String,
1456}
1457
1458impl Theme {
1459 /// Office's own built-in default theme ("Office": Calibri Light/ Calibri, the classic
1460 /// blue-accented palette) — byte-for-byte the same colors/fonts this crate's writer has always
1461 /// produced as its fixed theme, now expressed as data instead of a hardcoded XML literal.
1462 pub fn office_default() -> Self {
1463 Self {
1464 name: "Office".to_string(),
1465 colors: ColorScheme::office_default(),
1466 fonts: FontScheme::office_default(),
1467 }
1468 }
1469}
1470
1471impl ColorScheme {
1472 pub fn office_default() -> Self {
1473 Self {
1474 dark1: "000000".to_string(),
1475 light1: "FFFFFF".to_string(),
1476 dark2: "44546A".to_string(),
1477 light2: "E7E6E6".to_string(),
1478 accent1: "4472C4".to_string(),
1479 accent2: "ED7D31".to_string(),
1480 accent3: "A5A5A5".to_string(),
1481 accent4: "FFC000".to_string(),
1482 accent5: "5B9BD5".to_string(),
1483 accent6: "70AD47".to_string(),
1484 hyperlink: "0563C1".to_string(),
1485 followed_hyperlink: "954F72".to_string(),
1486 }
1487 }
1488}
1489
1490impl FontScheme {
1491 pub fn office_default() -> Self {
1492 Self {
1493 major_latin: "Calibri Light".to_string(),
1494 minor_latin: "Calibri".to_string(),
1495 }
1496 }
1497}
1498
1499/// Document metadata (`docProps/core.xml`'s Dublin Core fields plus `docProps/app.xml`'s
1500/// `Company`/`Manager`/`HyperlinkBase`, and `docProps/custom.xml`'s user-defined properties). A
1501/// direct port of `excel-ooxml::DocumentProperties`'s own shape (same fields, same posture — see
1502/// that type's own doc comment for the full rationale); no PowerPoint-specific fields added, since
1503/// `docProps/app.xml`'s schema (`CT_Properties`, extended-properties) is shared verbatim across
1504/// every OOXML application, only differing in which app-specific statistics fields (`Slides` here
1505/// vs. `Sheets`/`Words` elsewhere) each host actually sets — this crate already sets `Slides`
1506/// unconditionally in `writer.rs`, independent of this type.
1507#[derive(Debug, Default, Clone, PartialEq)]
1508pub struct DocumentProperties {
1509 /// `docProps/core.xml`'s `<dc:title>`.
1510 pub title: Option<String>,
1511 /// `docProps/core.xml`'s `<dc:creator>`.
1512 pub author: Option<String>,
1513 /// `docProps/core.xml`'s `<dc:subject>`.
1514 pub subject: Option<String>,
1515 /// `docProps/core.xml`'s `<cp:keywords>`.
1516 pub keywords: Option<String>,
1517 /// `docProps/app.xml`'s `<Company>`.
1518 pub company: Option<String>,
1519 /// `docProps/app.xml`'s `<Manager>`.
1520 pub manager: Option<String>,
1521 /// `docProps/app.xml`'s `<HyperlinkBase>`.
1522 pub hyperlink_base: Option<String>,
1523 /// User-defined custom document properties (`docProps/custom.xml`,
1524 /// `CT_Properties`/`CT_Property`). A `Vec` of `(name, value)` pairs, not a map: insertion order
1525 /// is preserved and determines the sequential `pid` each entry gets when written, matching
1526 /// `excel-ooxml`/`word-ooxml`'s own `custom_properties` convention exactly. Only written if
1527 /// non-empty.
1528 pub custom_properties: Vec<(String, CustomPropertyValue)>,
1529}
1530
1531/// A single custom document property's value — same 4 variants as
1532/// `excel-ooxml::CustomPropertyValue`/`word_ooxml::CustomPropertyValue` (`CT_Property`'s value
1533/// `xsd:choice` has several more — vectors, blobs, currency, a date variant — not modeled here,
1534/// same "simple case first" posture as those two sibling types).
1535#[derive(Debug, Clone, PartialEq)]
1536pub enum CustomPropertyValue {
1537 /// A text value (`vt:lpwstr`).
1538 Text(String),
1539 /// A boolean value (`vt:bool`).
1540 Bool(bool),
1541 /// A 32-bit signed integer value (`vt:i4`).
1542 Int(i32),
1543 /// A 64-bit floating-point value (`vt:r8`).
1544 Number(f64),
1545}
1546
1547impl DocumentProperties {
1548 pub fn new() -> Self {
1549 Self::default()
1550 }
1551
1552 pub fn with_title(mut self, title: impl Into<String>) -> Self {
1553 self.title = Some(title.into());
1554 self
1555 }
1556
1557 pub fn with_author(mut self, author: impl Into<String>) -> Self {
1558 self.author = Some(author.into());
1559 self
1560 }
1561
1562 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
1563 self.subject = Some(subject.into());
1564 self
1565 }
1566
1567 pub fn with_keywords(mut self, keywords: impl Into<String>) -> Self {
1568 self.keywords = Some(keywords.into());
1569 self
1570 }
1571
1572 pub fn with_company(mut self, company: impl Into<String>) -> Self {
1573 self.company = Some(company.into());
1574 self
1575 }
1576
1577 pub fn with_manager(mut self, manager: impl Into<String>) -> Self {
1578 self.manager = Some(manager.into());
1579 self
1580 }
1581
1582 pub fn with_hyperlink_base(mut self, hyperlink_base: impl Into<String>) -> Self {
1583 self.hyperlink_base = Some(hyperlink_base.into());
1584 self
1585 }
1586
1587 /// Appends one custom document property.
1588 pub fn with_custom_property(
1589 mut self,
1590 name: impl Into<String>,
1591 value: CustomPropertyValue,
1592 ) -> Self {
1593 self.custom_properties.push((name.into(), value));
1594 self
1595 }
1596}
1597
1598/// A custom table style (`<a:tblStyle styleId="." styleName=".">`, `CT_TableStyle`). Package-wide
1599/// ([`Presentation::table_styles`]), referenced by id from [`SlideTable::style_id`].
1600///
1601/// Grounded against a real fixture (`ppt/tableStyles.xml`, the built-in "Medium Style 2 - Accent 1"
1602/// style). Real `CT_TableStyle` has 12 optional "table part" children
1603/// (`wholeTbl`/`band1H`/`band2H`/`band1V`/`band2V`/`firstRow`/`lastRow`/
1604/// `firstCol`/`lastCol`/`neCell`/`nwCell`/`seCell`/`swCell`); this crate models the 9
1605/// commonly-authored ones (everything but the four single- corner-cell variants, a real-world
1606/// rarity). Within each part (`CT_TablePartStyle`), only fill + bold/italic + text color are
1607/// modeled (see [`TableStylePart`]) — no per-side border customization (`CT_TableCellBorderStyle`)
1608/// and no `fontRef` index, both schema- optional. A deliberate scope reduction, not a
1609/// fixture-exhaustive mirror of every real PowerPoint table style's structure.
1610#[derive(Debug, Clone, PartialEq, Default)]
1611pub struct SlideTableStyle {
1612 /// `styleId` (`ST_Guid`) — referenced from [`SlideTable::style_id`].
1613 pub id: String,
1614 /// `styleName`.
1615 pub name: String,
1616 /// `<a:wholeTbl>` — the base style every other part layers on top of.
1617 pub whole_table: Option<TableStylePart>,
1618 /// `<a:band1H>` — odd row banding.
1619 pub band1_horizontal: Option<TableStylePart>,
1620 /// `<a:band2H>` — even row banding.
1621 pub band2_horizontal: Option<TableStylePart>,
1622 /// `<a:band1V>` — odd column banding.
1623 pub band1_vertical: Option<TableStylePart>,
1624 /// `<a:band2V>` — even column banding.
1625 pub band2_vertical: Option<TableStylePart>,
1626 /// `<a:firstRow>` — header row.
1627 pub first_row: Option<TableStylePart>,
1628 /// `<a:lastRow>` — total/footer row.
1629 pub last_row: Option<TableStylePart>,
1630 /// `<a:firstCol>`.
1631 pub first_column: Option<TableStylePart>,
1632 /// `<a:lastCol>`.
1633 pub last_column: Option<TableStylePart>,
1634}
1635
1636impl SlideTableStyle {
1637 /// Creates a table style with the given id/name and no parts set yet (an empty `<a:tblStyle>` —
1638 /// schema-valid, though visually a no-op until at least one part is set).
1639 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
1640 Self {
1641 id: id.into(),
1642 name: name.into(),
1643 ..Default::default()
1644 }
1645 }
1646
1647 pub fn with_whole_table(mut self, part: TableStylePart) -> Self {
1648 self.whole_table = Some(part);
1649 self
1650 }
1651
1652 pub fn with_band1_horizontal(mut self, part: TableStylePart) -> Self {
1653 self.band1_horizontal = Some(part);
1654 self
1655 }
1656
1657 pub fn with_band2_horizontal(mut self, part: TableStylePart) -> Self {
1658 self.band2_horizontal = Some(part);
1659 self
1660 }
1661
1662 pub fn with_band1_vertical(mut self, part: TableStylePart) -> Self {
1663 self.band1_vertical = Some(part);
1664 self
1665 }
1666
1667 pub fn with_band2_vertical(mut self, part: TableStylePart) -> Self {
1668 self.band2_vertical = Some(part);
1669 self
1670 }
1671
1672 pub fn with_first_row(mut self, part: TableStylePart) -> Self {
1673 self.first_row = Some(part);
1674 self
1675 }
1676
1677 pub fn with_last_row(mut self, part: TableStylePart) -> Self {
1678 self.last_row = Some(part);
1679 self
1680 }
1681
1682 pub fn with_first_column(mut self, part: TableStylePart) -> Self {
1683 self.first_column = Some(part);
1684 self
1685 }
1686
1687 pub fn with_last_column(mut self, part: TableStylePart) -> Self {
1688 self.last_column = Some(part);
1689 self
1690 }
1691}
1692
1693/// One "table part" style (`CT_TableStyleTextStyle` + `CT_TableStyleCellStyle` combined — real
1694/// PowerPoint always writes both wrapped in the same `<a:wholeTbl>`/`<a:band1H>`/etc. element, so
1695/// this crate models them as a single value rather than two separate optional fields) — see
1696/// [`SlideTableStyle`]'s own doc comment for scope.
1697#[derive(Debug, Clone, PartialEq, Default)]
1698pub struct TableStylePart {
1699 /// `<a:tcStyle><a:fill>{fill}</a:fill></a:tcStyle>` — the cell's own background. `None` omits
1700 /// `<a:fill>` (inherits).
1701 pub fill: Option<Fill>,
1702 /// `<a:tcTxStyle b="on">` — bold text.
1703 pub bold: bool,
1704 /// `<a:tcTxStyle i="on">` — italic text.
1705 pub italic: bool,
1706 /// `<a:tcTxStyle>{color}</a:tcTxStyle>` — the cell's own text color. `None` omits the color
1707 /// choice (inherits). Reuses [`Color`] verbatim — like the rest of this crate's own color
1708 /// usage, a theme-relative `<a:schemeClr>` reference isn't modeled.
1709 pub text_color: Option<Color>,
1710}
1711
1712impl TableStylePart {
1713 pub fn new() -> Self {
1714 Self::default()
1715 }
1716
1717 pub fn with_fill(mut self, fill: Fill) -> Self {
1718 self.fill = Some(fill);
1719 self
1720 }
1721
1722 pub fn with_bold(mut self, bold: bool) -> Self {
1723 self.bold = bold;
1724 self
1725 }
1726
1727 pub fn with_italic(mut self, italic: bool) -> Self {
1728 self.italic = italic;
1729 self
1730 }
1731
1732 pub fn with_text_color(mut self, color: Color) -> Self {
1733 self.text_color = Some(color);
1734 self
1735 }
1736}
1737
1738/// A TrueType/OpenType font embedded directly in the package (`<p:embeddedFont><p:font
1739/// typeface="."/>{variants}</p:embeddedFont>`, `CT_EmbeddedFontListEntry`).
1740///
1741/// **Not grounded on a real fixture** — no `.pptx` in this project's local corpus embeds a font.
1742/// Built directly from ECMA-376's own `CT_EmbeddedFontListEntry`/ `CT_EmbeddedFontDataId` schema
1743/// text instead — flagged for extra scrutiny once opened in real PowerPoint, same posture already
1744/// used for [`Connector`]/[`SlideComment`] and [`crate::MediaFormat`]'s `p14:media` non-support.
1745#[derive(Debug, Clone, PartialEq, Default)]
1746pub struct EmbeddedFont {
1747 /// `<p:font typeface="..">` — the font family name real PowerPoint resolves text runs against.
1748 pub typeface: String,
1749 /// `<p:regular r:id="..">`'s own `.fntdata` bytes.
1750 pub regular: Option<Vec<u8>>,
1751 /// `<p:bold r:id="..">`'s own `.fntdata` bytes.
1752 pub bold: Option<Vec<u8>>,
1753 /// `<p:italic r:id="..">`'s own `.fntdata` bytes.
1754 pub italic: Option<Vec<u8>>,
1755 /// `<p:boldItalic r:id="..">`'s own `.fntdata` bytes.
1756 pub bold_italic: Option<Vec<u8>>,
1757}
1758
1759impl EmbeddedFont {
1760 /// Creates an embedded font entry with no variants set yet (use
1761 /// `with_regular`/`with_bold`/`with_italic`/`with_bold_italic` to attach at least one).
1762 pub fn new(typeface: impl Into<String>) -> Self {
1763 Self {
1764 typeface: typeface.into(),
1765 ..Default::default()
1766 }
1767 }
1768
1769 pub fn with_regular(mut self, data: impl Into<Vec<u8>>) -> Self {
1770 self.regular = Some(data.into());
1771 self
1772 }
1773
1774 pub fn with_bold(mut self, data: impl Into<Vec<u8>>) -> Self {
1775 self.bold = Some(data.into());
1776 self
1777 }
1778
1779 pub fn with_italic(mut self, data: impl Into<Vec<u8>>) -> Self {
1780 self.italic = Some(data.into());
1781 self
1782 }
1783
1784 pub fn with_bold_italic(mut self, data: impl Into<Vec<u8>>) -> Self {
1785 self.bold_italic = Some(data.into());
1786 self
1787 }
1788}