Skip to main content

mupdf_basic_text_extractor/
lib.rs

1use mupdf::{TextPageOptions};
2
3#[derive(Default, Debug, PartialEq, Clone)]
4pub struct Fragment {
5    pub text: String,
6    pub x: f64,
7    pub y: f64,
8    pub font_name: Option<String>,
9    pub font_size: f64,
10    pub bbox_width: f64,
11    pub bbox_height: f64
12
13}
14#[derive(Default, Debug, Clone)]
15pub struct Line {
16    pub text_fragments: Vec<Fragment>
17
18}
19#[derive(Default, Debug, Clone)]
20pub struct Page {
21    pub lines: Vec<Line>
22}
23#[derive(Default, Debug, Clone)]
24pub struct Doc {
25    pub pages: Vec<Page>
26}
27
28pub fn get_structured_document_from_filepath(path: String)-> Result<Doc, Box<dyn std::error::Error>> {
29    let filename: String = path;
30    let document = mupdf::Document::open(&filename)?;
31    let mut new_document: Doc = Doc::default();
32    
33    for page in document.pages()? {
34        let pageref = page.as_ref();
35        let pagebounds = &pageref.unwrap().bounds()?;
36        let text_page = &page?.to_text_page(TextPageOptions::empty())?;
37        let text_blocks = text_page.blocks();
38
39        let mut raw_page_fragments = Vec::<Fragment>::default();
40
41        for block in text_blocks {
42            
43            let lines = block.lines();
44            for line in lines {
45                let chars = line.chars();
46                let mut current_working_fragment= Fragment::default();
47
48                let mut working_frag_reset = false;
49                let mut first_char = true;
50                let mut last_was_whitespace = false;
51                for char in chars {
52                    if let Some(c) = char.char() {
53                        if c.is_whitespace() {
54                            raw_page_fragments.push(current_working_fragment);
55                            current_working_fragment = Fragment::default();
56                            last_was_whitespace = true;
57                            continue;
58                        } else if last_was_whitespace {
59                            working_frag_reset = true;
60                            last_was_whitespace = false;
61                        }
62                    }
63
64                    if working_frag_reset {
65                        first_char = true;
66                        working_frag_reset = false;
67                    }
68
69                    if first_char {
70                        current_working_fragment.x = char.quad().ll.x as f64;
71                        current_working_fragment.y = pagebounds.height() as f64 - char.quad().ll.y as f64 ;
72
73                        current_working_fragment.font_size = char.size() as f64;
74                        current_working_fragment.bbox_width = (char.quad().lr.x - char.quad().ll.x) as f64;
75                        current_working_fragment.bbox_height = (char.quad().ur.y - char.quad().lr.y) as f64;
76                        
77                        first_char = false;
78                    }
79                    if let Some(ch) = char.char() {
80                        current_working_fragment.text.push(ch);
81                    }
82                    
83                }
84                if current_working_fragment != Fragment::default() {
85                    raw_page_fragments.push(current_working_fragment);
86                }
87            }
88        }
89
90        let mut new_page = Page::default();
91
92        let mut current_working_newline = Line::default();
93        let mut working_newline_reset = false;
94        let mut last_y:f64 = 0.0;
95        if let Some(first_fr) = raw_page_fragments.first() {
96            last_y = first_fr.y.clone();
97        }
98
99        for fr in &raw_page_fragments {
100            if working_newline_reset {
101                working_newline_reset = false;
102            }
103            if fr.y != last_y {
104                new_page.lines.push(current_working_newline.clone());
105                current_working_newline = Line::default();
106                working_newline_reset = true;
107            }
108            current_working_newline.text_fragments.push(fr.clone());
109            last_y = fr.y;
110        }
111        if working_newline_reset {
112            new_page.lines.push(current_working_newline);
113        }
114
115        for line in &mut new_page.lines {
116            line.text_fragments.sort_by(|a, b| a.x.total_cmp(&b.x));
117        }
118        new_page.lines.sort_by(|a, b| b.text_fragments[0].y.total_cmp(&a.text_fragments[0].y)); // should sort lines by y position properly
119
120        new_document.pages.push(new_page);
121    }
122
123    Ok(new_document)
124}
125
126#[cfg(test)]
127mod tests {
128
129    use std::fs;
130
131    use super::*;
132
133    #[test]
134    fn it_works() {
135        let structured_doc_extracted =
136        get_structured_document_from_filepath("test_pdfs/VCR2L.pdf".into());
137        
138        if let Ok(doc) = structured_doc_extracted {
139            for page in doc.pages {
140                println!("PAGE -----");
141                for line in page.lines {
142                    print!("     LINE: ----- ");
143                    if let Some(first_frag) = line.text_fragments.first() {
144                        print!("Y: {}, FONTSIZE: {}", first_frag.y, first_frag.font_size);
145                        println!("\n");
146                    }
147                    print!("          ");
148                    for frag in line.text_fragments {
149                        print!("'{}', X:{:?} |", frag.text, frag.x);
150                    }
151                    println!("");
152                }
153            }
154        }
155
156    }
157}