mod cache;
mod execution;
pub(crate) mod features;
mod format;
mod initialization;
#[cfg(test)]
mod tests;
pub use cache::clear_processor_cache;
pub use format::apply_output_format;
use crate::Result;
use crate::core::config::ExtractionConfig;
use crate::types::ExtractedDocument;
use crate::types::internal::InternalDocument;
use execution::{execute_processor_stages, execute_validators};
use features::{execute_chunking, execute_language_detection, execute_token_reduction};
use initialization::{get_processors_from_cache, initialize_features, initialize_processor_cache};
#[cfg_attr(feature = "otel", tracing::instrument(
skip(doc, config),
fields(
pipeline.stage = "post_processing",
content.element_count = doc.elements.len(),
)
))]
#[cfg_attr(alef, alef(skip))]
pub async fn run_pipeline(mut doc: InternalDocument, config: &ExtractionConfig) -> Result<ExtractedDocument> {
doc.ocr_text_only = config.images.as_ref().map(|i| i.ocr_text_only).unwrap_or(false);
doc.append_ocr_text = config.images.as_ref().map(|i| i.append_ocr_text).unwrap_or(false);
doc.escape_markdown = config.escape_markdown;
doc.table_anchors = config.table_anchors;
#[cfg(all(feature = "ocr", feature = "tokio-runtime"))]
let image_ocr_enabled = config.images.as_ref().map(|i| i.run_ocr_on_images).unwrap_or(true);
#[cfg(all(feature = "ocr", feature = "tokio-runtime"))]
if image_ocr_enabled && config.ocr.is_some() && !doc.images.is_empty() {
let images_to_process = std::mem::take(&mut doc.images);
match crate::extraction::image_ocr::process_images_with_ocr(
images_to_process,
config,
&mut doc.processing_warnings,
)
.await
{
Ok(processed) => {
doc.images = processed;
}
Err(e) => {
doc.processing_warnings.push(crate::types::ProcessingWarning {
source: std::borrow::Cow::Borrowed("image_ocr"),
message: std::borrow::Cow::Owned(format!("Image OCR failed: {e}")),
});
}
}
}
replace_embedded_image_markdown_with_ocr(&mut doc);
append_embedded_image_ocr_text(&mut doc);
#[cfg(feature = "chunking")]
let chunker_heading_source = {
let needs_markdown = config.chunking.as_ref().is_some_and(|c| {
c.chunker_type == crate::core::config::ChunkerType::Markdown
|| c.resolve_preset().chunker_type == crate::core::config::ChunkerType::Markdown
}) && config.output_format == crate::core::config::OutputFormat::Plain;
if needs_markdown {
Some(crate::rendering::render_markdown(&doc))
} else {
None
}
};
#[cfg(feature = "html")]
let styled_html_prerender: Option<String> = {
use crate::plugins::InternalRenderer as _;
if config.output_format == crate::core::config::OutputFormat::Html {
config.html_output.as_ref().and_then(|html_cfg| {
match crate::rendering::StyledHtmlRenderer::new(html_cfg.clone()) {
Ok(renderer) => match renderer.render(&doc) {
Ok(html) => Some(html),
Err(e) => {
tracing::warn!("StyledHtmlRenderer render failed, falling back to default HTML: {e}");
None
}
},
Err(e) => {
tracing::warn!("StyledHtmlRenderer construction failed, falling back to default HTML: {e}");
None
}
}
})
} else {
None
}
};
let doc_for_elements = if config.result_format == crate::types::ResultFormat::ElementBased {
Some(doc.clone())
} else {
None
};
let include_structure = config.include_document_structure;
let mut result =
crate::extraction::derive::derive_extraction_result(doc, include_structure, config.output_format.clone());
result.internal_document = doc_for_elements;
#[cfg(feature = "html")]
if let Some(html) = styled_html_prerender {
result.formatted_content = Some(html);
}
#[cfg(feature = "chunking")]
let chunker_only_markdown = result.formatted_content.is_none();
#[cfg(feature = "chunking")]
if chunker_only_markdown && let Some(md) = chunker_heading_source {
result.formatted_content = Some(md);
}
#[cfg(feature = "image-encode")]
if let Some(ref image_cfg) = config.images {
apply_output_format_pass(&mut result, image_cfg);
}
if let Some(ref image_cfg) = config.images {
apply_data_base64_pass(&mut result, image_cfg);
}
let pp_config = config.postprocessor.as_ref();
let postprocessing_enabled = pp_config.is_none_or(|c| c.enabled);
let processor_stages = if postprocessing_enabled {
initialize_features();
initialize_processor_cache()?;
let (early_processors, middle_processors, late_processors) = get_processors_from_cache()?;
Some((early_processors, middle_processors, late_processors))
} else {
None
};
if let Some((early_processors, _, _)) = &processor_stages {
execute_processor_stages(
&mut result,
config,
&pp_config,
&[(
crate::plugins::ProcessingStage::Early,
std::sync::Arc::clone(early_processors),
)],
)
.await?;
}
execute_language_detection(&mut result, config)?;
execute_chunking(&mut result, config)?;
#[cfg(feature = "chunking")]
if chunker_only_markdown {
result.formatted_content = None;
}
if let Some((_, middle_processors, late_processors)) = &processor_stages {
execute_processor_stages(
&mut result,
config,
&pp_config,
&[
(
crate::plugins::ProcessingStage::Middle,
std::sync::Arc::clone(middle_processors),
),
(
crate::plugins::ProcessingStage::Late,
std::sync::Arc::clone(late_processors),
),
],
)
.await?;
}
execute_token_reduction(&mut result, config)?;
execute_validators(&result, config).await?;
apply_element_transform(&mut result, config);
normalize_nfc(&mut result);
#[cfg(all(feature = "liter-llm", not(target_arch = "wasm32")))]
if let Some(ref structured_config) = config.structured_extraction {
match crate::llm::structured::extract_structured(&result.content, structured_config).await {
Ok((output, usage)) => {
result.structured_output = Some(output);
crate::llm::usage::push_llm_usage(&mut result, usage);
}
Err(e) => {
tracing::warn!("Structured extraction failed: {e}");
result.processing_warnings.push(crate::types::ProcessingWarning {
source: std::borrow::Cow::Borrowed("structured_extraction"),
message: std::borrow::Cow::Owned(format!("Structured extraction failed: {e}")),
});
}
}
}
#[cfg(not(feature = "liter-llm"))]
if config.structured_extraction.is_some() {
result.processing_warnings.push(crate::types::ProcessingWarning {
source: std::borrow::Cow::Borrowed("structured_extraction"),
message: std::borrow::Cow::Borrowed("Structured extraction requires the 'liter-llm' feature"),
});
}
#[cfg(all(feature = "liter-llm", target_arch = "wasm32"))]
if config.structured_extraction.is_some() {
result.processing_warnings.push(crate::types::ProcessingWarning {
source: std::borrow::Cow::Borrowed("structured_extraction"),
message: std::borrow::Cow::Borrowed("Structured extraction is not available on wasm builds"),
});
}
result = apply_output_format(result, config.output_format.clone());
populate_document_counts(&mut result);
#[cfg(feature = "heuristics")]
{
use crate::heuristics::confidence::{ConfidenceSignals, ConfidenceWeights, SchemaCompliance, score_confidence};
const DEFAULT_TEXT_COVERAGE: f32 = 1.0;
let signals =
ConfidenceSignals::from_extraction_result(&result, SchemaCompliance::AllValid, DEFAULT_TEXT_COVERAGE);
result.extraction_confidence = Some(score_confidence(signals, ConfidenceWeights::default()));
}
Ok(result)
}
#[cfg(not(feature = "tokio-runtime"))]
#[cfg_attr(alef, alef(skip))]
pub fn run_pipeline_sync(mut doc: InternalDocument, config: &ExtractionConfig) -> Result<ExtractedDocument> {
doc.escape_markdown = config.escape_markdown;
doc.table_anchors = config.table_anchors;
#[cfg(feature = "chunking")]
let chunker_heading_source = {
let needs_markdown = config.chunking.as_ref().is_some_and(|c| {
c.chunker_type == crate::core::config::ChunkerType::Markdown
|| c.resolve_preset().chunker_type == crate::core::config::ChunkerType::Markdown
}) && config.output_format == crate::core::config::OutputFormat::Plain;
if needs_markdown {
Some(crate::rendering::render_markdown(&doc))
} else {
None
}
};
#[cfg(feature = "html")]
let styled_html_prerender: Option<String> = {
use crate::plugins::InternalRenderer as _;
if config.output_format == crate::core::config::OutputFormat::Html {
config.html_output.as_ref().and_then(|html_cfg| {
match crate::rendering::StyledHtmlRenderer::new(html_cfg.clone()) {
Ok(renderer) => match renderer.render(&doc) {
Ok(html) => Some(html),
Err(e) => {
tracing::warn!("StyledHtmlRenderer render failed, falling back to default HTML: {e}");
None
}
},
Err(e) => {
tracing::warn!("StyledHtmlRenderer construction failed, falling back to default HTML: {e}");
None
}
}
})
} else {
None
}
};
let doc_for_elements = if config.result_format == crate::types::ResultFormat::ElementBased {
Some(doc.clone())
} else {
None
};
let include_structure = config.include_document_structure;
let mut result =
crate::extraction::derive::derive_extraction_result(doc, include_structure, config.output_format.clone());
result.internal_document = doc_for_elements;
#[cfg(feature = "html")]
if let Some(html) = styled_html_prerender {
result.formatted_content = Some(html);
}
#[cfg(feature = "chunking")]
let chunker_only_markdown = result.formatted_content.is_none();
#[cfg(feature = "chunking")]
if chunker_only_markdown && let Some(md) = chunker_heading_source {
result.formatted_content = Some(md);
}
#[cfg(feature = "image-encode")]
if let Some(ref image_cfg) = config.images {
apply_output_format_pass(&mut result, image_cfg);
}
if let Some(ref image_cfg) = config.images {
apply_data_base64_pass(&mut result, image_cfg);
}
execute_chunking(&mut result, config)?;
#[cfg(feature = "chunking")]
if chunker_only_markdown {
result.formatted_content = None;
}
execute_language_detection(&mut result, config)?;
execute_token_reduction(&mut result, config)?;
apply_element_transform(&mut result, config);
normalize_nfc(&mut result);
result = apply_output_format(result, config.output_format.clone());
populate_document_counts(&mut result);
#[cfg(feature = "heuristics")]
{
use crate::heuristics::confidence::{ConfidenceSignals, ConfidenceWeights, SchemaCompliance, score_confidence};
const DEFAULT_TEXT_COVERAGE: f32 = 1.0;
let signals =
ConfidenceSignals::from_extraction_result(&result, SchemaCompliance::AllValid, DEFAULT_TEXT_COVERAGE);
result.extraction_confidence = Some(score_confidence(signals, ConfidenceWeights::default()));
}
Ok(result)
}
fn populate_document_counts(result: &mut ExtractedDocument) {
let pages = result
.metadata
.pages
.as_ref()
.map(|p| p.total_count as usize)
.filter(|&n| n > 0)
.or_else(|| result.pages.as_ref().map(Vec::len))
.unwrap_or(0);
result.counts = crate::types::DocumentCounts {
pages,
tables: result.tables.len(),
images: result.images.as_ref().map_or(0, Vec::len),
};
}
#[cfg(feature = "image-encode")]
fn apply_output_format_pass(
result: &mut ExtractedDocument,
config: &crate::core::config::extraction::ImageExtractionConfig,
) {
use crate::core::config::extraction::ImageOutputFormat;
use crate::core::image_encode::re_encode;
#[cfg(not(feature = "svg"))]
if matches!(config.output_format, ImageOutputFormat::Native) {
return;
}
#[cfg(feature = "svg")]
if matches!(config.output_format, ImageOutputFormat::Native) && !config.svg.sanitize {
return;
}
let target = config.output_format;
for image in result.images.iter_mut().flatten() {
match re_encode(
image,
target,
#[cfg(feature = "svg")]
&config.svg,
) {
Ok(_) => {}
Err(warning) => {
result.processing_warnings.push(crate::types::ProcessingWarning {
source: std::borrow::Cow::Borrowed("image_encoder"),
message: std::borrow::Cow::Owned(warning.to_string()),
});
}
}
}
}
fn apply_data_base64_pass(
result: &mut ExtractedDocument,
config: &crate::core::config::extraction::ImageExtractionConfig,
) {
if !config.include_data_base64 {
return;
}
use base64::Engine as _;
for image in result.images.iter_mut().flatten() {
image.data_base64 = Some(base64::engine::general_purpose::STANDARD.encode(&image.data));
}
}
fn apply_element_transform(result: &mut ExtractedDocument, config: &ExtractionConfig) {
if config.result_format == crate::types::ResultFormat::ElementBased {
result.elements = Some(crate::extraction::transform::transform_extraction_result_to_elements(
result,
));
}
}
fn replace_embedded_image_markdown_with_ocr(doc: &mut InternalDocument) {
if !doc.ocr_text_only || doc.images.is_empty() {
return;
}
let mut image_idx = 0usize;
for elem in &mut doc.elements {
if !matches!(elem.kind, crate::types::internal::ElementKind::Paragraph) {
continue;
}
if !is_markdown_image_reference(&elem.text) {
continue;
}
if let Some(img) = doc.images.get(image_idx)
&& let Some(ocr) = &img.ocr_result
&& !ocr.content.is_empty()
{
elem.text = ocr.content.clone();
image_idx += 1;
continue;
}
image_idx += 1;
}
for table in &mut doc.tables {
for row in &mut table.cells {
for cell in row {
if !is_markdown_image_reference(cell) {
continue;
}
if let Some(img) = doc.images.get(image_idx)
&& let Some(ocr) = &img.ocr_result
&& !ocr.content.is_empty()
{
*cell = ocr.content.clone();
image_idx += 1;
continue;
}
image_idx += 1;
}
}
}
}
fn append_embedded_image_ocr_text(doc: &mut InternalDocument) {
if doc.ocr_text_only || !doc.append_ocr_text || doc.images.is_empty() {
return;
}
let mut image_idx = 0usize;
let mut new_elements = Vec::with_capacity(doc.elements.len() * 2);
for elem in &doc.elements {
new_elements.push(elem.clone());
if matches!(elem.kind, crate::types::internal::ElementKind::Paragraph)
&& is_markdown_image_reference(&elem.text)
{
if let Some(img) = doc.images.get(image_idx)
&& let Some(ocr) = &img.ocr_result
&& !ocr.content.is_empty()
{
let ocr_elem = crate::types::internal::InternalElement::text(
crate::types::internal::ElementKind::Paragraph,
ocr.content.clone(),
0,
);
new_elements.push(ocr_elem);
}
image_idx += 1;
}
}
doc.elements = new_elements;
for table in &mut doc.tables {
for row in &mut table.cells {
for cell in row {
if !is_markdown_image_reference(cell) {
continue;
}
if let Some(img) = doc.images.get(image_idx)
&& let Some(ocr) = &img.ocr_result
&& !ocr.content.is_empty()
{
*cell = format!("{}\n\n{}", cell.trim(), ocr.content);
}
image_idx += 1;
}
}
}
}
fn is_markdown_image_reference(text: &str) -> bool {
let t = text.trim();
if !t.starts_with(" else {
return false;
};
if bracket_end < 2 {
return false;
}
let after = &t[bracket_end + 2..];
after.ends_with(')')
}
fn normalize_nfc(result: &mut ExtractedDocument) {
#[cfg(feature = "quality")]
{
use unicode_normalization::UnicodeNormalization;
result.content = result.content.nfc().collect();
if let Some(pages) = result.pages.as_mut() {
for page in pages.iter_mut() {
page.content = page.content.nfc().collect();
}
}
}
let _ = result;
}