1use crate::types::{PresentationMetadata, SlideGroupMeta};
2
3pub fn extract_embedded_metadata(raw_pdfpc_data: Option<&str>) -> Option<PresentationMetadata> {
11 let data = raw_pdfpc_data?.trim();
12 if data.is_empty() {
13 return None;
14 }
15
16 let meta = crate::pdfpc::parser::parse_pdfpc_str(data);
18
19 if meta.groups.is_empty() && meta.notes.is_empty() && meta.end_slide.is_none() {
21 return None;
22 }
23
24 Some(meta)
25}
26
27pub fn load_metadata(
37 pdf_path: &std::path::Path,
38 embedded_pdfpc_data: Option<&str>,
39) -> (PresentationMetadata, MetadataSource) {
40 use crate::format::SidecarFormat;
41
42 let dais_path = pdf_path.with_extension("dais");
44 if dais_path.exists() {
45 let format = crate::dais_format::DaisFormat;
46 if let Ok(meta) = format.read(&dais_path) {
47 return (meta, MetadataSource::Sidecar(dais_path));
48 }
49 tracing::warn!("Failed to parse sidecar file: {}", dais_path.display());
50 }
51
52 let sidecar_path = pdf_path.with_extension("pdfpc");
54 if sidecar_path.exists() {
55 let format = crate::pdfpc::PdfpcFormat;
56 if let Ok(meta) = format.read(&sidecar_path) {
57 return (meta, MetadataSource::Sidecar(sidecar_path));
58 }
59 tracing::warn!("Failed to parse sidecar file: {}", sidecar_path.display());
60 }
61
62 if let Some(meta) = extract_embedded_metadata(embedded_pdfpc_data) {
64 return (meta, MetadataSource::Embedded);
65 }
66
67 (PresentationMetadata::default(), MetadataSource::None)
69}
70
71#[derive(Debug, Clone)]
73pub enum MetadataSource {
74 Embedded,
76 Sidecar(std::path::PathBuf),
78 None,
80}
81
82pub fn parse_overlay_groups(overlay_str: &str) -> Vec<SlideGroupMeta> {
86 let mut groups = Vec::new();
87 for line in overlay_str.lines() {
88 let parts: Vec<&str> = line.split_whitespace().collect();
89 if parts.len() == 2
90 && let (Ok(start), Ok(end)) = (parts[0].parse::<usize>(), parts[1].parse::<usize>())
91 {
92 groups.push(SlideGroupMeta {
93 start_page: start.saturating_sub(1),
94 end_page: end.saturating_sub(1),
95 });
96 }
97 }
98 groups
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn extract_none_from_empty() {
107 assert!(extract_embedded_metadata(None).is_none());
108 assert!(extract_embedded_metadata(Some("")).is_none());
109 assert!(extract_embedded_metadata(Some(" ")).is_none());
110 }
111
112 #[test]
113 fn extract_from_embedded_pdfpc_string() {
114 let data = "[notes]\n### 1\nHello\n[overlay]\n1 3\n";
115 let meta = extract_embedded_metadata(Some(data)).unwrap();
116 assert_eq!(meta.notes.len(), 1);
117 assert_eq!(meta.notes[&0], "Hello");
118 assert_eq!(meta.groups.len(), 1);
119 }
120
121 #[test]
122 fn extract_returns_none_if_no_useful_content() {
123 let data = "[file]\ntest.pdf\n";
124 assert!(extract_embedded_metadata(Some(data)).is_none());
125 }
126
127 #[test]
128 fn load_metadata_with_no_sources() {
129 let (meta, source) = load_metadata(std::path::Path::new("nonexistent.pdf"), None);
130 assert!(meta.groups.is_empty());
131 assert!(matches!(source, MetadataSource::None));
132 }
133
134 #[test]
135 fn load_metadata_embedded_fallback_when_no_sidecar() {
136 let data = "[overlay]\n1 3\n";
137 let (meta, source) = load_metadata(std::path::Path::new("nonexistent.pdf"), Some(data));
138 assert_eq!(meta.groups.len(), 1);
139 assert!(matches!(source, MetadataSource::Embedded));
140 }
141
142 #[test]
143 fn load_metadata_sidecar_overrides_embedded() {
144 use crate::format::SidecarFormat;
145 use crate::pdfpc::PdfpcFormat;
146
147 let dir = std::env::temp_dir().join("dais_test_sidecar_over_embedded");
148 let _ = std::fs::create_dir_all(&dir);
149 let pdf_path = dir.join("talk.pdf");
150 std::fs::write(&pdf_path, b"fake pdf").unwrap();
151
152 let sidecar_meta = PresentationMetadata {
154 title: Some("Sidecar".to_string()),
155 groups: vec![crate::types::SlideGroupMeta { start_page: 0, end_page: 2 }],
156 ..Default::default()
157 };
158 PdfpcFormat.write(&dir.join("talk.pdfpc"), &sidecar_meta).unwrap();
159
160 let embedded = "[notes]\n### 1\nEmbedded note\n";
162 let (meta, source) = load_metadata(&pdf_path, Some(embedded));
163
164 assert_eq!(meta.title.as_deref(), Some("Sidecar"));
166 assert_eq!(meta.groups.len(), 1);
167 assert!(matches!(source, MetadataSource::Sidecar(_)));
168
169 let _ = std::fs::remove_file(dir.join("talk.pdf"));
170 let _ = std::fs::remove_file(dir.join("talk.pdfpc"));
171 }
172
173 #[test]
174 fn load_metadata_dais_over_pdfpc() {
175 use crate::dais_format::DaisFormat;
176 use crate::format::SidecarFormat;
177 use crate::pdfpc::PdfpcFormat;
178
179 let dir = std::env::temp_dir().join("dais_test_metadata_priority");
180 let _ = std::fs::create_dir_all(&dir);
181 let pdf_path = dir.join("talk.pdf");
182 std::fs::write(&pdf_path, b"fake pdf").unwrap();
183
184 let dais_meta =
186 PresentationMetadata { title: Some("From Dais".to_string()), ..Default::default() };
187 DaisFormat.write(&dir.join("talk.dais"), &dais_meta).unwrap();
188
189 let pdfpc_meta =
191 PresentationMetadata { title: Some("From Pdfpc".to_string()), ..Default::default() };
192 PdfpcFormat.write(&dir.join("talk.pdfpc"), &pdfpc_meta).unwrap();
193
194 let (meta, source) = load_metadata(&pdf_path, None);
196 assert_eq!(meta.title.as_deref(), Some("From Dais"));
197 assert!(
198 matches!(source, MetadataSource::Sidecar(ref p) if p.extension().unwrap() == "dais")
199 );
200
201 let _ = std::fs::remove_file(dir.join("talk.pdf"));
203 let _ = std::fs::remove_file(dir.join("talk.dais"));
204 let _ = std::fs::remove_file(dir.join("talk.pdfpc"));
205 }
206
207 #[test]
208 fn parse_overlay_groups_basic() {
209 let groups = parse_overlay_groups("1 3\n4 5\n");
210 assert_eq!(groups.len(), 2);
211 assert_eq!(groups[0].start_page, 0);
212 assert_eq!(groups[0].end_page, 2);
213 assert_eq!(groups[1].start_page, 3);
214 assert_eq!(groups[1].end_page, 4);
215 }
216}