pptx_rs/oxml/ole.rs
1//! OLE 对象嵌入(`<p:oleObj>`,TODO-043)。
2//!
3//! 本模块定义 OLE 对象的强类型模型。OLE 对象在 OOXML 中通过
4//! `<p:graphicFrame>` + `<a:graphicData uri=".../ole">` + `<p:oleObj>` 引用一个
5//! 独立的 `/ppt/embeddings/oleObjectN.bin` part(CFB 复合文档二进制)。
6//!
7//! # 与 python-pptx 的对应
8//!
9//! - `pptx.shapes.graphfrm.GraphicFrame` ←→ [`crate::oxml::shape::GraphicFrame`](承载位置/尺寸);
10//! - `pptx.parts.oleobject.OleObjectPart` ←→ `/ppt/embeddings/oleObjectN.bin` part;
11//! - python-pptx 0.6.19+ 的 `shapes.add_ole_object()` ←→ [`crate::slide::ShapesMut::add_ole_object`]。
12//!
13//! # 写出语义
14//!
15//! - `OleObject` 序列化时只写出 `<p:oleObj r:id="..."/>` 引用元素 + 可选 `<p:pic>` 图标;
16//! - 真正的 OLE 二进制数据由 [`crate::presentation::Presentation::save`] 在
17//! `to_opc_package` 中遍历每张 slide 的 `ole_entries` 写出独立的 `oleObjectN.bin` part;
18//! - slide 的 `_rels/slideN.xml.rels` 中会添加 `oleObject` 关系指向该 part。
19//!
20//! # 限制
21//!
22//! - 当前仅支持**嵌入**(`<p:embed/>`),不支持链接(`<p:link/>`);
23//! - 图标图片可选(`image_rid` 为空时不写 `<p:pic>`,PowerPoint 会用默认图标);
24//! - 不解析已有 OLE 对象的 `<p:oleObj>` 内容(读路径仅保留 graphicFrame 外壳)。
25
26use crate::oxml::writer::XmlWriter;
27use crate::units::Emu;
28
29/// OLE 对象 URI(`<a:graphicData uri="...">`)。
30///
31/// 固定为 `http://schemas.openxmlformats.org/presentationml/2006/ole`,
32/// 与 Chart/Table 的 URI 同级。
33pub const OLE_GRAPHIC_DATA_URI: &str = "http://schemas.openxmlformats.org/presentationml/2006/ole";
34
35/// OLE 对象模型(`<p:oleObj>`)。
36///
37/// 一个完整的 OLE 嵌入对象,引用独立的 `/ppt/embeddings/oleObjectN.bin` part。
38///
39/// # 字段说明
40///
41/// - `rid`:指向 `oleObjectN.bin` 的关系 id(由 `ShapesMut::add_ole_object` 分配,
42/// `to_opc_package` 在 slideN.xml.rels 中注册)。
43/// - `image_rid`:指向图标图片的关系 id(与 `image_rid` 共用 slide rels 命名空间)。
44/// 为空字符串时表示无图标图片,PowerPoint 会用默认图标显示。
45/// - `prog_id`:OLE 程序标识符(如 `"Excel.Sheet.12"` / `"Word.Document.12"` /
46/// `"Package"`)。PowerPoint 通过 progId 决定双击时调用哪个 OLE 服务器。
47/// - `name`:显示名(在 PowerPoint 中作为对象名)。
48/// - `show_as_icon`:是否以图标形式显示(`true` 时写出 `showAsIcon="1"`)。
49/// - `image_width` / `image_height`:图标显示尺寸(EMU)。
50/// - `pic_id` / `pic_name`:图标图片 Pic 形状的 id 与 name。
51#[derive(Clone, Debug)]
52pub struct OleObject {
53 /// 指向 `oleObjectN.bin` 的关系 id。
54 pub rid: String,
55 /// 指向图标图片的关系 id(空字符串表示无图标)。
56 pub image_rid: String,
57 /// OLE 程序标识符(如 `"Excel.Sheet.12"`)。
58 pub prog_id: String,
59 /// 显示名(如 `"Worksheet"` / `"Document"`)。
60 pub name: String,
61 /// 是否以图标形式显示。
62 pub show_as_icon: bool,
63 /// 图标宽度(EMU)。
64 pub image_width: Emu,
65 /// 图标高度(EMU)。
66 pub image_height: Emu,
67 /// 图标 Pic 形状的 id。
68 pub pic_id: u32,
69 /// 图标 Pic 形状的 name。
70 pub pic_name: String,
71}
72
73impl Default for OleObject {
74 fn default() -> Self {
75 OleObject {
76 rid: String::new(),
77 image_rid: String::new(),
78 prog_id: "Package".to_string(),
79 name: "OLE Object".to_string(),
80 show_as_icon: true,
81 image_width: Emu(914400), // 1 英寸
82 image_height: Emu(914400),
83 pic_id: 0,
84 pic_name: "OLE Icon".to_string(),
85 }
86 }
87}
88
89impl OleObject {
90 /// 构造一个指定 progId 与显示名的 OLE 对象(rid/image_rid 留空,由 presentation 层填充)。
91 ///
92 /// # 参数
93 /// - `prog_id`:OLE 程序标识符(如 `"Excel.Sheet.12"` / `"Package"`)。
94 /// - `name`:显示名(如 `"Worksheet"`)。
95 pub fn new(prog_id: impl Into<String>, name: impl Into<String>) -> Self {
96 OleObject {
97 prog_id: prog_id.into(),
98 name: name.into(),
99 ..Default::default()
100 }
101 }
102
103 /// 写出 `<p:oleObj>` 完整 XML(含 `<p:embed/>` 与可选 `<p:pic>`)。
104 ///
105 /// # 元素结构
106 ///
107 /// ```text
108 /// <p:oleObj spid="..." name="..." r:id="..." imgW="..." imgH="..." progId="..." showAsIcon="1">
109 /// <p:embed/>
110 /// <p:pic> ← 仅当 image_rid 非空时写出
111 /// <p:nvPicPr>...
112 /// <p:blipFill>...
113 /// <p:spPr>...
114 /// </p:pic>
115 /// </p:oleObj>
116 /// ```
117 ///
118 /// # 注意
119 ///
120 /// 本方法**只**写出 `<p:oleObj>` 元素本身;外层的 `<a:graphicData uri="...">`
121 /// 由 [`crate::oxml::shape::GraphicFrame::write_xml`] 负责包裹。
122 pub fn write_xml(&self, w: &mut XmlWriter) {
123 let spid = format!("_x0000_s{}", self.pic_id);
124 let img_w = self.image_width.0.to_string();
125 let img_h = self.image_height.0.to_string();
126 let show_as_icon = if self.show_as_icon { "1" } else { "0" };
127 let rid = if self.rid.is_empty() {
128 "rId1"
129 } else {
130 self.rid.as_str()
131 };
132
133 // <p:oleObj ...>
134 w.open_with(
135 "p:oleObj",
136 &[
137 ("spid", spid.as_str()),
138 ("name", self.name.as_str()),
139 ("r:id", rid),
140 ("imgW", img_w.as_str()),
141 ("imgH", img_h.as_str()),
142 ("progId", self.prog_id.as_str()),
143 ("showAsIcon", show_as_icon),
144 ],
145 );
146 // <p:embed/> ← 嵌入对象(与 <p:link/> 对立)
147 w.empty("p:embed");
148 // <p:pic> ← 图标图片(可选)
149 if !self.image_rid.is_empty() {
150 self.write_pic_xml(w);
151 }
152 w.close("p:oleObj");
153 }
154
155 /// 写出图标图片的 `<p:pic>` 元素。
156 ///
157 /// 包含三个子元素:`<p:nvPicPr>` / `<p:blipFill>` / `<p:spPr>`,
158 /// 与普通 Pic 形状结构一致,但 spPr 用固定的矩形几何。
159 fn write_pic_xml(&self, w: &mut XmlWriter) {
160 let pic_id_s = self.pic_id.to_string();
161 let img_rid = self.image_rid.as_str();
162 let cx_s = self.image_width.0.to_string();
163 let cy_s = self.image_height.0.to_string();
164
165 w.open("p:pic");
166 // <p:nvPicPr>
167 w.open("p:nvPicPr");
168 w.empty_with(
169 "p:cNvPr",
170 &[("id", pic_id_s.as_str()), ("name", self.pic_name.as_str())],
171 );
172 w.empty("p:cNvPicPr");
173 w.empty("p:nvPr");
174 w.close("p:nvPicPr");
175 // <p:blipFill>
176 w.open("p:blipFill");
177 w.empty_with(
178 "a:blip",
179 &[
180 (
181 "xmlns:r",
182 "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
183 ),
184 ("r:embed", img_rid),
185 ],
186 );
187 w.open("a:stretch");
188 w.empty("a:fillRect");
189 w.close("a:stretch");
190 w.close("p:blipFill");
191 // <p:spPr>
192 w.open("p:spPr");
193 w.open("a:xfrm");
194 w.empty_with("a:off", &[("x", "0"), ("y", "0")]);
195 w.empty_with("a:ext", &[("cx", cx_s.as_str()), ("cy", cy_s.as_str())]);
196 w.close("a:xfrm");
197 // prstGeom:rect 几何(与 Geometry::Preset(Rectangle, []) 一致)
198 w.open_with("a:prstGeom", &[("prst", "rect")]);
199 w.empty("a:avLst");
200 w.close("a:prstGeom");
201 w.close("p:spPr");
202 w.close("p:pic");
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 /// `new` 正确构造 OleObject:progId/name 自定义,其余字段为默认值。
211 #[test]
212 fn new_ole_object_basics() {
213 let ole = OleObject::new("Excel.Sheet.12", "Worksheet");
214 assert_eq!(ole.prog_id, "Excel.Sheet.12");
215 assert_eq!(ole.name, "Worksheet");
216 assert_eq!(ole.rid, "");
217 assert_eq!(ole.image_rid, "");
218 assert!(ole.show_as_icon);
219 assert_eq!(ole.image_width, Emu(914400));
220 }
221
222 /// `write_xml` 在无图标图片时正确省略 `<p:pic>`。
223 #[test]
224 fn write_xml_without_icon() {
225 let mut ole = OleObject::new("Package", "OLE Object");
226 ole.rid = "rIdOle1".to_string();
227 let mut w = XmlWriter::new();
228 ole.write_xml(&mut w);
229 let xml = w.into_string();
230 assert!(xml.contains("<p:oleObj"));
231 assert!(xml.contains("r:id=\"rIdOle1\""));
232 assert!(xml.contains("progId=\"Package\""));
233 assert!(xml.contains("<p:embed/>"));
234 // 无图标时不应出现 <p:pic>
235 assert!(!xml.contains("<p:pic>"));
236 }
237
238 /// `write_xml` 在有图标图片时正确写出 `<p:pic>`。
239 #[test]
240 fn write_xml_with_icon() {
241 let mut ole = OleObject::new("Word.Document.12", "Document");
242 ole.rid = "rIdOle1".to_string();
243 ole.image_rid = "rIdImg1".to_string();
244 ole.pic_id = 1026;
245 let mut w = XmlWriter::new();
246 ole.write_xml(&mut w);
247 let xml = w.into_string();
248 assert!(xml.contains("<p:pic>"));
249 assert!(xml.contains("r:embed=\"rIdImg1\""));
250 assert!(xml.contains("spid=\"_x0000_s1026\""));
251 }
252
253 /// `show_as_icon=false` 时写出 `showAsIcon="0"`。
254 #[test]
255 fn write_xml_show_as_icon_false() {
256 let mut ole = OleObject::new("Package", "Object");
257 ole.rid = "rId1".to_string();
258 ole.show_as_icon = false;
259 let mut w = XmlWriter::new();
260 ole.write_xml(&mut w);
261 let xml = w.into_string();
262 assert!(xml.contains("showAsIcon=\"0\""));
263 }
264}