Skip to main content

pptx_rs/oxml/
notesmaster.rs

1//! 备注母版 `<p:notesMaster>` 模型(TODO-045)。
2//!
3//! 备注母版是"主-版-页"三层中的备注页对应母版——所有 notesSlide
4//! 默认继承它的形状与样式。对应 python-pptx 中 `NotesMaster` / `NotesMasterPart`。
5//!
6//! # 与 python-pptx 的对应
7//!
8//! - `pptx.parts.notesmaster.NotesMasterPart` ←→ 本模块 [`NotesMaster`] 的 OPC 宿主;
9//! - `pptx.NotesMaster` ←→ [`crate::notes_masters::NotesMasterRef`] 高阶句柄。
10//!
11//! # 当前实现范围
12//!
13//! 本模块支持**读写双向**——`from_opc` 解析已有 .pptx 的 notesMaster,
14//! `to_opc_package` 循环写出 notesMasterN.xml part + 关系 + notesMasterIdLst 元素。
15//! `clrMap` / `notesStyle` 保留原始 XML 字符串以实现 round-trip 保真。
16//!
17//! # OOXML 结构
18//!
19//! ```text
20//! <p:notesMaster>
21//!   <p:cSld>
22//!     <p:bg>?</p:bg>                     可选背景
23//!     <p:spTree>...</p:spTree>           形状树(含占位符:日期/页码/正文等)
24//!   </p:cSld>
25//!   <p:clrMap .../>                      颜色映射
26//!   <p:notesStyle>...</p:notesStyle>     备注文本样式
27//! </p:notesMaster>
28//! ```
29
30use crate::oxml::shape::Sp;
31use crate::oxml::slide::SlideBackground;
32use crate::oxml::writer::XmlWriter;
33
34/// 默认 `<p:notesStyle>` 内容(极简版,与 SldMaster 的 TX_STYLES 对齐)。
35///
36/// 包含 titleStyle / bodyStyle / otherStyle 各 1 个 lvl1pPr,确保 OOXML 校验通过。
37/// round-trip 场景下优先使用解析到的原始 notesStyle XML。
38const DEFAULT_NOTES_STYLE: &str = r##"<p:notesStyle><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="+mj-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:notesStyle>"##;
39
40/// 默认 `<p:clrMap>` 内容(与 SldMaster 对齐)。
41const DEFAULT_CLR_MAP: &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\"/>";
42
43/// `<p:notesMaster>` 的内存模型。
44///
45/// 承载 `cSld/spTree` 内的形状列表 + 可选背景 + clrMap/notesStyle 原始 XML。
46/// 读写双向支持:`from_opc` 解析时回填,`to_opc_package` 写出时调用 [`NotesMaster::to_xml`]。
47#[derive(Clone, Debug, Default)]
48pub struct NotesMaster {
49    /// 备注母版中的形状列表(`<p:cSld>/<p:spTree>` 内的 `<p:sp>`)。
50    pub shapes: Vec<Sp>,
51    /// 备注母版背景(`<p:bg>`,可选)。`None` 表示使用默认背景。
52    pub background: Option<SlideBackground>,
53    /// 原始 `<p:clrMap>` XML 字符串(round-trip 保真)。
54    ///
55    /// `None` 时 [`NotesMaster::to_xml`] 使用 [`DEFAULT_CLR_MAP`]。
56    /// `Some` 时直接 raw 输出原始 XML,确保读→写不漂移。
57    pub clr_map_xml: Option<String>,
58    /// 原始 `<p:notesStyle>` XML 字符串(round-trip 保真)。
59    ///
60    /// `None` 时 [`NotesMaster::to_xml`] 使用 [`DEFAULT_NOTES_STYLE`]。
61    /// `Some` 时直接 raw 输出原始 XML,确保读→写不漂移。
62    pub notes_style_xml: Option<String>,
63}
64
65impl NotesMaster {
66    /// 创建一个空的备注母版。
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// 形状数量。
72    pub fn len(&self) -> usize {
73        self.shapes.len()
74    }
75
76    /// 是否无形状。
77    pub fn is_empty(&self) -> bool {
78        self.shapes.is_empty()
79    }
80
81    /// 写出 `<p:notesMaster>` 完整 XML(notesMasterN.xml 内容)。
82    ///
83    /// # 元素结构(OOXML 严格顺序)
84    ///
85    /// ```text
86    /// <p:notesMaster xmlns:a=... xmlns:p=... xmlns:r=...>
87    ///   <p:cSld>
88    ///     <p:bg>?</p:bg>           可选背景
89    ///     <p:spTree>...</p:spTree> 形状树
90    ///   </p:cSld>
91    ///   <p:clrMap .../>             颜色映射(优先用 clr_map_xml,否则默认)
92    ///   <p:notesStyle>...</p:notesStyle>  备注文本样式(优先用 notes_style_xml,否则默认)
93    /// </p:notesMaster>
94    /// ```
95    ///
96    /// # 与 python-pptx 的对应
97    ///
98    /// python-pptx 不直接生成 notesMaster XML(仅读取),本库为 round-trip 保真实现写路径。
99    pub fn to_xml(&self) -> String {
100        let mut xml = String::with_capacity(1024);
101        xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
102        xml.push_str(
103            "<p:notesMaster xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\"",
104        );
105        xml.push_str(" xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\"");
106        xml.push_str(
107            " xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">",
108        );
109        // cSld —— 必须按 <p:bg>? <p:spTree> <p:custDataLst>? <p:controls>? <p:extLst>? 顺序
110        xml.push_str("<p:cSld>");
111        // 背景在 spTree 之前
112        if let Some(bg) = &self.background {
113            let mut w = XmlWriter::default();
114            bg.write_xml(&mut w);
115            xml.push_str(&w.into_string());
116        }
117        xml.push_str("<p:spTree>");
118        // nvGrpSpPr + grpSpPr (spTree 必填项)
119        xml.push_str(
120            "<p:nvGrpSpPr><p:cNvPr id=\"1\" name=\"\"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>",
121        );
122        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>");
123        // 用户 shapes
124        let mut w = XmlWriter::default();
125        for s in &self.shapes {
126            s.write_xml(&mut w);
127        }
128        xml.push_str(&w.into_string());
129        xml.push_str("</p:spTree></p:cSld>");
130        // clrMap —— 优先用解析到的原始 XML,否则用默认
131        if let Some(clr_map) = &self.clr_map_xml {
132            xml.push_str(clr_map);
133        } else {
134            xml.push_str(DEFAULT_CLR_MAP);
135        }
136        // notesStyle —— 优先用解析到的原始 XML,否则用默认
137        if let Some(notes_style) = &self.notes_style_xml {
138            xml.push_str(notes_style);
139        } else {
140            xml.push_str(DEFAULT_NOTES_STYLE);
141        }
142        xml.push_str("</p:notesMaster>");
143        xml
144    }
145}