Skip to main content

pptx_rs/opc/
package.rs

1//! `OpcPackage`:包加载/保存。
2//!
3//! 本文件是 OPC 容器层的入口。它把"加载/保存一个 `.pptx`"和"注册 part +
4//! 维护 Content-Types + 维护关系链"三个动作统一到 [`OpcPackage`] 一个类型上。
5//!
6//! # 加载流程
7//!
8//! 1. 打开文件 → `zip::ZipArchive`;
9//! 2. 读取 `[Content_Types].xml` → `ContentTypes` 模型;
10//! 3. 遍历 zip 全部条目,把每个 part 装入 `parts: BTreeMap<PartName, Part>`;
11//! 4. 关系文件 `.rels` 同样作为 part 装入(content-type 固定为
12//!    `application/vnd...relationships+xml`)。
13//!
14//! # 保存流程
15//!
16//! 1. 创建 `zip::ZipWriter`;
17//! 2. 写 `[Content_Types].xml`(序列化 [`ContentTypes`]);
18//! 3. 按 partname 顺序遍历 `parts`,逐个写入 zip;
19//! 4. 关闭 zip。
20//!
21//! # 设计取舍
22//!
23//! - **不维护反向关系索引**:本结构只持有"由 partname 找 part"的正向映射;
24//!   反向"由 partname 找关系链"由调用方在 `Relationships` 上自行迭代。
25//! - **`BTreeMap` 而非 `HashMap`**:让 `iter_parts()` 总是按 partname 字典序
26//!   输出,便于 byte-diff 测试与稳定写入顺序。
27//! - **不解析 `.rels`**:关系文件以原始 blob 形式保留,调用方在需要时
28//!   调用 `Relationships::from_xml` 显式解析。
29
30use std::collections::BTreeMap;
31use std::fs::File;
32use std::io::{Read, Write};
33use std::path::Path;
34
35use super::content_types::{ContentTypes, DefaultExt};
36use super::part::{Part, PartName};
37use super::rels::Relationship;
38
39/// 标准 Content-Type 常量集合。
40///
41/// 这些常量与 ECMA-376 / OOXML 规范一一对应,使用 `pub const` 暴露
42/// 便于在序列化时直接引用而无需硬编码字符串。
43pub mod ct {
44    /// 根关系 Content-Type:`application/vnd.openxmlformats-package.relationships+xml`。
45    pub const RELATIONSHIPS: &str = "application/vnd.openxmlformats-package.relationships+xml";
46    /// 主演示文稿:`.../presentationml.presentation.main+xml`。
47    pub const PRESENTATION: &str =
48        "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml";
49    /// 幻灯片:`.../presentationml.slide+xml`。
50    pub const SLIDE: &str =
51        "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
52    /// 幻灯片布局:`.../presentationml.slideLayout+xml`。
53    pub const SLIDE_LAYOUT: &str =
54        "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml";
55    /// 幻灯片母版:`.../presentationml.slideMaster+xml`。
56    pub const SLIDE_MASTER: &str =
57        "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml";
58    /// 主题:`.../theme+xml`。
59    pub const THEME: &str = "application/vnd.openxmlformats-officedocument.theme+xml";
60    /// 核心属性(`cp:coreProperties`)。
61    pub const CORE_PROPS: &str = "application/vnd.openxmlformats-package.core-properties+xml";
62    /// 应用属性(`extended-properties`)。
63    pub const APP_PROPS: &str =
64        "application/vnd.openxmlformats-officedocument.extended-properties+xml";
65    /// 自定义属性(`custom-properties`,`/docProps/custom.xml`)。
66    ///
67    /// 对应 OOXML 中 `Properties` 根元素,命名空间为
68    /// `http://schemas.openxmlformats.org/officeDocument/2006/custom-properties`。
69    pub const CUSTOM_PROPS: &str =
70        "application/vnd.openxmlformats-officedocument.custom-properties+xml";
71    /// 备注页:`.../presentationml.notesSlide+xml`。
72    ///
73    /// python-pptx 中由 `Slide.notes_slide` 隐式管理。
74    pub const NOTES_SLIDE: &str =
75        "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml";
76    /// 备注母版:`.../presentationml.notesMaster+xml`(`/ppt/notesMasters/notesMasterN.xml`,TODO-045)。
77    ///
78    /// 对应 OOXML 中 `<p:notesMaster>` 根元素,被 `presentation.xml`
79    /// 的 `<p:notesMasterIdLst>` 引用。
80    pub const NOTES_MASTER: &str =
81        "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml";
82    /// 幻灯片评论:`.../presentationml.comments+xml`(`/ppt/comments/commentN.xml`)。
83    ///
84    /// 对应 OOXML 中 `<p:cmLst>` 根元素,命名空间为
85    /// `http://schemas.openxmlformats.org/presentationml/2006/main`。
86    pub const COMMENTS: &str =
87        "application/vnd.openxmlformats-officedocument.presentationml.comments+xml";
88    /// 评论作者列表:`.../presentationml.commentAuthors+xml`(`/ppt/commentAuthors.xml`)。
89    ///
90    /// 全局共享的作者清单,对应 `<p:cmAuthorLst>` 根元素。
91    pub const COMMENT_AUTHORS: &str =
92        "application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml";
93    /// 图表:`.../drawingml.chart+xml`(`/ppt/charts/chartN.xml`)。
94    ///
95    /// 对应 OOXML 中 `<c:chartSpace>` 根元素,命名空间为
96    /// `http://schemas.openxmlformats.org/drawingml/2006/chart`。
97    pub const CHART: &str = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml";
98    /// OLE 对象:`.../oleobject`(`/ppt/embeddings/oleObjectN.bin`,TODO-043)。
99    ///
100    /// 对应二进制 OLE 复合文档(CFB),被 slide 的 `<p:oleObj r:id="..."/>` 引用。
101    /// Content-Type 固定为 `application/vnd.openxmlformats-officedocument.oleobject`,
102    /// 与文件扩展名无关(PowerPoint 通过 progId 区分具体 OLE 服务器)。
103    pub const OLE_OBJECT: &str = "application/vnd.openxmlformats-officedocument.oleobject";
104    /// 嵌入式 Excel 工作簿:`.../spreadsheetml.sheet`(`/ppt/embeddings/Microsoft_Excel_WorksheetN.xlsx`,TODO-004 Excel 嵌入)。
105    ///
106    /// 对应 ECMA-376 SpreadsheetML 包格式,被 chart 的
107    /// `<c:externalData r:id="..."/>` 引用。PowerPoint 打开图表时
108    /// 会从该 xlsx part 读取数据源("编辑数据" 会启动 Excel)。
109    pub const SPREADSHEET_XLSX: &str =
110        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
111    /// 视频:`video/mp4`(`/ppt/media/mediaN.mp4`,TODO-033)。
112    ///
113    /// MP4 是 PowerPoint 最通用的视频格式。其它视频格式(wmv/avi)使用各自 MIME。
114    pub const VIDEO_MP4: &str = "video/mp4";
115    /// 音频:`audio/mp3`(`/ppt/media/mediaN.mp3`,TODO-033)。
116    ///
117    /// MP3 是 PowerPoint 最通用的音频格式。其它音频格式(wav/aac)使用各自 MIME。
118    pub const AUDIO_MP3: &str = "audio/mp3";
119    /// SmartArt 数据:`.../drawingml.diagramData+xml`(`/ppt/diagrams/dataN.xml`,TODO-037)。
120    ///
121    /// 对应 OOXML 中 `<dgm:dataModel>` 根元素,命名空间为
122    /// `http://schemas.openxmlformats.org/drawingml/2006/diagram`。
123    /// 被 slide 的 `<p:graphicFrame>` 内 `<dgm:relIds r:dm="..."/>` 引用。
124    pub const DIAGRAM_DATA: &str =
125        "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml";
126    /// SmartArt 布局:`.../drawingml.diagramLayout+xml`(`/ppt/diagrams/layoutN.xml`,TODO-037)。
127    ///
128    /// 对应 OOXML 中 `<dgm:layoutDef>` 根元素,定义节点排列算法与拓扑。
129    pub const DIAGRAM_LAYOUT: &str =
130        "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml";
131    /// SmartArt 快速样式:`.../drawingml.diagramQuickStyle+xml`(`/ppt/diagrams/quickStylesN.xml`,TODO-037)。
132    ///
133    /// 对应 OOXML 中 `<dgm:styleData>` 根元素,定义节点/连接线视觉样式。
134    pub const DIAGRAM_QUICK_STYLE: &str =
135        "application/vnd.openxmlformats-officedocument.drawingml.diagramQuickStyle+xml";
136    /// SmartArt 颜色:`.../drawingml.diagramColors+xml`(`/ppt/diagrams/colorsN.xml`,TODO-037)。
137    ///
138    /// 对应 OOXML 中 `<dgm:colorsDef>` 根元素,定义颜色变体映射。
139    pub const DIAGRAM_COLORS: &str =
140        "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml";
141}
142
143/// 一个 OPC 包。
144///
145/// 内部包含:
146/// - `parts`:所有 part 的字典(key 为 partname 字符串);
147/// - `content_types`:根 `[Content_Types].xml` 的强类型模型。
148#[derive(Debug, Clone, Default)]
149pub struct OpcPackage {
150    /// 所有 part(partname → Part),按 partname 字典序排列。
151    pub parts: BTreeMap<String, Part>,
152    /// ContentTypes 模型。
153    pub content_types: ContentTypes,
154}
155
156impl OpcPackage {
157    /// 构造空包(含默认 ContentTypes)。
158    pub fn new() -> Self {
159        OpcPackage {
160            parts: BTreeMap::new(),
161            content_types: ContentTypes::new_default(),
162        }
163    }
164
165    /// 放入一个 part(覆盖同名)。
166    ///
167    /// 在插入前会调用 [`Part::contribute_to`],由 part 自己决定如何
168    /// 更新 Content-Types(添加 override 等)。
169    pub fn put_part(&mut self, p: Part) {
170        p.contribute_to(&mut self.content_types);
171        self.parts.insert(p.partname.as_str().to_string(), p);
172    }
173
174    /// 取一个 part 的不可变引用。
175    pub fn get_part(&self, partname: &str) -> Option<&Part> {
176        self.parts.get(partname)
177    }
178
179    /// 取一个 part 的可变引用。
180    pub fn get_part_mut(&mut self, partname: &str) -> Option<&mut Part> {
181        self.parts.get_mut(partname)
182    }
183
184    /// 列出所有 part。
185    pub fn iter_parts(&self) -> impl Iterator<Item = &Part> {
186        self.parts.values()
187    }
188
189    /// part 数量。
190    pub fn part_count(&self) -> usize {
191        self.parts.len()
192    }
193
194    /// 加载 `.pptx`(zip 文件)。
195    ///
196    /// # 流程
197    /// 1. 读取 `[Content_Types].xml` 并解析;
198    /// 2. 遍历 zip 全部条目,把非目录、非 `[Content_Types].xml` 的条目
199    ///    装入 `parts`。
200    ///
201    /// # 错误
202    /// - `Error::Io`:文件读取失败;
203    /// - `Error::Zip`:zip 解压失败;
204    /// - `Error::Xml`:`[Content_Types].xml` 解析失败。
205    pub fn load(path: impl AsRef<Path>) -> crate::Result<Self> {
206        let file = File::open(path.as_ref())?;
207        let mut zip = zip::ZipArchive::new(file)?;
208        let mut pkg = OpcPackage::new();
209
210        // 1) 先读 [Content_Types].xml
211        let mut ct_xml = String::new();
212        zip.by_name("[Content_Types].xml")?
213            .read_to_string(&mut ct_xml)?;
214        pkg.content_types = parse_content_types_public(&ct_xml)?;
215
216        // 2) 读所有 part
217        for i in 0..zip.len() {
218            let mut entry = zip.by_index(i)?;
219            let name = entry.name().to_string();
220            if name == "[Content_Types].xml" {
221                continue;
222            }
223            if entry.is_dir() {
224                continue;
225            }
226
227            // 关系文件 (单独维护) 仍以 part 形式加入,但 content_type 固定
228            let mut blob = Vec::with_capacity(entry.size() as usize);
229            entry.read_to_end(&mut blob)?;
230            let ct = if name.ends_with(".rels") {
231                ct::RELATIONSHIPS.to_string()
232            } else {
233                derive_content_type(&pkg.content_types, &format!("/{}", name))
234            };
235            let partname = format!("/{}", name);
236            let part = Part::new(PartName::from_unchecked(partname), ct, blob);
237            pkg.parts.insert(part.partname.as_str().to_string(), part);
238        }
239        Ok(pkg)
240    }
241
242    /// 保存为 `.pptx`。
243    ///
244    /// 写入顺序:先 `[Content_Types].xml`,再按字典序遍历所有 part。
245    /// 所有条目使用 `deflate` 压缩 + `0o644` Unix 权限。
246    pub fn save(&self, path: impl AsRef<Path>) -> crate::Result<()> {
247        let file = File::create(path.as_ref())?;
248        let mut zip = zip::ZipWriter::new(file);
249        let opts: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default()
250            .compression_method(zip::CompressionMethod::Deflated)
251            .unix_permissions(0o644);
252
253        // 1) [Content_Types].xml
254        zip.start_file("[Content_Types].xml", opts)?;
255        zip.write_all(self.content_types.to_xml().as_bytes())?;
256
257        // 2) 其余 part
258        for part in self.parts.values() {
259            let p = part.partname.to_zip_path();
260            zip.start_file(p, opts)?;
261            zip.write_all(&part.blob)?;
262        }
263        zip.finish()?;
264        Ok(())
265    }
266
267    /// 写回内存 zip(用于测试)。
268    pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
269        use std::io::Cursor;
270        let mut buf: Vec<u8> = Vec::new();
271        {
272            let cursor = Cursor::new(&mut buf);
273            let mut zip = zip::ZipWriter::new(cursor);
274            let opts: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default()
275                .compression_method(zip::CompressionMethod::Deflated);
276            zip.start_file("[Content_Types].xml", opts)?;
277            zip.write_all(self.content_types.to_xml().as_bytes())?;
278            for part in self.parts.values() {
279                zip.start_file(part.partname.to_zip_path(), opts)?;
280                zip.write_all(&part.blob)?;
281            }
282            zip.finish()?;
283        }
284        Ok(buf)
285    }
286}
287
288/// 在 ContentTypes 中根据 partname 查找 Content-Type(override 优先)。
289///
290/// 顺序:先查 `overrides`(按 partname 完全匹配),再查 `defaults`(按扩展名)。
291/// 全部未命中则回退到 `application/octet-stream`。
292///
293/// # 用途
294/// - 在 [`OpcPackage::load`] / `Presentation::load_bytes` 之后,
295///   对新加入 `parts` 的非 `.rels` 元素推断 content_type;
296/// - 在 `to_opc_package` 中也可以用,但通常**不需要**——写路径下
297///   `Part::contribute_to` 会主动调用 [`Part`] 的 `put_part` 自动写 override。
298pub fn derive_content_type(ct: &ContentTypes, partname: &str) -> String {
299    for o in &ct.overrides {
300        if o.partname == partname {
301            return o.content_type.clone();
302        }
303    }
304    // 用扩展名查 defaults
305    let ext = match partname.rfind('.') {
306        Some(i) => &partname[i + 1..],
307        None => "",
308    };
309    for d in &ct.defaults {
310        if d.extension == ext {
311            return d.content_type.clone();
312        }
313    }
314    "application/octet-stream".to_string()
315}
316
317/// 极简解析 `[Content_Types].xml`,主要提取 override。
318///
319/// 完整 schema 还支持 `<Default Extension="..." ContentType="..."/>` 与
320/// `<Override PartName="..." ContentType="..."/>` 两种元素,本函数
321/// 同时识别两者。
322pub fn parse_content_types_public(xml: &str) -> crate::Result<ContentTypes> {
323    use quick_xml::events::Event;
324    use quick_xml::reader::Reader;
325    let mut ct = ContentTypes::new_default();
326    let mut rd = Reader::from_str(xml);
327    rd.config_mut().trim_text(true);
328    let mut buf = Vec::new();
329    loop {
330        match rd.read_event_into(&mut buf) {
331            Ok(Event::Empty(e)) => {
332                let name = e.name();
333                match name.as_ref() {
334                    b"Default" => {
335                        let mut ext = None;
336                        let mut ctype = None;
337                        for a in e.attributes().flatten() {
338                            match a.key.as_ref() {
339                                b"Extension" => {
340                                    ext = a
341                                        .normalized_value(quick_xml::XmlVersion::Implicit1_0)
342                                        .ok()
343                                        .map(|v| v.to_string())
344                                }
345                                b"ContentType" => {
346                                    ctype = a
347                                        .normalized_value(quick_xml::XmlVersion::Implicit1_0)
348                                        .ok()
349                                        .map(|v| v.to_string())
350                                }
351                                _ => {}
352                            }
353                        }
354                        if let (Some(ext), Some(ctype)) = (ext, ctype) {
355                            ct.defaults.push(DefaultExt::new(ext, ctype));
356                        }
357                    }
358                    b"Override" => {
359                        let mut partname = None;
360                        let mut ctype = None;
361                        for a in e.attributes().flatten() {
362                            match a.key.as_ref() {
363                                b"PartName" => {
364                                    partname = a
365                                        .normalized_value(quick_xml::XmlVersion::Implicit1_0)
366                                        .ok()
367                                        .map(|v| v.to_string())
368                                }
369                                b"ContentType" => {
370                                    ctype = a
371                                        .normalized_value(quick_xml::XmlVersion::Implicit1_0)
372                                        .ok()
373                                        .map(|v| v.to_string())
374                                }
375                                _ => {}
376                            }
377                        }
378                        if let (Some(p), Some(c)) = (partname, ctype) {
379                            ct.add_override(&p, &c);
380                        }
381                    }
382                    _ => {}
383                }
384            }
385            Ok(Event::Eof) => break,
386            Err(e) => return Err(crate::Error::Xml(format!("content-types parse: {e}"))),
387            _ => {}
388        }
389        buf.clear();
390    }
391    Ok(ct)
392}
393
394/// 工具:把一个 PartName 映射成 zip 内的"关系文件"路径。
395///
396/// 严格遵循 OPC 约定:`/ppt/slides/slide1.xml` 的关系文件应在
397/// `/ppt/slides/_rels/slide1.xml.rels`。
398///
399/// # 示例
400/// - `/ppt/slides/slide1.xml` → `/ppt/slides/_rels/slide1.xml.rels`
401/// - `/word/document.xml` → `/word/_rels/document.xml.rels`
402/// - `/foo` → `/_rels/foo.rels`
403pub fn rels_partname_for(partname: &str) -> String {
404    let p = partname.trim_start_matches('/');
405    let last_slash = p.rfind('/');
406    match last_slash {
407        None => format!("/_rels/{}.rels", p),
408        // 父目录 _rels 文件名.rels
409        Some(i) => format!("/{}/_rels/{}.rels", &p[..i], &p[i + 1..]),
410    }
411}
412
413/// 工具:建立"父 part → (id, reltype, target)"链;返回每个 target 的下一个 rId。
414///
415/// 通过扫描已有关系 ID(如 `rId1` / `rId2` ...)找到最大编号,分配 `max+1`。
416/// prefix 必须以字符串形式给出,常用 `rId`。
417pub fn next_rid(existing: &[Relationship], prefix: &str) -> String {
418    let mut max = 0u32;
419    for r in existing {
420        if let Some(n) = r.id.strip_prefix(prefix) {
421            if let Ok(v) = n.parse::<u32>() {
422                if v > max {
423                    max = v;
424                }
425            }
426        }
427    }
428    format!("{}{}", prefix, max + 1)
429}