df-ocr-switcher 0.1.0

Document OCR pipeline in pure Rust: scanned PDF / multi-page TIFF / image → Markdown. PaddleOCR PP-OCRv6 and Tesseract 5.5 as interchangeable engines, PP-DocLayoutV3 layout for both.
Documentation
//! `DocPipeline` — orchestratore principale.
//!
//! Gestisce:
//! - Scelta engine (ppocr-rs o tesseract-engine)
//! - Caricamento LayoutAnalyzer (PP-DocLayoutV3, condiviso tra pagine)
//! - Dispatch per tipo di input: singola immagine, TIFF multi-pagina
//! - Generazione output: Markdown o JSON raw

use std::path::Path;

use image::RgbImage;
use ppocr_rs::LayoutAnalyzer;

use crate::engine::{OcrEngine, OcrPageResult};
use crate::error::{Error, Result};
use crate::input::{load_image, load_tiff_pages};
use crate::output::to_markdown;

/// Output format richiesto.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
    #[default]
    Markdown,
    Json,
    // SearchablePdf,  // TODO feature = searchable-pdf
}

/// Tipo di input rilevato dall'estensione file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InputKind { Image, Tiff }

/// Orchestratore principale. Tiene lo stato condiviso tra pagine:
/// `LayoutAnalyzer` (sessione ONNX già caricata) e l'engine scelto.
pub struct DocPipeline {
    engine: Box<dyn OcrEngine>,
    layout: LayoutAnalyzer,
}

impl DocPipeline {
    /// Costruisce la pipeline.
    ///
    /// - `engine`: istanza già inizializzata di `PpOcrEngine` o `TesseractEngine`.
    /// - `layout_model`: path al file `PP-DocLayoutV3.onnx`.
    pub fn new(engine: Box<dyn OcrEngine>, layout_model: impl AsRef<Path>) -> Result<Self> {
        let layout_model = layout_model.as_ref();
        if !layout_model.exists() {
            return Err(Error::ModelNotFound(format!(
                "PP-DocLayoutV3.onnx non trovato: {}",
                layout_model.display()
            )));
        }
        let layout = LayoutAnalyzer::from_path(layout_model)?;
        Ok(Self { engine, layout })
    }

    /// Processa un singolo file (immagine o TIFF multi-pagina).
    /// Ritorna il testo nel formato richiesto.
    pub fn process_file(
        &mut self,
        path:   impl AsRef<Path>,
        format: OutputFormat,
    ) -> Result<String> {
        let path   = path.as_ref();
        let kind   = detect_kind(path);
        let pages  = self.load_pages(path, kind)?;
        let results = self.process_pages(pages)?;
        Ok(render(&results, format))
    }

    /// Processa un'immagine già in memoria.
    pub fn process_image(&mut self, img: &RgbImage) -> Result<OcrPageResult> {
        self.engine.process_page(img, &mut self.layout)
    }

    // ── Interni ──────────────────────────────────────────────────────────────

    fn load_pages(&self, path: &Path, kind: InputKind) -> Result<Vec<RgbImage>> {
        match kind {
            InputKind::Tiff  => load_tiff_pages(path),
            InputKind::Image => load_image(path).map(|img| vec![img]),
        }
    }

    fn process_pages(&mut self, pages: Vec<RgbImage>) -> Result<Vec<OcrPageResult>> {
        pages.iter()
            .map(|img| self.engine.process_page(img, &mut self.layout))
            .collect()
    }
}

// ─── Helper ──────────────────────────────────────────────────────────────────

fn detect_kind(path: &Path) -> InputKind {
    match path.extension().and_then(|e| e.to_str()).map(|s| s.to_ascii_lowercase()).as_deref() {
        Some("tif" | "tiff") => InputKind::Tiff,
        _                    => InputKind::Image,
    }
}

fn render(results: &[OcrPageResult], format: OutputFormat) -> String {
    match format {
        OutputFormat::Markdown => to_markdown(results),
        OutputFormat::Json     => serde_json::to_string_pretty(results)
                                    .unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}") ),
    }
}