Skip to main content

fastpaper/
read.rs

1use std::path::Path;
2
3/// Extract full text from a local PDF file.
4pub fn extract_text(path: &Path) -> Result<String, String> {
5    let bytes = std::fs::read(path).map_err(|e| format!("Failed to read file: {}", e))?;
6    extract_text_from_bytes(&bytes)
7}
8
9/// Extract full text from PDF bytes in memory.
10pub fn extract_text_from_bytes(bytes: &[u8]) -> Result<String, String> {
11    let doc = pdf_oxide::PdfDocument::from_bytes(bytes.to_vec())
12        .map_err(|e| format!("Failed to open PDF: {}", e))?;
13    let text = doc
14        .extract_all_text()
15        .map_err(|e| format!("Failed to extract text: {}", e))?;
16    Ok(text)
17}
18
19/// Extract only the abstract section from PDF text.
20pub fn extract_section_abstract(full_text: &str) -> Option<String> {
21    let lower = full_text.to_lowercase();
22    let start = lower.find("abstract")?;
23    let after_abstract = start + "abstract".len();
24    // Find next section heading
25    let section_headings = [
26        "introduction",
27        "background",
28        "related work",
29        "methods",
30        "methodology",
31        "results",
32        "discussion",
33        "conclusion",
34    ];
35    let end = section_headings
36        .iter()
37        .filter_map(|h| lower[after_abstract..].find(h).map(|i| after_abstract + i))
38        .min()
39        .unwrap_or(full_text.len());
40    let section = full_text[after_abstract..end].trim();
41    if section.is_empty() {
42        None
43    } else {
44        Some(section.to_string())
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use std::path::PathBuf;
52
53    fn fixture_path() -> PathBuf {
54        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/test.pdf")
55    }
56
57    // Behavior 1: read local test.pdf → output contains text
58    #[test]
59    fn extract_text_returns_content() {
60        let text = extract_text(&fixture_path()).unwrap();
61        assert!(!text.is_empty(), "extracted text should not be empty");
62    }
63
64    #[test]
65    fn extract_text_contains_expected_words() {
66        let text = extract_text(&fixture_path()).unwrap();
67        assert!(
68            text.contains("Transformer") || text.contains("attention") || text.contains("Attention"),
69            "text should contain expected words, got: {}",
70            &text[..text.len().min(200)]
71        );
72    }
73
74    // Behavior 2: --section abstract → only abstract part
75    #[test]
76    fn extract_section_abstract_returns_content() {
77        let text = extract_text(&fixture_path()).unwrap();
78        let abstract_text = extract_section_abstract(&text);
79        assert!(abstract_text.is_some(), "should find abstract section");
80        let abs = abstract_text.unwrap();
81        assert!(!abs.is_empty());
82    }
83
84    #[test]
85    fn extract_section_abstract_does_not_contain_introduction() {
86        let text = extract_text(&fixture_path()).unwrap();
87        let abs = extract_section_abstract(&text).unwrap();
88        let lower = abs.to_lowercase();
89        assert!(
90            !lower.contains("introduction"),
91            "abstract should not contain introduction heading, got: {}",
92            abs
93        );
94    }
95
96    #[test]
97    fn extract_text_from_bytes_works() {
98        let bytes = std::fs::read(fixture_path()).unwrap();
99        let text = extract_text_from_bytes(&bytes).unwrap();
100        assert!(!text.is_empty());
101    }
102
103    #[test]
104    fn extract_text_nonexistent_file_returns_err() {
105        let result = extract_text(Path::new("/nonexistent/fake.pdf"));
106        assert!(result.is_err());
107    }
108}