use crate::cache::{GraphCacheKey, GraphCacheValue};
use crate::classifier::DocumentClassifier;
use crate::config::ParsingConfig;
use crate::graphs::builder::GraphBuilder;
use crate::preprocessors::{Preprocessor, TikaPreprocessor};
use crate::rules::{engine::DebugConfig, RuleEngine};
use crate::storage::{calculate_config_hash, calculate_pdf_hash, DocumentStorage, FileStorage};
use crate::types::*;
use anyhow::Result;
use std::path::Path;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, serde::Serialize)]
pub struct PipelineStages {
pub xhtml: String,
pub text_elements: Vec<PdfTextElement>,
pub parsed_elements: Vec<ParsedPdfElement>,
pub graph: DocumentGraph,
}
pub struct StepProfiler {
enabled: bool,
timings: Vec<(String, Duration)>,
}
impl StepProfiler {
pub fn new(enabled: bool) -> Self {
Self {
enabled,
timings: Vec::new(),
}
}
pub fn time_step<F, R>(&mut self, step_name: &str, f: F) -> R
where
F: FnOnce() -> R,
{
if !self.enabled {
return f();
}
let start = Instant::now();
let result = f();
let elapsed = start.elapsed();
self.timings.push((step_name.to_string(), elapsed));
println!("⏱️ {}: {:.0}ms", step_name, elapsed.as_millis());
result
}
pub fn print_summary(&self) {
if !self.enabled || self.timings.is_empty() {
return;
}
println!("\n📊 Performance Summary:");
let total: Duration = self.timings.iter().map(|(_, d)| *d).sum();
for (step, duration) in &self.timings {
let percentage = (duration.as_secs_f64() / total.as_secs_f64()) * 100.0;
println!(
" {:.<35} {:.0}ms ({:.1}%)",
step,
duration.as_millis(),
percentage
);
}
println!(" {:.<35} {:.0}ms", "Total", total.as_millis());
}
}
pub struct DocumentProcessor {
preprocessor: Box<dyn Preprocessor>,
storage: Box<dyn DocumentStorage + Send + Sync>,
classifier: DocumentClassifier,
rule_engine: RuleEngine,
graph_builder: GraphBuilder,
}
impl DocumentProcessor {
pub fn new_with_dependencies(
preprocessor: Box<dyn Preprocessor>,
storage: Box<dyn DocumentStorage + Send + Sync>,
) -> Result<Self> {
Ok(Self {
preprocessor,
storage,
classifier: DocumentClassifier::new(),
rule_engine: RuleEngine::new()?,
graph_builder: GraphBuilder::new(),
})
}
#[cfg(feature = "jni-backend")]
pub fn new_cli_jni(jre_path: &std::path::Path, jar_path: &std::path::Path) -> Result<Self> {
let preprocessor = Box::new(TikaPreprocessor::new_with_jni(jre_path, jar_path)?);
let storage = Box::new(FileStorage::new("cache")?);
Self::new_with_dependencies(preprocessor, storage)
}
#[cfg(feature = "jni-backend")]
pub fn new_cli_jni_with_cache(
jre_path: &std::path::Path,
jar_path: &std::path::Path,
cache_dir: &str,
) -> Result<Self> {
let preprocessor = Box::new(TikaPreprocessor::new_with_jni(jre_path, jar_path)?);
let storage = Box::new(FileStorage::new(cache_dir)?);
Self::new_with_dependencies(preprocessor, storage)
}
pub fn process_document_with_config_and_profiling(
&mut self,
input_path: &str,
config: &ParsingConfig,
enable_profiling: bool,
skip_cache: bool,
) -> Result<DocumentGraph> {
if enable_profiling {
self.process_document_with_config_and_profiler(
input_path,
config,
StepProfiler::new(true),
skip_cache,
)
} else if skip_cache {
self.process_document_with_config_and_profiler(
input_path,
config,
StepProfiler::new(false),
skip_cache,
)
} else {
self.process_document_with_config(input_path, config)
}
}
pub fn process_document_with_config(
&mut self,
input_path: &str,
config: &ParsingConfig,
) -> Result<DocumentGraph> {
let start_time = Instant::now();
let pdf_bytes = std::fs::read(input_path)?;
let pdf_hash = calculate_pdf_hash(&pdf_bytes);
let config_hash = calculate_config_hash(config)?;
let cache_key = GraphCacheKey::new(pdf_hash.clone(), config_hash);
if let Some(cached) = self.storage.get_graph_output(&cache_key)? {
println!("🎯 Cache hit: Found graph for PDF + config combination");
println!(
"⏱️ Total processing time: {:.3}s (cached)",
start_time.elapsed().as_secs_f64()
);
return Ok(cached.graph);
}
println!("📄 Processing document with config: {}", input_path);
let graph = self.process_with_config_flow(input_path, config)?;
let processing_time = start_time.elapsed().as_millis() as u64;
let cache_value = GraphCacheValue::new(graph.clone(), processing_time);
self.storage.store_graph_output(&cache_key, &cache_value)?;
println!(
"⏱️ Total processing time: {:.3}s",
start_time.elapsed().as_secs_f64()
);
Ok(graph)
}
fn process_document_with_config_and_profiler(
&mut self,
input_path: &str,
config: &ParsingConfig,
mut profiler: StepProfiler,
skip_cache: bool,
) -> Result<DocumentGraph> {
let start_time = Instant::now();
let (_pdf_hash, cache_key) = profiler.time_step("Cache Key Generation", || {
let pdf_bytes = std::fs::read(input_path)?;
let pdf_hash = calculate_pdf_hash(&pdf_bytes);
let config_hash = calculate_config_hash(config)?;
let cache_key = GraphCacheKey::new(pdf_hash.clone(), config_hash);
Ok::<(String, GraphCacheKey), anyhow::Error>((pdf_hash, cache_key))
})?;
let cached_result = if skip_cache {
println!("🚫 Skipping cache lookup (--skip-cache enabled)");
None
} else {
profiler.time_step("Cache Lookup", || self.storage.get_graph_output(&cache_key))?
};
if let Some(cached) = cached_result {
println!("🎯 Cache hit: Found graph for PDF + config combination");
profiler.print_summary();
println!(
"⏱️ Total processing time: {:.0}ms (cached)",
start_time.elapsed().as_millis()
);
return Ok(cached.graph);
}
println!("📄 Processing document with config: {}", input_path);
let graph =
self.process_with_config_flow_and_profiler(input_path, config, &mut profiler)?;
if !skip_cache {
profiler.time_step("Cache Storage", || {
let processing_time = start_time.elapsed().as_millis() as u64;
let cache_value = GraphCacheValue::new(graph.clone(), processing_time);
self.storage.store_graph_output(&cache_key, &cache_value)
})?;
} else {
println!("🚫 Skipping cache storage (--skip-cache enabled)");
}
profiler.print_summary();
println!(
"⏱️ Total processing time: {:.0}ms",
start_time.elapsed().as_millis()
);
Ok(graph)
}
fn process_with_config_flow(
&mut self,
input_path: &str,
config: &ParsingConfig,
) -> Result<DocumentGraph> {
let stage1_start = Instant::now();
let input_path = Path::new(input_path);
let preprocessor_output = self.preprocessor.process_file(input_path)?;
println!(
"⏱️ Preprocessing: {:.3}s",
stage1_start.elapsed().as_secs_f64()
);
let stage2_start = Instant::now();
let classification = self.classifier.classify(&preprocessor_output)?;
println!("📋 Document classified as: {:?}", classification);
println!(
"⏱️ Classification: {:.3}s",
stage2_start.elapsed().as_secs_f64()
);
let stage3_start = Instant::now();
let document_analysis =
DocumentAnalysis::analyze_text_elements(&preprocessor_output.text_elements);
let parsed_elements = if config.minimal_parse {
println!("🔄 Minimal parse mode - skipping rule processing");
self.rule_engine
.convert_text_elements_to_parsed(&preprocessor_output.text_elements)
} else {
let font_size_analysis = self.rule_engine.analyze_font_sizes(
&preprocessor_output.text_elements,
&preprocessor_output.style_data,
);
self.rule_engine.apply_rules_with_config(
&preprocessor_output.text_elements,
&classification,
&document_analysis,
&font_size_analysis,
&preprocessor_output.style_data,
config, )?
};
println!(
"⏱️ Rule processing: {:.3}s",
stage3_start.elapsed().as_secs_f64()
);
let stage4_start = Instant::now();
let inferred_title = infer_title(&parsed_elements);
let mut graph = self.graph_builder.build_graph(parsed_elements)?;
println!(
"⏱️ Graph construction: {:.3}s",
stage4_start.elapsed().as_secs_f64()
);
if let Some(title) = inferred_title {
graph.document_info.document_metadata.title = Some(title);
}
graph.document_info.document_metadata.merge_extracted(preprocessor_output.metadata);
graph.document_info.document_analysis = document_analysis;
graph.compute_structural_profile();
graph.compute_breadcrumbs();
Ok(graph)
}
fn process_with_config_flow_and_profiler(
&mut self,
input_path: &str,
config: &ParsingConfig,
profiler: &mut StepProfiler,
) -> Result<DocumentGraph> {
let input_path = Path::new(input_path);
let pdf_bytes = std::fs::read(input_path)?;
let markup = profiler.time_step("1. PDF → Markup", || {
self.preprocessor.parse_pdf_to_markup_language(&pdf_bytes)
})?;
let preprocessor_output = profiler.time_step("2. Markup → TextElements", || {
self.preprocessor
.parse_markup_to_preprocessor_output(&markup)
})?;
let classification = profiler.time_step("3. Classification", || {
self.classifier.classify(&preprocessor_output)
})?;
let document_analysis = profiler.time_step("4a. Document Analysis", || {
DocumentAnalysis::analyze_text_elements(&preprocessor_output.text_elements)
});
let parsed_elements = if config.minimal_parse {
profiler.time_step("4. Minimal Parse", || {
self.rule_engine
.convert_text_elements_to_parsed(&preprocessor_output.text_elements)
})
} else {
let font_size_analysis = profiler.time_step("4b. Font Analysis", || {
self.rule_engine.analyze_font_sizes(
&preprocessor_output.text_elements,
&preprocessor_output.style_data,
)
});
profiler.time_step("4c. Rules Processing", || {
self.rule_engine.apply_rules_with_config(
&preprocessor_output.text_elements,
&classification,
&document_analysis,
&font_size_analysis,
&preprocessor_output.style_data,
config,
)
})?
};
let inferred_title = infer_title(&parsed_elements);
let mut graph = profiler.time_step("5. Graph Construction", || {
self.graph_builder.build_graph(parsed_elements)
})?;
if let Some(title) = inferred_title {
graph.document_info.document_metadata.title = Some(title);
}
graph.document_info.document_metadata.merge_extracted(preprocessor_output.metadata);
graph.document_info.document_analysis = document_analysis;
graph.compute_structural_profile();
graph.compute_breadcrumbs();
Ok(graph)
}
pub fn process_document_with_options(
&mut self,
input_path: &str,
include_raw_tika: bool,
output_dir: Option<&str>,
debug_output: bool,
debug_filters: &[String],
minimal_parse: Option<bool>,
) -> Result<DocumentGraph> {
let start_time = Instant::now();
println!("📄 Processing document: {}", input_path);
let preprocessor_output = if include_raw_tika || output_dir.is_some() {
let input_path = Path::new(input_path);
let pdf_bytes = std::fs::read(input_path)?;
let markup = self.preprocessor.parse_pdf_to_markup_language(&pdf_bytes)?;
if include_raw_tika {
if let Some(output_dir) = output_dir {
use std::fs;
let raw_path = format!("{}/raw_tika_output.html", output_dir);
if let Err(e) = fs::write(&raw_path, &markup) {
println!("⚠️ Failed to save raw markup to {}: {}", raw_path, e);
} else {
println!("💾 Saved raw markup to {}", raw_path);
}
}
}
self.preprocessor
.parse_markup_to_preprocessor_output(&markup)?
} else {
let input_path = Path::new(input_path);
self.preprocessor.process_file(input_path)?
};
println!(
"⏱️ Preprocessing complete: {:.3}s",
start_time.elapsed().as_secs_f64()
);
let step2_start = Instant::now();
let classification = self.classifier.classify(&preprocessor_output)?;
println!("📋 Document classified as: {:?}", classification);
println!(
"⏱️ Text parsing: {:.3}s",
step2_start.elapsed().as_secs_f64()
);
let step3_start = Instant::now();
let document_analysis =
DocumentAnalysis::analyze_text_elements(&preprocessor_output.text_elements);
let parsed_elements = if minimal_parse.unwrap_or(false) {
println!("🔄 Minimal parse mode - skipping rule processing");
self.rule_engine
.convert_text_elements_to_parsed(&preprocessor_output.text_elements)
} else {
if debug_output {
let debug_config = DebugConfig {
enabled: true,
filter_patterns: debug_filters.to_vec(),
};
self.rule_engine.set_debug_config(debug_config);
}
let font_size_analysis = self.rule_engine.analyze_font_sizes(
&preprocessor_output.text_elements,
&preprocessor_output.style_data,
);
self.rule_engine.apply_rules(
&preprocessor_output.text_elements,
&classification,
&document_analysis,
&font_size_analysis,
&preprocessor_output.style_data,
)?
};
println!(
"⏱️ Rule processing: {:.3}s",
step3_start.elapsed().as_secs_f64()
);
let step4_start = Instant::now();
let inferred_title = infer_title(&parsed_elements);
let mut graph = self.graph_builder.build_graph(parsed_elements)?;
if let Some(title) = inferred_title {
graph.document_info.document_metadata.title = Some(title);
}
graph.document_info.document_metadata.merge_extracted(preprocessor_output.metadata);
graph.document_info.document_analysis = document_analysis;
graph.compute_structural_profile();
graph.compute_breadcrumbs();
println!(
"⏱️ Graph construction: {:.3}s",
step4_start.elapsed().as_secs_f64()
);
println!(
"⏱️ Total processing time: {:.3}s",
start_time.elapsed().as_secs_f64()
);
Ok(graph)
}
pub fn process_document_capture_stages(
&mut self,
input_path: &str,
config: &ParsingConfig,
) -> Result<PipelineStages> {
let input_path_ref = Path::new(input_path);
let pdf_bytes = std::fs::read(input_path_ref)?;
let xhtml = self.preprocessor.parse_pdf_to_markup_language(&pdf_bytes)?;
println!("📋 Stage 1a: XHTML captured ({} bytes)", xhtml.len());
let preprocessor_output = self
.preprocessor
.parse_markup_to_preprocessor_output(&xhtml)?;
let text_elements = preprocessor_output.text_elements.clone();
println!("📋 Stage 1b: {} TextElements captured", text_elements.len());
let classification = self.classifier.classify(&preprocessor_output)?;
let document_analysis =
DocumentAnalysis::analyze_text_elements(&preprocessor_output.text_elements);
let parsed_elements = if config.minimal_parse {
self.rule_engine
.convert_text_elements_to_parsed(&preprocessor_output.text_elements)
} else {
let font_size_analysis = self.rule_engine.analyze_font_sizes(
&preprocessor_output.text_elements,
&preprocessor_output.style_data,
);
self.rule_engine.apply_rules_with_config(
&preprocessor_output.text_elements,
&classification,
&document_analysis,
&font_size_analysis,
&preprocessor_output.style_data,
config,
)?
};
println!(
"📋 Stage 2: {} ParsedElements captured",
parsed_elements.len()
);
let inferred_title = infer_title(&parsed_elements);
let mut graph = self.graph_builder.build_graph(parsed_elements.clone())?;
if let Some(title) = inferred_title {
graph.document_info.document_metadata.title = Some(title);
}
graph.document_info.document_metadata.merge_extracted(preprocessor_output.metadata);
graph.document_info.document_analysis = document_analysis;
graph.compute_structural_profile();
graph.compute_breadcrumbs();
println!(
"📋 Stage 3: Graph captured ({} nodes)",
graph.nodes.len()
);
Ok(PipelineStages {
xhtml,
text_elements,
parsed_elements,
graph,
})
}
pub fn process_document(&mut self, input_path: &str) -> Result<DocumentGraph> {
let default_config = ParsingConfig::default();
self.process_document_with_config(input_path, &default_config)
}
pub fn process_document_with_config_file(
&mut self,
input_path: &str,
config_path: &str,
) -> Result<DocumentGraph> {
let config = ParsingConfig::load_from_file(config_path)?;
self.process_document_with_config(input_path, &config)
}
}