Skip to main content

pptx_rs/oxml/
slide.rs

1//! 单个 slide XML:`<p:sld>`。
2//!
3//! 每一张幻灯片对应一个 part:`/ppt/slides/slideN.xml`。
4//!
5//! # 元素结构
6//!
7//! ```text
8//! <p:sld>
9//!   <p:cSld>
10//!     <p:spTree>
11//!       <p:nvGrpSpPr>...</p:nvGrpSpPr>   spTree 必填项
12//!       <p:grpSpPr>...</p:grpSpPr>
13//!       <p:sp>...</p:sp>                ← 形状
14//!       <p:pic>...</p:pic>
15//!       <p:grpSp>...</p:grpSp>
16//!       <p:cxnSp>...</p:cxnSp>
17//!       <p:graphicFrame>...</p:graphicFrame>
18//!     </p:spTree>
19//!   </p:cSld>
20//!   <p:clrMapOvr>...</p:clrMapOvr>      可选的颜色映射覆盖
21//!   <p:transition>...</p:transition>     可选的过渡
22//!   <p:timing>...</p:timing>            可选的时序
23//! </p:sld>
24//! ```
25//!
26//! # 与 python-pptx 的对应
27//!
28//! - `pptx.oxml.slide.Slide` ←→ [`Sld`];
29//! - `pptx.shapes.shapetree` 内部枚举 ←→ [`SlideShape`]。
30
31use crate::oxml::ns::{NS_DRAWING_MAIN, NS_PRESENTATION_MAIN};
32use crate::oxml::shape::{Connector, GraphicFrame, Group, Pic, Sp};
33use crate::oxml::txbody::TextBody;
34use crate::oxml::writer::XmlWriter;
35
36/// 一张幻灯片。
37#[derive(Clone, Debug, Default)]
38pub struct Sld {
39    /// 内部 ID(`id="256"` 等)。
40    pub id: u32,
41    /// slide 关系 id(指向 `slideLayoutN.xml`)。
42    pub layout_rid: String,
43    /// 用户可读的 slide 名(对应 `<p:sld>/<p:cSld>/@name`,可选)。
44    ///
45    /// 对标 python-pptx `Slide.name`。空字符串表示未命名。
46    /// 序列化时若非空,写到 `<p:cSld name="...">`。
47    pub name: String,
48    /// 幻灯片背景(`<p:bg>`)。`None` 表示遵循母版背景。
49    pub background: Option<SlideBackground>,
50    /// 幻灯片中的形状列表。
51    pub shapes: Vec<SlideShape>,
52    /// 备注(notes)。
53    pub notes: Option<TextBody>,
54    /// 幻灯片过渡(`<p:transition>`,可选)。
55    pub transition: Option<Transition>,
56    /// 形状树(spTree)末尾的扩展列表。
57    pub ext_lst: Option<crate::oxml::shape::ExtensionList>,
58}
59
60/// 过渡速度。
61#[derive(Clone, Debug, Default, PartialEq, Eq)]
62pub enum TransitionSpeed {
63    /// 慢速。
64    Slow,
65    /// 中速(默认)。
66    #[default]
67    Medium,
68    /// 快速。
69    Fast,
70}
71
72impl TransitionSpeed {
73    /// 转 OOXML 属性值。
74    pub fn as_str(&self) -> &'static str {
75        match self {
76            TransitionSpeed::Slow => "slow",
77            TransitionSpeed::Medium => "med",
78            TransitionSpeed::Fast => "fast",
79        }
80    }
81}
82
83/// 过渡类型(`<p:transition>` 的子元素)。
84#[derive(Clone, Debug, Default, PartialEq, Eq)]
85pub enum TransitionType {
86    /// 淡入淡出(`<p:fade thruBlk="1|0"/>`)。
87    Fade { thru_blk: bool },
88    /// 推入(`<p:push dir="..."/>`)。
89    Push { dir: TransitionDirection },
90    /// 擦除(`<p:wipe dir="..."/>`)。
91    Wipe { dir: TransitionDirection },
92    /// 分割(`<p:split orient="...|..." dir="..."/>`)。
93    Split {
94        orient: SplitOrientation,
95        dir: TransitionDirection,
96    },
97    /// 覆盖(`<p:cover dir="..."/>`)。
98    Cover { dir: TransitionDirection },
99    /// 拉出(`<p:pull dir="..."/>`)。
100    Pull { dir: TransitionDirection },
101    /// 切割(`<p:cut thruBlk="1|0"/>`)。
102    Cut { thru_blk: bool },
103    /// 缩放(`<p:zoom dir="..."/>`)。
104    Zoom { dir: TransitionDirection },
105    /// 变形(`<p:morph option="byObject|byWord|byChar"/>`)。
106    Morph { option: MorphOption },
107    /// 无过渡(默认)。
108    #[default]
109    None,
110}
111
112/// 过渡方向(`dir` 属性)。
113#[derive(Clone, Debug, Default, PartialEq, Eq)]
114pub enum TransitionDirection {
115    /// 向左(`dir="l"`)。
116    Left,
117    /// 向右(`dir="r"`,默认)。
118    #[default]
119    Right,
120    /// 向上(`dir="u"`)。
121    Up,
122    /// 向下(`dir="d"`)。
123    Down,
124    /// 左上(`dir="lu"`)。
125    LeftUp,
126    /// 左下(`dir="ld"`)。
127    LeftDown,
128    /// 右上(`dir="ru"`)。
129    RightUp,
130    /// 右下(`dir="rd"`)。
131    RightDown,
132}
133
134impl TransitionDirection {
135    /// 转 OOXML 属性值。
136    pub fn as_str(&self) -> &'static str {
137        match self {
138            TransitionDirection::Left => "l",
139            TransitionDirection::Right => "r",
140            TransitionDirection::Up => "u",
141            TransitionDirection::Down => "d",
142            TransitionDirection::LeftUp => "lu",
143            TransitionDirection::LeftDown => "ld",
144            TransitionDirection::RightUp => "ru",
145            TransitionDirection::RightDown => "rd",
146        }
147    }
148}
149
150/// 分割方向(`orient` 属性)。
151#[derive(Clone, Debug, Default, PartialEq, Eq)]
152pub enum SplitOrientation {
153    /// 水平(`orient="horz"`,默认)。
154    #[default]
155    Horizontal,
156    /// 垂直(`orient="vert"`)。
157    Vertical,
158}
159
160impl SplitOrientation {
161    /// 转 OOXML 属性值。
162    pub fn as_str(&self) -> &'static str {
163        match self {
164            SplitOrientation::Horizontal => "horz",
165            SplitOrientation::Vertical => "vert",
166        }
167    }
168}
169
170/// Morph 选项(`option` 属性)。
171#[derive(Clone, Debug, Default, PartialEq, Eq)]
172pub enum MorphOption {
173    /// 按对象(默认)。
174    #[default]
175    ByObject,
176    /// 按单词。
177    ByWord,
178    /// 按字符。
179    ByChar,
180}
181
182impl MorphOption {
183    /// 转 OOXML 属性值。
184    pub fn as_str(&self) -> &'static str {
185        match self {
186            MorphOption::ByObject => "byObject",
187            MorphOption::ByWord => "byWord",
188            MorphOption::ByChar => "byChar",
189        }
190    }
191}
192
193/// 幻灯片过渡(`<p:transition>`)。
194///
195/// 对应 OOXML 中 `<p:sld>` 内 `<p:cSld>` 之后的 `<p:transition>` 元素。
196#[derive(Clone, Debug, Default)]
197pub struct Transition {
198    /// 过渡速度。
199    pub speed: TransitionSpeed,
200    /// 是否点击换片(`advClick="1|0"`,默认 true)。
201    pub advance_click: bool,
202    /// 自动换片时间(毫秒,`advTm="..."`)。`None` 表示不自动换片。
203    pub advance_after_ms: Option<u32>,
204    /// 过渡类型。
205    pub transition_type: TransitionType,
206}
207
208impl Transition {
209    /// 写出 `<p:transition>` 元素。
210    pub fn write_xml(&self, w: &mut XmlWriter) {
211        let mut attrs: Vec<(&str, &str)> = Vec::new();
212        attrs.push(("spd", self.speed.as_str()));
213        if !self.advance_click {
214            attrs.push(("advClick", "0"));
215        }
216        let adv_tm_s;
217        if let Some(ms) = self.advance_after_ms {
218            adv_tm_s = ms.to_string();
219            attrs.push(("advTm", adv_tm_s.as_str()));
220        }
221        w.open_with("p:transition", &attrs);
222        // 子元素:过渡类型
223        match &self.transition_type {
224            TransitionType::Fade { thru_blk } => {
225                if *thru_blk {
226                    w.empty_with("p:fade", &[("thruBlk", "1")]);
227                } else {
228                    w.empty("p:fade");
229                }
230            }
231            TransitionType::Push { dir } => {
232                w.empty_with("p:push", &[("dir", dir.as_str())]);
233            }
234            TransitionType::Wipe { dir } => {
235                w.empty_with("p:wipe", &[("dir", dir.as_str())]);
236            }
237            TransitionType::Split { orient, dir } => {
238                w.empty_with(
239                    "p:split",
240                    &[("orient", orient.as_str()), ("dir", dir.as_str())],
241                );
242            }
243            TransitionType::Cover { dir } => {
244                w.empty_with("p:cover", &[("dir", dir.as_str())]);
245            }
246            TransitionType::Pull { dir } => {
247                w.empty_with("p:pull", &[("dir", dir.as_str())]);
248            }
249            TransitionType::Cut { thru_blk } => {
250                if *thru_blk {
251                    w.empty_with("p:cut", &[("thruBlk", "1")]);
252                } else {
253                    w.empty("p:cut");
254                }
255            }
256            TransitionType::Zoom { dir } => {
257                w.empty_with("p:zoom", &[("dir", dir.as_str())]);
258            }
259            TransitionType::Morph { option } => {
260                w.empty_with("p:morph", &[("option", option.as_str())]);
261            }
262            TransitionType::None => {}
263        }
264        w.close("p:transition");
265    }
266}
267
268/// 幻灯片背景(对应 `<p:bg>`)。
269///
270/// OOXML 中 `<p:bg>` 是 `<p:cSld>` 的第一个子元素(在 `<p:spTree>` 之前)。
271/// 支持两种模式:
272/// - `BackgroundProperty`:显式背景属性(`<p:bgPr>`),如纯色填充;
273/// - `BackgroundReference`:引用主题背景(`<p:bgRef>`),idx 指向主题中的背景样式。
274#[derive(Clone, Debug)]
275pub enum SlideBackground {
276    /// 显式背景属性(`<p:bgPr>`)。
277    Property(BackgroundProperty),
278    /// 背景引用(`<p:bgRef idx="...">`)。
279    Reference(BackgroundReference),
280}
281
282/// 显式背景属性(`<p:bgPr>` 内的填充)。
283#[derive(Clone, Debug, Default)]
284pub struct BackgroundProperty {
285    /// 纯色填充颜色。`Color::None` 表示不写出 `<a:solidFill>`。
286    pub solid_fill: crate::oxml::color::Color,
287}
288
289/// 背景引用(`<p:bgRef idx="...">`)。
290#[derive(Clone, Debug, Default)]
291pub struct BackgroundReference {
292    /// 背景样式索引(如 `1001` = bg1, `1002` = bg2)。
293    pub idx: u32,
294    /// 方案颜色(如 `bg1` / `bg2` / `tx1`)。
295    pub scheme_color: String,
296}
297
298impl SlideBackground {
299    /// 创建一个纯色背景。
300    pub fn solid(color: crate::oxml::color::Color) -> Self {
301        SlideBackground::Property(BackgroundProperty { solid_fill: color })
302    }
303
304    /// 创建一个遵循母版的背景引用(`idx=1001`,`schemeClr=bg1`)。
305    pub fn follow_master() -> Self {
306        SlideBackground::Reference(BackgroundReference {
307            idx: 1001,
308            scheme_color: "bg1".to_string(),
309        })
310    }
311
312    /// 写出 `<p:bg>` XML。
313    pub fn write_xml(&self, w: &mut XmlWriter) {
314        w.open("p:bg");
315        match self {
316            SlideBackground::Property(p) => {
317                w.open("p:bgPr");
318                if !matches!(p.solid_fill, crate::oxml::color::Color::None) {
319                    p.solid_fill.write_solid_fill(w);
320                }
321                // bgPr 必须以 empty 的 bgPr 结尾属性收尾(OOXML 要求 a:effectLst 可选)
322                w.close("p:bgPr");
323            }
324            SlideBackground::Reference(r) => {
325                let idx_s = r.idx.to_string();
326                w.open_with("p:bgRef", &[("idx", idx_s.as_str())]);
327                w.empty_with("a:schemeClr", &[("val", r.scheme_color.as_str())]);
328                w.close("p:bgRef");
329            }
330        }
331        w.close("p:bg");
332    }
333}
334
335#[derive(Clone, Debug)]
336pub enum SlideShape {
337    Sp(Sp),
338    Pic(Pic),
339    CxnSp(Connector),
340    Group(Box<Group>),
341    GraphicFrame(GraphicFrame),
342}
343
344impl Sld {
345    /// 写 XML。
346    pub fn to_xml(&self) -> String {
347        let mut w = XmlWriter::with_decl();
348        let attrs: Vec<(&str, &str)> = vec![
349            ("xmlns:a", NS_DRAWING_MAIN),
350            ("xmlns:p", NS_PRESENTATION_MAIN),
351            ("xmlns:r", crate::oxml::ns::NS_DRAWING_RELS),
352        ];
353        w.open_with("p:sld", &attrs);
354        // cSld
355        // name 是可选属性;空字符串 / 仅空白时**不**写出,避免脏数据进入文件
356        if !self.name.trim().is_empty() {
357            w.open_with("p:cSld", &[("name", self.name.as_str())]);
358        } else {
359            w.open("p:cSld");
360        }
361        // bg:背景(可选,必须在 spTree 之前)
362        if let Some(bg) = &self.background {
363            bg.write_xml(&mut w);
364        }
365        // spTree
366        w.open("p:spTree");
367        // nvGrpSpPr:spTree 必填项;cNvPr/cNvGrpSpPr/nvPr 三个子元素按规范顺序输出。
368        w.open("p:nvGrpSpPr");
369        w.empty_with("p:cNvPr", &[("id", "1"), ("name", "")]);
370        w.empty("p:cNvGrpSpPr");
371        w.empty("p:nvPr");
372        w.close("p:nvGrpSpPr");
373        // grpSpPr:spTree 必填项;这里写空 xfrm 占位(OOXML 允许 grpSpPr 为空)。
374        w.open("p:grpSpPr");
375        w.empty("a:xfrm");
376        w.close("p:grpSpPr");
377        for shape in &self.shapes {
378            match shape {
379                SlideShape::Sp(s) => s.write_xml(&mut w),
380                SlideShape::Pic(p) => p.write_xml(&mut w),
381                SlideShape::CxnSp(c) => c.write_xml(&mut w),
382                SlideShape::Group(g) => g.write_xml(&mut w),
383                SlideShape::GraphicFrame(g) => g.write_xml(&mut w),
384            }
385        }
386        // spTree 末尾的 extLst(CT_GroupShape 允许最后包含一个 extLst)
387        if let Some(ext) = &self.ext_lst {
388            ext.write_xml(&mut w);
389        }
390        w.close("p:spTree");
391        w.close("p:cSld");
392        // clrMapOvr
393        w.empty("p:clrMapOvr");
394        // transition(TODO-020:幻灯片过渡,可选,必须在 clrMapOvr 之后、timing 之前)
395        if let Some(tr) = &self.transition {
396            tr.write_xml(&mut w);
397        }
398        // timing 暂略
399        w.close("p:sld");
400        w.into_string()
401    }
402
403    /// 设置 layout 关系 id(指向 `ppt/slideLayouts/slideLayoutN.xml`)。
404    ///
405    /// 由 `Presentation::from_opc` 在 read 路径里
406    /// 从 `slideN.xml.rels` 解析出 SlideLayout 关系后回填。
407    pub fn set_layout_rid(&mut self, rid: String) {
408        self.layout_rid = rid;
409    }
410}
411
412/// 写一段**备注页** XML(`<p:notes>`)。
413///
414/// 备注页与 slide 结构类似,但内部只有一个 placeholder(`type="body"`)
415/// 承载 [`TextBody`]。OOXML 规范要求 placeholder id 显式标注(常用 `idx="1"`)。
416///
417/// # 与 python-pptx 的对应
418///
419/// python-pptx 中 `slide.notes_slide.notes_text_frame.text = "..."` 最终会
420/// 序列化为本函数产出的格式。
421///
422/// # 元素顺序(OOXML 规范)
423///
424/// ```text
425/// <p:notes>
426///   <p:cSld>
427///     <p:spTree>
428///       <p:nvGrpSpPr/>
429///       <p:grpSpPr/>
430///       <p:sp>           ← placeholder body
431///         <p:nvSpPr>
432///           <p:cNvPr id="2" name="Notes Placeholder"/>
433///           <p:cNvSpPr txBox="1"/>
434///           <p:nvPr><p:ph type="body" idx="1"/></p:nvPr>
435///         </p:nvSpPr>
436///         <p:spPr><a:xfrm/><a:prstGeom prst="rect"/></p:spPr>
437///         <p:txBody>...   ← TextBody 内容
438///       </p:sp>
439///     </p:spTree>
440///   </p:cSld>
441///   <p:clrMapOvr bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2"/>
442/// </p:notes>
443/// ```
444pub fn notes_xml(tb: &TextBody) -> String {
445    let mut w = XmlWriter::with_decl();
446    let attrs: Vec<(&str, &str)> = vec![
447        ("xmlns:a", NS_DRAWING_MAIN),
448        ("xmlns:p", NS_PRESENTATION_MAIN),
449        ("xmlns:r", crate::oxml::ns::NS_DRAWING_RELS),
450    ];
451    w.open_with("p:notes", &attrs);
452    // cSld
453    w.open("p:cSld");
454    // spTree
455    w.open("p:spTree");
456    w.empty_with("p:nvGrpSpPr", &[]);
457    w.empty_with("p:grpSpPr", &[]);
458    // 备注占位符 sp
459    w.open("p:sp");
460    w.open("p:nvSpPr");
461    w.empty_with("p:cNvPr", &[("id", "2"), ("name", "Notes Placeholder")]);
462    w.empty_with("p:cNvSpPr", &[("txBox", "1")]);
463    w.open("p:nvPr");
464    w.empty_with("p:ph", &[("type", "body"), ("idx", "1")]);
465    w.close("p:nvPr");
466    w.close("p:nvSpPr");
467    // spPr:占位符占满整张备注页(EMU:默认 6858000 x 9144000)
468    w.open("p:spPr");
469    w.open("a:xfrm");
470    w.empty_with("a:off", &[("x", "0"), ("y", "0")]);
471    w.empty_with("a:ext", &[("cx", "6858000"), ("cy", "9144000")]);
472    w.close("a:xfrm");
473    w.open_with("a:prstGeom", &[("prst", "rect")]);
474    w.empty("a:avLst");
475    w.close("a:prstGeom");
476    w.close("p:spPr");
477    // txBody
478    tb.write_xml(&mut w, "p:txBody");
479    w.close("p:sp");
480    w.close("p:spTree");
481    w.close("p:cSld");
482    // clrMapOvr
483    w.empty("p:clrMapOvr");
484    w.close("p:notes");
485    w.into_string()
486}