Skip to main content

pptx_rs/opc/
content_types.rs

1//! `[Content_Types].xml` 模型。
2//!
3//! 该文件位于 zip 根目录,是 OPC 包的"媒体类型注册表"。
4//! 它由两类元素组成:
5//!
6//! - `<Default Extension="..." ContentType="..."/>`:按扩展名兜底;
7//! - `<Override PartName="..." ContentType="..."/>`:按 partname 精确覆盖。
8//!
9//! 实际解析时按"override 优先,default 次之"规则匹配。
10//!
11//! # 序列化
12//!
13//! - [`ContentTypes::to_xml`] → 序列化;
14//! - 解析在 `OpcPackage::load` 流程中通过 [`super::package::parse_content_types_public`] 完成。
15//!
16//! # 与 OOXML 规范的对应
17//!
18//! - 命名空间 `http://schemas.openxmlformats.org/package/2006/content-types`;
19//! - 默认值(`xml` / `rels` / `png` / `jpeg` / ...)由 [`ContentTypes::new_default`]
20//!   预置,符合 PowerPoint 输出的标准文件。
21
22use std::collections::BTreeMap;
23
24/// 默认 Content-Type(按扩展名匹配)。
25#[derive(Debug, Clone)]
26pub struct DefaultExt {
27    /// 扩展名(不带 `.`)。
28    pub extension: String,
29    /// Content-Type 字符串。
30    pub content_type: String,
31}
32
33impl DefaultExt {
34    /// 构造一条 Default。自动去掉前导 `.`。
35    pub fn new(ext: impl Into<String>, ct: impl Into<String>) -> Self {
36        DefaultExt {
37            extension: ext.into().trim_start_matches('.').to_string(),
38            content_type: ct.into(),
39        }
40    }
41}
42
43/// Override:按 part 名称覆盖 Content-Type(通常用于 XML 部件)。
44#[derive(Debug, Clone)]
45pub struct Override {
46    /// Part 名称(绝对路径,含前导 `/`)。
47    pub partname: String,
48    /// Content-Type 字符串。
49    pub content_type: String,
50}
51
52/// `[Content_Types].xml` 模型。
53///
54/// 内部用 `BTreeMap` 索引 `by_partname` 提供 O(log n) 的"是否已 override"查询;
55/// `Vec<Override>` 用于序列化时保留插入顺序。
56#[derive(Debug, Clone, Default)]
57pub struct ContentTypes {
58    /// 默认项(按扩展名)。
59    pub defaults: Vec<DefaultExt>,
60    /// 覆盖项(按 partname)。
61    pub overrides: Vec<Override>,
62    by_partname: BTreeMap<String, usize>,
63}
64
65impl ContentTypes {
66    /// 构造一个带默认项的 ContentTypes(xml/rels/png/jpeg/jpg/gif/bmp/svg)。
67    ///
68    /// 默认项与 PowerPoint 标准输出对齐,可直接被 Office 解析。
69    #[allow(clippy::field_reassign_with_default)]
70    pub fn new_default() -> Self {
71        let mut ct = ContentTypes::default();
72        ct.defaults.push(DefaultExt::new("xml", "application/xml"));
73        ct.defaults.push(DefaultExt::new(
74            "rels",
75            "application/vnd.openxmlformats-package.relationships+xml",
76        ));
77        ct.defaults.push(DefaultExt::new("png", "image/png"));
78        ct.defaults.push(DefaultExt::new("jpeg", "image/jpeg"));
79        ct.defaults.push(DefaultExt::new("jpg", "image/jpeg"));
80        ct.defaults.push(DefaultExt::new("gif", "image/gif"));
81        ct.defaults.push(DefaultExt::new("bmp", "image/bmp"));
82        ct.defaults.push(DefaultExt::new("svg", "image/svg+xml"));
83        ct
84    }
85
86    /// 添加或覆盖一个 partname。
87    ///
88    /// 已存在则**不覆盖**——首个插入的 Content-Type 生效(与 python-pptx 行为一致)。
89    pub fn add_override(&mut self, partname: &str, content_type: &str) {
90        if self.by_partname.contains_key(partname) {
91            return;
92        }
93        let idx = self.overrides.len();
94        self.overrides.push(Override {
95            partname: partname.to_string(),
96            content_type: content_type.to_string(),
97        });
98        self.by_partname.insert(partname.to_string(), idx);
99    }
100
101    /// 是否已 override。
102    pub fn has_override(&self, partname: &str) -> bool {
103        self.by_partname.contains_key(partname)
104    }
105
106    /// 转成 XML 字符串。
107    pub fn to_xml(&self) -> String {
108        let mut s = String::with_capacity(512);
109        s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
110        s.push_str(
111            "<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">",
112        );
113        for d in &self.defaults {
114            s.push_str(&format!(
115                "<Default Extension=\"{}\" ContentType=\"{}\"/>",
116                super::rels::xml_escape(&d.extension),
117                super::rels::xml_escape(&d.content_type),
118            ));
119        }
120        for o in &self.overrides {
121            s.push_str(&format!(
122                "<Override PartName=\"{}\" ContentType=\"{}\"/>",
123                super::rels::xml_escape(&o.partname),
124                super::rels::xml_escape(&o.content_type),
125            ));
126        }
127        s.push_str("</Types>");
128        s
129    }
130}