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
//! Decodifica TIFF multi-pagina → `Vec<RgbImage>`.
//!
//! Usa il crate `tiff 0.10` che supporta I/O diretto senza dipendere da
//! libtiff di sistema. Gestisce Gray8, RGB8, RGBA8; scala U16 a U8.

use std::io::BufReader;
use std::path::Path;

use image::{DynamicImage, RgbImage};
use tiff::ColorType as TiffColorType;
use tiff::decoder::{Decoder, DecodingResult};

use crate::error::{Error, Result};

/// Carica tutte le pagine di un TIFF multi-pagina come `Vec<RgbImage>`.
/// Restituisce almeno una pagina o `Err` se il file non è leggibile.
pub fn load_tiff_pages(path: impl AsRef<Path>) -> Result<Vec<RgbImage>> {
    let path = path.as_ref();
    let file  = BufReader::new(std::fs::File::open(path)?);
    let mut dec = Decoder::new(file)
        .map_err(|e| Error::Tiff(format!("init decoder: {e}")))?;

    let mut pages = Vec::new();
    loop {
        let page = decode_current(&mut dec, pages.len())?;
        pages.push(page);
        if dec.more_images() {
            dec.next_image()
                .map_err(|e| Error::Tiff(format!("next_image pag.{}: {e}", pages.len())))?;
        } else {
            break;
        }
    }
    Ok(pages)
}

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

fn decode_current<R: std::io::Read + std::io::Seek>(
    dec: &mut Decoder<R>,
    page_idx: usize,
) -> Result<RgbImage> {
    let (w, h)  = dec.dimensions()
        .map_err(|e| Error::Tiff(format!("dimensions pag.{page_idx}: {e}")))?;
    let color   = dec.colortype()
        .map_err(|e| Error::Tiff(format!("colortype pag.{page_idx}: {e}")))?;
    let buf     = dec.read_image()
        .map_err(|e| Error::Tiff(format!("read_image pag.{page_idx}: {e}")))?;

    let dyn_img: DynamicImage = match (color, buf) {
        (TiffColorType::Gray(8), DecodingResult::U8(b)) =>
            DynamicImage::ImageLuma8(
                image::ImageBuffer::from_raw(w, h, b)
                    .ok_or_else(|| tiff_err(page_idx, "Luma8 from_raw"))?),

        (TiffColorType::RGB(8), DecodingResult::U8(b)) =>
            DynamicImage::ImageRgb8(
                image::ImageBuffer::from_raw(w, h, b)
                    .ok_or_else(|| tiff_err(page_idx, "RGB8 from_raw"))?),

        (TiffColorType::RGBA(8), DecodingResult::U8(b)) =>
            DynamicImage::ImageRgba8(
                image::ImageBuffer::from_raw(w, h, b)
                    .ok_or_else(|| tiff_err(page_idx, "RGBA8 from_raw"))?),

        // TIFF 16-bit (scanners medicali, mappe): scala a 8-bit
        (TiffColorType::Gray(16), DecodingResult::U16(b)) => {
            let b8: Vec<u8> = b.iter().map(|&v| (v >> 8) as u8).collect();
            DynamicImage::ImageLuma8(
                image::ImageBuffer::from_raw(w, h, b8)
                    .ok_or_else(|| tiff_err(page_idx, "Gray16→8 from_raw"))?)
        }

        (TiffColorType::RGB(16), DecodingResult::U16(b)) => {
            let b8: Vec<u8> = b.iter().map(|&v| (v >> 8) as u8).collect();
            DynamicImage::ImageRgb8(
                image::ImageBuffer::from_raw(w, h, b8)
                    .ok_or_else(|| tiff_err(page_idx, "RGB16→8 from_raw"))?)
        }

        (ct, _) => return Err(Error::Tiff(
            format!("pag.{page_idx}: tipo colore non gestito: {ct:?}"))),
    };

    Ok(dyn_img.to_rgb8())
}

fn tiff_err(page: usize, msg: &str) -> Error {
    Error::Tiff(format!("pag.{page}: {msg} fallito"))
}