Skip to main content

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}