Download src/spatial.rs from martinsmessias/rasgapdf: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/martinsmessias/rasgapdf/resolve/main/src/spatial.rs
- Command line
-
hf download hf://martinsmessias/rasgapdf/src/spatial.rs
-
curl -L -o spatial.rs https://huggingface.co/martinsmessias/rasgapdf/resolve/main/src/spatial.rs
14.4 kB
| use std::path::Path; | |
| use std::time::Instant; | |
| use mupdf::pdf::{PdfDocument, PdfPage}; | |
| use mupdf::{Document, TextPageFlags}; | |
| use rayon::prelude::*; | |
| use crate::drawing; | |
| use crate::image; | |
| use crate::table::{RawSpan, TableDetector}; | |
| use crate::types::*; | |
| /// DTOs mínimos do JSON de texto estruturado produzido pelo MuPDF. | |
| /// | |
| /// Substituem `mupdf::page::StextPage`: o `Block` daquele crate exige o campo `lines`, | |
| /// que **não existe** nos blocos de imagem (`{"type":"image","bbox":{...}}`) nem nos de | |
| /// estrutura. Com o tipo do crate, a desserialização falhava e a página inteira era | |
| /// descartada — apagando todo o texto de qualquer página que contivesse uma imagem. | |
| struct RawStextPage { | |
| blocks: Vec<RawBlock>, | |
| } | |
| struct RawBlock { | |
| block_type: String, | |
| bbox: Option<RawBBox>, | |
| lines: Vec<RawLine>, | |
| } | |
| struct RawBBox { | |
| x: f64, | |
| y: f64, | |
| w: f64, | |
| h: f64, | |
| } | |
| impl RawBBox { | |
| fn x0(&self) -> f32 { | |
| self.x as f32 | |
| } | |
| fn y0(&self) -> f32 { | |
| self.y as f32 | |
| } | |
| fn x1(&self) -> f32 { | |
| (self.x + self.w) as f32 | |
| } | |
| fn y1(&self) -> f32 { | |
| (self.y + self.h) as f32 | |
| } | |
| fn to_bounding_box(&self) -> BoundingBox { | |
| BoundingBox::new(self.x0(), self.y0(), self.x1(), self.y1()) | |
| } | |
| } | |
| struct RawLine { | |
| text: String, | |
| bbox: Option<RawBBox>, | |
| font: Option<RawFont>, | |
| } | |
| struct RawFont { | |
| name: String, | |
| size: f64, | |
| } | |
| /// União dos bounding boxes das linhas, usada quando o bloco não traz `bbox`. | |
| fn lines_bounding_box(lines: &[SpatialLine]) -> BoundingBox { | |
| let mut x0 = f32::MAX; | |
| let mut y0 = f32::MAX; | |
| let mut x1 = f32::MIN; | |
| let mut y1 = f32::MIN; | |
| for line in lines { | |
| x0 = x0.min(line.bbox.x0); | |
| y0 = y0.min(line.bbox.y0); | |
| x1 = x1.max(line.bbox.x1); | |
| y1 = y1.max(line.bbox.y1); | |
| } | |
| if x0 > x1 { | |
| BoundingBox::new(0.0, 0.0, 0.0, 0.0) | |
| } else { | |
| BoundingBox::new(x0, y0, x1, y1) | |
| } | |
| } | |
| /// Modo 1: Parse Bruto Espacial (Raw X/Y) | |
| /// Retorna exclusivamente a lista plana e direta de coordenadas físicas de cada elemento | |
| pub fn parse_raw_file(pdf_path: &str) -> Result<RawSpatialDocument, Box<dyn std::error::Error + Send + Sync>> { | |
| let start = Instant::now(); | |
| let path = Path::new(pdf_path); | |
| if !path.exists() { | |
| return Err(format!("PDF file not found: {}", pdf_path).into()); | |
| } | |
| let file_name = path.file_name() | |
| .map(|f| f.to_string_lossy().to_string()) | |
| .unwrap_or_else(|| "document.pdf".to_string()); | |
| let doc = Document::open(pdf_path)?; | |
| let total_pages = doc.page_count()? as usize; | |
| let pages_spans: Vec<Vec<RawCoordSpan>> = (0..total_pages) | |
| .into_par_iter() | |
| .map(|page_idx| { | |
| let local_doc = match Document::open(pdf_path) { | |
| Ok(d) => d, | |
| Err(_) => return Vec::new(), | |
| }; | |
| let page = match local_doc.load_page(page_idx as i32) { | |
| Ok(p) => p, | |
| Err(_) => return Vec::new(), | |
| }; | |
| let text_page = match page.to_text_page(TextPageFlags::empty()) { | |
| Ok(tp) => tp, | |
| Err(_) => return Vec::new(), | |
| }; | |
| let json_str = match text_page.to_json(1.0) { | |
| Ok(j) => j, | |
| Err(_) => return Vec::new(), | |
| }; | |
| let stext: RawStextPage = match serde_json::from_str(&json_str) { | |
| Ok(st) => st, | |
| Err(_) => return Vec::new(), | |
| }; | |
| let mut spans = Vec::new(); | |
| for block in stext.blocks { | |
| if block.block_type != "text" { | |
| continue; | |
| } | |
| for line in block.lines { | |
| let text = line.text.trim().to_string(); | |
| if !text.is_empty() { | |
| let Some(line_bbox) = line.bbox.as_ref() else { | |
| continue; | |
| }; | |
| spans.push(RawCoordSpan { | |
| page: page_idx + 1, | |
| x0: line_bbox.x0(), | |
| y0: line_bbox.y0(), | |
| x1: line_bbox.x1(), | |
| y1: line_bbox.y1(), | |
| font_size: line.font.as_ref().map(|f| f.size as f32).unwrap_or(0.0), | |
| font_name: line.font.as_ref().map(|f| f.name.clone()).unwrap_or_default(), | |
| text, | |
| }); | |
| } | |
| } | |
| } | |
| // Ordenação espacial estrita por linha (Y) e depois horizontal (X) | |
| spans.sort_by(|a, b| { | |
| let y_bucket_a = (a.y0 / 4.0).round() as i32; | |
| let y_bucket_b = (b.y0 / 4.0).round() as i32; | |
| if y_bucket_a != y_bucket_b { | |
| y_bucket_a.cmp(&y_bucket_b) | |
| } else { | |
| a.x0.partial_cmp(&b.x0).unwrap_or(std::cmp::Ordering::Equal) | |
| } | |
| }); | |
| spans | |
| }) | |
| .collect(); | |
| let mut all_spans = Vec::new(); | |
| for page_spans in pages_spans { | |
| all_spans.extend(page_spans); | |
| } | |
| let parse_time_ms = start.elapsed().as_secs_f64() * 1000.0; | |
| let total_spans = all_spans.len(); | |
| Ok(RawSpatialDocument { | |
| file_name, | |
| total_pages, | |
| total_spans, | |
| parse_time_ms, | |
| spans: all_spans, | |
| }) | |
| } | |
| /// Modo 2: Parse Estruturado (Hierárquico com Blocos e Tabelas) | |
| pub fn parse_file(pdf_path: &str, options: &ParserOptions) -> Result<SpatialDocument, Box<dyn std::error::Error + Send + Sync>> { | |
| let start = Instant::now(); | |
| let path = Path::new(pdf_path); | |
| if !path.exists() { | |
| return Err(format!("PDF file not found: {}", pdf_path).into()); | |
| } | |
| let file_name = path.file_name() | |
| .map(|f| f.to_string_lossy().to_string()) | |
| .unwrap_or_else(|| "document.pdf".to_string()); | |
| let doc = Document::open(pdf_path)?; | |
| let total_pages = doc.page_count()? as usize; | |
| let detector = TableDetector::new( | |
| options.min_table_columns, | |
| options.column_tolerance, | |
| options.row_tolerance, | |
| ); | |
| // Paralelização por página via Rayon | |
| let pages: Vec<SpatialPage> = (0..total_pages) | |
| .into_par_iter() | |
| .map(|page_idx| { | |
| let local_doc = match Document::open(pdf_path) { | |
| Ok(d) => d, | |
| Err(_) => return fallback_empty_page(page_idx + 1), | |
| }; | |
| let page = match local_doc.load_page(page_idx as i32) { | |
| Ok(p) => p, | |
| Err(_) => return fallback_empty_page(page_idx + 1), | |
| }; | |
| let bounds = page.bounds().unwrap_or(mupdf::Rect::new(0.0, 0.0, 595.0, 842.0)); | |
| let page_width = (bounds.x1 - bounds.x0).abs(); | |
| let page_height = (bounds.y1 - bounds.y0).abs(); | |
| // `PRESERVE_IMAGES` faz o MuPDF emitir blocos `{"type":"image"}` no JSON de texto, | |
| // permitindo correlacionar os metadados da imagem com seu retângulo de pintura. | |
| let text_flags = if options.extract_images { | |
| TextPageFlags::PRESERVE_IMAGES | |
| } else { | |
| TextPageFlags::empty() | |
| }; | |
| let text_page = match page.to_text_page(text_flags) { | |
| Ok(tp) => tp, | |
| Err(_) => return fallback_empty_page(page_idx + 1), | |
| }; | |
| let json_str = match text_page.to_json(1.0) { | |
| Ok(j) => j, | |
| Err(_) => return fallback_empty_page(page_idx + 1), | |
| }; | |
| let stext: RawStextPage = match serde_json::from_str(&json_str) { | |
| Ok(st) => st, | |
| Err(_) => return fallback_empty_page(page_idx + 1), | |
| }; | |
| let mut raw_spans: Vec<RawSpan> = Vec::new(); | |
| let mut text_blocks: Vec<SpatialBlock> = Vec::new(); | |
| let mut image_block_boxes: Vec<BoundingBox> = Vec::new(); | |
| let mut block_counter = 1; | |
| for block in stext.blocks { | |
| // Blocos de imagem: sem `lines`, apenas o retângulo de posicionamento. | |
| // Usados como aproximação do bbox das imagens embutidas. | |
| if block.block_type == "image" { | |
| if let Some(block_bbox) = block.bbox.as_ref() { | |
| image_block_boxes.push(block_bbox.to_bounding_box()); | |
| } | |
| continue; | |
| } | |
| if block.block_type != "text" { | |
| continue; | |
| } | |
| let block_bbox = block.bbox.as_ref().map(RawBBox::to_bounding_box); | |
| let mut lines: Vec<SpatialLine> = Vec::new(); | |
| for line in block.lines { | |
| let Some(line_bbox) = line.bbox.as_ref() else { | |
| continue; | |
| }; | |
| let text = line.text.trim().to_string(); | |
| if text.is_empty() { | |
| continue; | |
| } | |
| let x0 = line_bbox.x0(); | |
| let y0 = line_bbox.y0(); | |
| let x1 = line_bbox.x1(); | |
| let y1 = line_bbox.y1(); | |
| let font_size = line.font.as_ref().map(|f| f.size as f32).unwrap_or(0.0); | |
| let font_name = line.font.as_ref().map(|f| f.name.clone()).unwrap_or_default(); | |
| raw_spans.push(RawSpan { | |
| x0, | |
| y0, | |
| x1, | |
| y1, | |
| font_size, | |
| text: text.clone(), | |
| }); | |
| let span = SpatialSpan { | |
| text: text.clone(), | |
| bbox: BoundingBox::new(x0, y0, x1, y1), | |
| font_name, | |
| font_size, | |
| char_count: text.chars().count(), | |
| }; | |
| lines.push(SpatialLine { | |
| text, | |
| bbox: BoundingBox::new(x0, y0, x1, y1), | |
| spans: vec![span], | |
| }); | |
| } | |
| if !lines.is_empty() { | |
| let bbox = block_bbox.unwrap_or_else(|| lines_bounding_box(&lines)); | |
| text_blocks.push(SpatialBlock { | |
| block_id: block_counter, | |
| block_type: "text".to_string(), | |
| bbox, | |
| lines, | |
| }); | |
| block_counter += 1; | |
| } | |
| } | |
| // Ordena spans espacialmente (Top-to-Bottom, Left-to-Right) | |
| raw_spans.sort_by(|a, b| { | |
| let y_bucket_a = (a.y0 / options.row_tolerance).round() as i32; | |
| let y_bucket_b = (b.y0 / options.row_tolerance).round() as i32; | |
| if y_bucket_a != y_bucket_b { | |
| y_bucket_a.cmp(&y_bucket_b) | |
| } else { | |
| a.x0.partial_cmp(&b.x0).unwrap_or(std::cmp::Ordering::Equal) | |
| } | |
| }); | |
| // Extração de tabelas espaciais | |
| let (tables, _) = if options.detect_tables { | |
| detector.extract_tables(&raw_spans, 1) | |
| } else { | |
| (Vec::new(), raw_spans.clone()) | |
| }; | |
| // --- Gráficos (opcional): vetores e imagens embutidas --- | |
| let mut drawings: Vec<SpatialDrawing> = Vec::new(); | |
| let mut images: Vec<SpatialImage> = Vec::new(); | |
| let mut rotation: i32 = 0; | |
| if options.extract_drawings { | |
| if let Ok(found) = page.drawings() { | |
| drawings = drawing::map_drawings( | |
| &found, | |
| page_width, | |
| page_height, | |
| options.filter_background_fills, | |
| ); | |
| } | |
| } | |
| // A conversão em `PdfPage` também dá acesso à rotação real da página. | |
| if let Ok(pdf_page) = PdfPage::try_from(page) { | |
| rotation = pdf_page.rotation().unwrap_or(0); | |
| if options.extract_images { | |
| if let Ok(pdf_doc) = PdfDocument::try_from(local_doc) { | |
| if let Ok(infos) = pdf_page.images() { | |
| for (idx, info) in infos.iter().enumerate() { | |
| let bytes = image::extract_image_bytes(&pdf_doc, info.xref); | |
| let bbox = image_block_boxes.get(idx).cloned(); | |
| images.push(image::build_spatial_image( | |
| idx + 1, | |
| info, | |
| bbox, | |
| bytes.as_deref(), | |
| )); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| SpatialPage { | |
| page_number: page_idx + 1, | |
| width: page_width, | |
| height: page_height, | |
| rotation, | |
| text_blocks, | |
| tables, | |
| raw_span_count: raw_spans.len(), | |
| images, | |
| drawings, | |
| } | |
| }) | |
| .collect(); | |
| let parse_time_ms = start.elapsed().as_secs_f64() * 1000.0; | |
| Ok(SpatialDocument { | |
| file_name, | |
| total_pages, | |
| parse_time_ms, | |
| pages, | |
| }) | |
| } | |
| fn fallback_empty_page(page_number: usize) -> SpatialPage { | |
| SpatialPage { | |
| page_number, | |
| width: 595.0, | |
| height: 842.0, | |
| rotation: 0, | |
| text_blocks: Vec::new(), | |
| tables: Vec::new(), | |
| raw_span_count: 0, | |
| images: Vec::new(), | |
| drawings: Vec::new(), | |
| } | |
| } | |