Skip to main content

pptx_rs/oxml/
slidemaster.rs

1//! 幻灯片母版 `<p:sldMaster>` 简化模型。
2//!
3//! 当前只生成"能跑"级别的母版;样式继承仍走 `a:lstStyle` 的最小默认。
4//! 注意:PowerPoint 严格要求 master 拥有 `<p:cSld>/<p:spTree>`、`<p:clrMap>`、`<p:sldLayoutIdLst>` 与
5//! `<p:txStyles>`(titleStyle / bodyStyle / otherStyle)。
6//!
7//! # 与 python-pptx 的对应
8//!
9//! - `pptx.oxml.slidemaster.SlideMaster` ←→ [`SldMaster`];
10//! - `SlideMasterPart` 由 `pptx.parts.slidemaster` 维护,本库不直接对应。
11//!
12//! # 默认内容
13//!
14//! `TX_STYLES` 是与 python-pptx 默认输出对齐的极简版
15//! (`titleStyle` / `bodyStyle` / `otherStyle` 各 1 个 `lvl1pPr`),
16//! 已通过 PowerPoint 打开测试。
17
18use crate::oxml::shape::Sp;
19use crate::oxml::slide::SlideBackground;
20
21/// `<p:txStyles>` 三段标题/正文/其它文本样式的极简版。
22/// python-pptx 输出的 bodyStyle 包含 9 个 lvlXpPr,这里给每个 1 个 lvl1pPr 让 OOXML 校验通过。
23const TX_STYLES: &str = r##"<p:txStyles><p:titleStyle><a:lvl1pPr algn="ctr" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:spcBef><a:spcPct val="0"/></a:spcBef><a:buNone/><a:defRPr sz="4400" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mj-lt"/><a:ea typeface="+mj-ea"/><a:cs typeface="+mj-cs"/></a:defRPr></a:lvl1pPr></p:titleStyle><p:bodyStyle><a:lvl1pPr marL="342900" indent="-342900" algn="l" defTabSz="457200" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:spcBef><a:spcPct val="20000"/></a:spcBef><a:buFont typeface="Arial"/><a:buChar char="•"/><a:defRPr sz="3200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mj-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl1pPr></p:bodyStyle><p:otherStyle><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="+mj-ea"/><a:cs typeface="+mj-cs"/></a:defRPr></a:lvl1pPr></p:otherStyle></p:txStyles>"##;
24
25#[derive(Clone, Debug, Default)]
26pub struct SldMaster {
27    /// 母版中的形状列表。
28    pub shapes: Vec<Sp>,
29    /// 关联的版式关系 ID 列表(指向 `slideLayoutN.xml`)。
30    pub layout_rids: Vec<String>,
31    /// 母版背景(`<p:bg>`,可选)。`None` 表示使用默认背景。
32    ///
33    /// 由 Slide、SlideLayout 继承。详见 OOXML `CT_SlideMaster` 的 `<p:cSld>/<p:bg>`。
34    pub background: Option<SlideBackground>,
35}
36
37impl SldMaster {
38    /// 写出最小但合规的 `<p:sldMaster>` XML。
39    pub fn to_xml(&self) -> String {
40        // 头部(含三个 namespace)+ 完整结构
41        let mut xml = String::with_capacity(1024);
42        xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
43        xml.push_str(
44            "<p:sldMaster xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\"",
45        );
46        xml.push_str(" xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\"");
47        xml.push_str(
48            " xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">",
49        );
50        // cSld —— 必须按 <p:bg>? <p:spTree> <p:custDataLst>? <p:controls>? <p:extLst>? 顺序
51        xml.push_str("<p:cSld>");
52        // 背景在 spTree 之前(OOXML 顺序:bg? → spTree → custDataLst? → controls? → extLst?)
53        if let Some(bg) = &self.background {
54            let mut w = crate::oxml::writer::XmlWriter::default();
55            bg.write_xml(&mut w);
56            xml.push_str(&w.into_string());
57        }
58        xml.push_str("<p:spTree>");
59        // nvGrpSpPr + grpSpPr (spTree 必填项)
60        xml.push_str(
61            "<p:nvGrpSpPr><p:cNvPr id=\"1\" name=\"\"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>",
62        );
63        xml.push_str("<p:grpSpPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"0\" cy=\"0\"/><a:chOff x=\"0\" y=\"0\"/><a:chExt cx=\"0\" cy=\"0\"/></a:xfrm></p:grpSpPr>");
64        // 用户 shapes
65        let mut w = crate::oxml::writer::XmlWriter::default();
66        for s in &self.shapes {
67            s.write_xml(&mut w);
68        }
69        xml.push_str(&w.into_string());
70        xml.push_str("</p:spTree></p:cSld>");
71        // clrMap
72        xml.push_str("<p:clrMap bg1=\"lt1\" tx1=\"dk1\" bg2=\"lt2\" tx2=\"dk2\" accent1=\"accent1\" accent2=\"accent2\" accent3=\"accent3\" accent4=\"accent4\" accent5=\"accent5\" accent6=\"accent6\" hlink=\"hlink\" folHlink=\"folHlink\"/>");
73        // sldLayoutIdLst - 必须存在且至少有一个 sldLayoutId
74        // 优先使用 self.layout_rids(round-trip 场景由 parse_sld_master 填充),
75        // 为空时兜底写 rId1(新建场景兼容旧行为)
76        xml.push_str("<p:sldLayoutIdLst>");
77        if self.layout_rids.is_empty() {
78            xml.push_str("<p:sldLayoutId id=\"2147483649\" r:id=\"rId1\"/>");
79        } else {
80            for (i, rid) in self.layout_rids.iter().enumerate() {
81                // OOXML 推荐 sldLayoutId 的 id 从 2147483649 起递增
82                let layout_id = 2147483649u32 + i as u32;
83                xml.push_str(&format!(
84                    "<p:sldLayoutId id=\"{}\" r:id=\"{}\"/>",
85                    layout_id, rid
86                ));
87            }
88        }
89        xml.push_str("</p:sldLayoutIdLst>");
90        // txStyles (必须有,否则 PowerPoint 解析报错)
91        xml.push_str(TX_STYLES);
92        xml.push_str("</p:sldMaster>");
93        xml
94    }
95}