Skip to main content

easypdf_core/
metadata.rs

1//! PDF 文档的元数据类型。
2
3/// PDF 文档级元数据。
4///
5/// 映射到 PDF 文件中的 `/Info` 字典。
6#[derive(Debug, Clone, Default)]
7pub struct PdfMetadata {
8    /// 文档标题。
9    pub title: Option<String>,
10    /// 文档作者。
11    pub author: Option<String>,
12    /// 文档主题。
13    pub subject: Option<String>,
14    /// 与文档关联的关键词。
15    pub keywords: Option<String>,
16    /// 创建原始文档的应用程序。
17    pub creator: Option<String>,
18    /// 生成此 PDF 的应用程序(自动填充)。
19    pub producer: Option<String>,
20}
21
22impl PdfMetadata {
23    /// 创建所有字段为空的元数据。
24    #[must_use]
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// 设置标题。
30    #[must_use]
31    pub fn title(mut self, title: impl Into<String>) -> Self {
32        self.title = Some(title.into());
33        self
34    }
35
36    /// 设置作者。
37    #[must_use]
38    pub fn author(mut self, author: impl Into<String>) -> Self {
39        self.author = Some(author.into());
40        self
41    }
42
43    /// 设置主题。
44    #[must_use]
45    pub fn subject(mut self, subject: impl Into<String>) -> Self {
46        self.subject = Some(subject.into());
47        self
48    }
49
50    /// 设置关键词(逗号分隔)。
51    #[must_use]
52    pub fn keywords(mut self, keywords: impl Into<String>) -> Self {
53        self.keywords = Some(keywords.into());
54        self
55    }
56
57    /// 生成用于 PDF/A 兼容性的 XMP 元数据 XML 字符串。
58    #[must_use]
59    pub fn to_xmp(&self) -> String {
60        let title = self.title.as_deref().unwrap_or("");
61        let author = self.author.as_deref().unwrap_or("");
62        let subject = self.subject.as_deref().unwrap_or("");
63        let keywords = self.keywords.as_deref().unwrap_or("");
64        format!(
65            r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
66<x:xmpmeta xmlns:x="adobe:ns:meta/">
67 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
68  <rdf:Description rdf:about=""
69   xmlns:dc="http://purl.org/dc/elements/1.1/">
70   <dc:title><rdf:Alt><rdf:li xml:lang="x-default">{title}</rdf:li></rdf:Alt></dc:title>
71   <dc:creator><rdf:Seq><rdf:li>{author}</rdf:li></rdf:Seq></dc:creator>
72   <dc:description><rdf:Alt><rdf:li xml:lang="x-default">{subject}</rdf:li></rdf:Alt></dc:description>
73   <dc:subject><rdf:Bag><rdf:li>{keywords}</rdf:li></rdf:Bag></dc:subject>
74  </rdf:Description>
75 </rdf:RDF>
76</x:xmpmeta>
77<?xpacket end="w"?>"#
78        )
79    }
80}
81
82/// 单个书签/大纲条目。
83#[derive(Debug, Clone)]
84pub struct PdfBookmark {
85    /// 显示标题。
86    pub title: String,
87    /// 目标页码(一基)。
88    pub page: usize,
89    /// 子书签(用于层级大纲)。
90    pub children: Vec<PdfBookmark>,
91}
92
93impl PdfBookmark {
94    /// 创建新的顶层书签。
95    #[must_use]
96    pub fn new(title: impl Into<String>, page: usize) -> Self {
97        Self {
98            title: title.into(),
99            page,
100            children: Vec::new(),
101        }
102    }
103
104    /// 添加子书签。
105    #[must_use]
106    pub fn child(mut self, child: PdfBookmark) -> Self {
107        self.children.push(child);
108        self
109    }
110}
111
112#[cfg(test)]
113#[allow(clippy::uninlined_format_args, clippy::float_cmp)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn default_metadata() {
119        let meta = PdfMetadata::default();
120        assert!(meta.title.is_none());
121        assert!(meta.author.is_none());
122        assert!(meta.subject.is_none());
123        assert!(meta.keywords.is_none());
124        assert!(meta.creator.is_none());
125        assert!(meta.producer.is_none());
126    }
127
128    #[test]
129    fn new_is_default() {
130        let meta = PdfMetadata::new();
131        assert!(meta.title.is_none());
132    }
133
134    #[test]
135    fn title_builder() {
136        let meta = PdfMetadata::default().title("My Doc");
137        assert_eq!(meta.title.as_deref(), Some("My Doc"));
138    }
139
140    #[test]
141    fn author_builder() {
142        let meta = PdfMetadata::default().author("John");
143        assert_eq!(meta.author.as_deref(), Some("John"));
144    }
145
146    #[test]
147    fn subject_builder() {
148        let meta = PdfMetadata::default().subject("Test");
149        assert_eq!(meta.subject.as_deref(), Some("Test"));
150    }
151
152    #[test]
153    fn keywords_builder() {
154        let meta = PdfMetadata::default().keywords("a,b,c");
155        assert_eq!(meta.keywords.as_deref(), Some("a,b,c"));
156    }
157
158    #[test]
159    fn to_xmp_includes_fields() {
160        let meta = PdfMetadata::default()
161            .title("Title")
162            .author("Author")
163            .subject("Subject")
164            .keywords("kw");
165        let xmp = meta.to_xmp();
166        assert!(xmp.contains("Title"));
167        assert!(xmp.contains("Author"));
168        assert!(xmp.contains("Subject"));
169        assert!(xmp.contains("kw"));
170    }
171
172    #[test]
173    fn to_xmp_empty_fields() {
174        let meta = PdfMetadata::default();
175        let xmp = meta.to_xmp();
176        assert!(xmp.contains("xmpmeta"));
177    }
178
179    #[test]
180    fn debug_format() {
181        let meta = PdfMetadata::default();
182        let dbg = format!("{:?}", meta);
183        assert!(dbg.contains("PdfMetadata"));
184    }
185
186    #[test]
187    fn clone_preserves() {
188        let meta = PdfMetadata::default().title("Test");
189        let cloned = meta.clone();
190        assert_eq!(meta.title, cloned.title);
191    }
192
193    #[test]
194    fn bookmark_new() {
195        let bm = PdfBookmark::new("Chapter 1", 1);
196        assert_eq!(bm.title, "Chapter 1");
197        assert_eq!(bm.page, 1);
198        assert!(bm.children.is_empty());
199    }
200
201    #[test]
202    fn bookmark_with_child() {
203        let bm = PdfBookmark::new("Root", 1).child(PdfBookmark::new("Child", 2));
204        assert_eq!(bm.children.len(), 1);
205        assert_eq!(bm.children[0].title, "Child");
206    }
207
208    #[test]
209    fn bookmark_nested_children() {
210        let bm = PdfBookmark::new("Root", 1)
211            .child(PdfBookmark::new("L1", 2).child(PdfBookmark::new("L2", 3)));
212        assert_eq!(bm.children.len(), 1);
213        assert_eq!(bm.children[0].children.len(), 1);
214    }
215
216    #[test]
217    fn bookmark_debug() {
218        let bm = PdfBookmark::new("Test", 1);
219        let dbg = format!("{:?}", bm);
220        assert!(dbg.contains("PdfBookmark"));
221    }
222}