use crate::types::{PresentationMetadata, SlideGroupMeta};
pub fn extract_embedded_metadata(raw_pdfpc_data: Option<&str>) -> Option<PresentationMetadata> {
let data = raw_pdfpc_data?.trim();
if data.is_empty() {
return None;
}
let meta = crate::pdfpc::parser::parse_pdfpc_str(data);
if meta.groups.is_empty() && meta.notes.is_empty() && meta.end_slide.is_none() {
return None;
}
Some(meta)
}
pub fn load_metadata(
pdf_path: &std::path::Path,
embedded_pdfpc_data: Option<&str>,
) -> (PresentationMetadata, MetadataSource) {
use crate::format::SidecarFormat;
let dais_path = pdf_path.with_extension("dais");
if dais_path.exists() {
let format = crate::dais_format::DaisFormat;
if let Ok(meta) = format.read(&dais_path) {
return (meta, MetadataSource::Sidecar(dais_path));
}
tracing::warn!("Failed to parse sidecar file: {}", dais_path.display());
}
let sidecar_path = pdf_path.with_extension("pdfpc");
if sidecar_path.exists() {
let format = crate::pdfpc::PdfpcFormat;
if let Ok(meta) = format.read(&sidecar_path) {
return (meta, MetadataSource::Sidecar(sidecar_path));
}
tracing::warn!("Failed to parse sidecar file: {}", sidecar_path.display());
}
if let Some(meta) = extract_embedded_metadata(embedded_pdfpc_data) {
return (meta, MetadataSource::Embedded);
}
(PresentationMetadata::default(), MetadataSource::None)
}
#[derive(Debug, Clone)]
pub enum MetadataSource {
Embedded,
Sidecar(std::path::PathBuf),
None,
}
pub fn parse_overlay_groups(overlay_str: &str) -> Vec<SlideGroupMeta> {
let mut groups = Vec::new();
for line in overlay_str.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() == 2
&& let (Ok(start), Ok(end)) = (parts[0].parse::<usize>(), parts[1].parse::<usize>())
{
groups.push(SlideGroupMeta {
start_page: start.saturating_sub(1),
end_page: end.saturating_sub(1),
});
}
}
groups
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_none_from_empty() {
assert!(extract_embedded_metadata(None).is_none());
assert!(extract_embedded_metadata(Some("")).is_none());
assert!(extract_embedded_metadata(Some(" ")).is_none());
}
#[test]
fn extract_from_embedded_pdfpc_string() {
let data = "[notes]\n### 1\nHello\n[overlay]\n1 3\n";
let meta = extract_embedded_metadata(Some(data)).unwrap();
assert_eq!(meta.notes.len(), 1);
assert_eq!(meta.notes[&0], "Hello");
assert_eq!(meta.groups.len(), 1);
}
#[test]
fn extract_returns_none_if_no_useful_content() {
let data = "[file]\ntest.pdf\n";
assert!(extract_embedded_metadata(Some(data)).is_none());
}
#[test]
fn load_metadata_with_no_sources() {
let (meta, source) = load_metadata(std::path::Path::new("nonexistent.pdf"), None);
assert!(meta.groups.is_empty());
assert!(matches!(source, MetadataSource::None));
}
#[test]
fn load_metadata_embedded_fallback_when_no_sidecar() {
let data = "[overlay]\n1 3\n";
let (meta, source) = load_metadata(std::path::Path::new("nonexistent.pdf"), Some(data));
assert_eq!(meta.groups.len(), 1);
assert!(matches!(source, MetadataSource::Embedded));
}
#[test]
fn load_metadata_sidecar_overrides_embedded() {
use crate::format::SidecarFormat;
use crate::pdfpc::PdfpcFormat;
let dir = std::env::temp_dir().join("dais_test_sidecar_over_embedded");
let _ = std::fs::create_dir_all(&dir);
let pdf_path = dir.join("talk.pdf");
std::fs::write(&pdf_path, b"fake pdf").unwrap();
let sidecar_meta = PresentationMetadata {
title: Some("Sidecar".to_string()),
groups: vec![crate::types::SlideGroupMeta { start_page: 0, end_page: 2 }],
..Default::default()
};
PdfpcFormat.write(&dir.join("talk.pdfpc"), &sidecar_meta).unwrap();
let embedded = "[notes]\n### 1\nEmbedded note\n";
let (meta, source) = load_metadata(&pdf_path, Some(embedded));
assert_eq!(meta.title.as_deref(), Some("Sidecar"));
assert_eq!(meta.groups.len(), 1);
assert!(matches!(source, MetadataSource::Sidecar(_)));
let _ = std::fs::remove_file(dir.join("talk.pdf"));
let _ = std::fs::remove_file(dir.join("talk.pdfpc"));
}
#[test]
fn load_metadata_dais_over_pdfpc() {
use crate::dais_format::DaisFormat;
use crate::format::SidecarFormat;
use crate::pdfpc::PdfpcFormat;
let dir = std::env::temp_dir().join("dais_test_metadata_priority");
let _ = std::fs::create_dir_all(&dir);
let pdf_path = dir.join("talk.pdf");
std::fs::write(&pdf_path, b"fake pdf").unwrap();
let dais_meta =
PresentationMetadata { title: Some("From Dais".to_string()), ..Default::default() };
DaisFormat.write(&dir.join("talk.dais"), &dais_meta).unwrap();
let pdfpc_meta =
PresentationMetadata { title: Some("From Pdfpc".to_string()), ..Default::default() };
PdfpcFormat.write(&dir.join("talk.pdfpc"), &pdfpc_meta).unwrap();
let (meta, source) = load_metadata(&pdf_path, None);
assert_eq!(meta.title.as_deref(), Some("From Dais"));
assert!(
matches!(source, MetadataSource::Sidecar(ref p) if p.extension().unwrap() == "dais")
);
let _ = std::fs::remove_file(dir.join("talk.pdf"));
let _ = std::fs::remove_file(dir.join("talk.dais"));
let _ = std::fs::remove_file(dir.join("talk.pdfpc"));
}
#[test]
fn parse_overlay_groups_basic() {
let groups = parse_overlay_groups("1 3\n4 5\n");
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].start_page, 0);
assert_eq!(groups[0].end_page, 2);
assert_eq!(groups[1].start_page, 3);
assert_eq!(groups[1].end_page, 4);
}
}