Skip to main content

pptx_rs/oxml/
presentation.rs

1//! 演示文稿顶级 XML:`<p:presentation>` / `<p:sldIdLst>` / `<p:sldSz>`。
2//!
3//! 本文件定义"根 part"——`/ppt/presentation.xml` 对应的 Rust 模型。
4//!
5//! # 元素结构(OOXML 规范严格顺序)
6//!
7//! ```text
8//! <p:presentation>
9//!   <p:sldMasterIdLst>...</p:sldMasterIdLst>   必须存在(可为空)
10//!   <p:notesSz .../>                             备注尺寸
11//!   <p:sldIdLst>                                 所有 slide 的 ID 列表
12//!     <p:sldId id="256" r:id="rId5"/>
13//!     ...
14//!   </p:sldIdLst>
15//!   <p:sldSz cx="..." cy="..."/>                 幻灯片尺寸(EMU)
16//!   ...其它可选元素...
17//!   <p:defaultTextStyle>...</p:defaultTextStyle> 必须存在
18//! </p:presentation>
19//! ```
20//!
21//! # 与 python-pptx 的对应
22//!
23//! - `pptx.oxml.presentation.Presentation` ←→ [`PresentationRoot`];
24//! - `pptx.oxml.presentation.SldId` ←→ [`SlideIdEntry`]。
25//!
26//! # 序列化要点
27//!
28//! - **`sldMasterIdLst` 必须存在**——PowerPoint 要求至少一个 `sldMasterId`,即使实际不挂母版。
29//! - **`defaultTextStyle` 必须存在**——python-pptx 给出 9 段 `lvlXpPr` 的极简版,
30//!   本库直接复用(`DEFAULT_TEXT_STYLE`)。
31
32use std::str::FromStr;
33
34use crate::oxml::ns::{NS_DRAWING_MAIN, NS_PRESENTATION_MAIN};
35use crate::oxml::writer::XmlWriter;
36use crate::units::Emu;
37
38/// 演示文稿的根 `<p:presentation>` 元素。
39#[derive(Clone, Debug)]
40pub struct PresentationRoot {
41    /// 幻灯片宽度(EMU)。`None` 表示不写出 `<p:sldSz>`。
42    pub slide_width: Option<Emu>,
43    /// 幻灯片高度(EMU)。`None` 表示不写出 `<p:sldSz>`。
44    pub slide_height: Option<Emu>,
45    /// 所有 slide 的 ID 列表(`<p:sldIdLst>`)。
46    pub slide_ids: Vec<SlideIdEntry>,
47    /// 母版 ID 列表(`<p:sldMasterIdLst>`)。
48    ///
49    /// TODO-001:建模 `sldMasterIdLst`,支持多母版。
50    /// 空列表时 `to_xml` 使用默认的单个母版(id=2147483648, rIdP1)。
51    pub sld_master_ids: Vec<SldMasterIdEntry>,
52    /// 默认文本样式(`<p:defaultTextStyle>`),XML 字符串。`None` 使用内置默认。
53    pub default_text_style: Option<String>,
54    /// 备注页尺寸(宽, 高,EMU)。`None` 使用默认 6858000 × 9144000。
55    pub notes_size: Option<(Emu, Emu)>,
56    /// 章节分组(`<p14:sectionLst>` 扩展元素,TODO-039)。
57    ///
58    /// 空列表时不输出任何 section 相关 XML;
59    /// 非空时在 `<p:defaultTextStyle>` 之后追加 `<p:extLst><p:ext ...><p14:sectionLst>...`。
60    pub sections: crate::oxml::section::SectionList,
61}
62
63impl Default for PresentationRoot {
64    fn default() -> Self {
65        // PowerPoint 严格要求包含 defaultTextStyle,否则打开时弹错。
66        // 这里使用 python-pptx 默认的 9 段 lvlXpPr 极简版。
67        Self {
68            slide_width: None,
69            slide_height: None,
70            slide_ids: Vec::new(),
71            sld_master_ids: Vec::new(),
72            default_text_style: Some(DEFAULT_TEXT_STYLE.to_string()),
73            notes_size: None,
74            sections: crate::oxml::section::SectionList::default(),
75        }
76    }
77}
78
79/// 单一 slide 引用(`p:sldId` 元素)。
80#[derive(Clone, Debug)]
81pub struct SlideIdEntry {
82    /// slide 在 sldIdLst 中的序号(`id` 属性)。
83    pub id: u32,
84    /// 关系 id(`r:id` 属性),指向 `ppt/slides/slideN.xml`。
85    pub rid: String,
86}
87
88/// 单一母版引用(`<p:sldMasterId>` 元素)。
89///
90/// TODO-001:建模 `sldMasterIdLst` 中的每个条目。
91#[derive(Clone, Debug)]
92pub struct SldMasterIdEntry {
93    /// 母版 ID(`id` 属性,通常 >= 2147483648)。
94    pub id: u32,
95    /// 关系 id(`r:id` 属性),指向 `ppt/slideMasters/slideMasterN.xml`。
96    pub rid: String,
97}
98
99impl PresentationRoot {
100    /// 写 XML。
101    pub fn to_xml(&self) -> String {
102        let mut w = XmlWriter::with_decl();
103        let root_attrs: Vec<(&str, &str)> = vec![
104            ("xmlns:a", NS_DRAWING_MAIN),
105            ("xmlns:p", NS_PRESENTATION_MAIN),
106            ("xmlns:r", crate::oxml::ns::NS_DRAWING_RELS),
107            (
108                "xmlns:p14",
109                "http://schemas.microsoft.com/office/powerpoint/2010/main",
110            ),
111        ];
112        w.open_with("p:presentation", &root_attrs);
113
114        // 1) sldMasterIdLst(必须存在,列出所有母版)
115        w.open("p:sldMasterIdLst");
116        if self.sld_master_ids.is_empty() {
117            // 默认:单个母版(兼容旧行为)
118            w.empty_with("p:sldMasterId", &[("id", "2147483648"), ("r:id", "rIdP1")]);
119        } else {
120            // TODO-001:使用解析出的母版 ID 列表
121            for m in &self.sld_master_ids {
122                let id_s = m.id.to_string();
123                w.empty_with("p:sldMasterId", &[("id", &id_s), ("r:id", m.rid.as_str())]);
124            }
125        }
126        w.close("p:sldMasterIdLst");
127
128        // 2) sldIdLst(所有 slide)
129        w.open("p:sldIdLst");
130        for s in &self.slide_ids {
131            // let 绑定延长生命周期
132            let id_s = s.id.to_string();
133            w.empty_with("p:sldId", &[("id", &id_s), ("r:id", s.rid.as_str())]);
134        }
135        w.close("p:sldIdLst");
136
137        // 3) sldSz
138        if let (Some(w_), Some(h)) = (self.slide_width, self.slide_height) {
139            let cx_s = w_.value().to_string();
140            let cy_s = h.value().to_string();
141            w.empty_with("p:sldSz", &[("cx", &cx_s), ("cy", &cy_s)]);
142        }
143        // 4) notesSz(注释页尺寸:默认 6858000 x 9144000 EMU,约 7.5" x 10")
144        let (nw, nh) = self.notes_size.unwrap_or((Emu(6_858_000), Emu(9_144_000)));
145        let ncx = nw.value().to_string();
146        let ncy = nh.value().to_string();
147        w.empty_with("p:notesSz", &[("cx", &ncx), ("cy", &ncy)]);
148        // 5) defaultTextStyle
149        if let Some(s) = &self.default_text_style {
150            w.raw(s);
151        }
152        // 6) sectionLst(TODO-039,PowerPoint 2010 扩展)
153        // 必须放在 `<p:extLst>` 内的 `<p:ext>` 中,且 uri 固定。
154        // 空列表时不输出任何内容。
155        let section_xml = self.sections.write_xml();
156        if !section_xml.is_empty() {
157            w.raw(&section_xml);
158        }
159
160        w.close("p:presentation");
161        w.into_string()
162    }
163}
164
165impl FromStr for PresentationRoot {
166    type Err = crate::Error;
167    fn from_str(_s: &str) -> Result<Self, Self::Err> {
168        // 完整解析器仍在路线图,读取走专门函数
169        Ok(PresentationRoot::default())
170    }
171}
172
173/// 默认 `<p:defaultTextStyle>` 内容(与 python-pptx 默认输出对齐)。
174/// 9 段 lvlXpPr,每段 18x 字号 / 主题色 / latin/+mn-lt 等。
175const DEFAULT_TEXT_STYLE: &str = r##"<p:defaultTextStyle><a:defPPr><a:defRPr lang="en-US"/></a:defPPr><a:lvl1pPr marL="0" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl1pPr><a:lvl2pPr marL="457200" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl2pPr><a:lvl3pPr marL="914400" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl3pPr><a:lvl4pPr marL="1371600" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl4pPr><a:lvl5pPr marL="1828800" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl5pPr><a:lvl6pPr marL="2286000" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl6pPr><a:lvl7pPr marL="2743200" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl7pPr><a:lvl8pPr marL="3200400" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl8pPr><a:lvl9pPr marL="3657600" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1800" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl9pPr></p:defaultTextStyle>"##;