ocr 0.1.2

A pure Rust CLI OCR tool for printed text extraction
Documentation
//! Output Formatting Service
//!
//! High-level service for formatting OCR results in various formats.
//! Supports plain text, JSON, hOCR, TSV, ALTO, Box, and PDF output.

use super::{OutputFormattingError, OutputFormat};
use crate::core::text::TextResult;
use crate::core::output::{format_alto, format_box, format_hocr, format_tsv, format_markdown, format_structured_json, to_json_output};
use std::path::PathBuf;

pub struct OutputFormattingService;

impl OutputFormattingService {
    pub fn new() -> Self {
        Self
    }

    pub fn format_result(
        &self,
        result: &TextResult,
        format: OutputFormat,
    ) -> Result<String, OutputFormattingError> {
        match format {
            OutputFormat::PlainText => {
                if result.text.is_empty() {
                    Ok(String::new())
                } else {
                    Ok(result.text.clone())
                }
            }
            OutputFormat::Json => {
                let json = to_json_output(result);
                serde_json::to_string_pretty(&json)
                    .map_err(|e| OutputFormattingError::FormattingFailed(e.to_string()))
            }
            OutputFormat::Hocr => {
                format_hocr(result)
                    .map_err(|e| OutputFormattingError::FormattingFailed(e.to_string()))
            }
            OutputFormat::Tsv => {
                format_tsv(result)
                    .map_err(|e| OutputFormattingError::FormattingFailed(e.to_string()))
            }
            OutputFormat::Alto => {
                format_alto(result)
                    .map_err(|e| OutputFormattingError::FormattingFailed(e.to_string()))
            }
            OutputFormat::Box => {
                format_box(result)
                    .map_err(|e| OutputFormattingError::FormattingFailed(e.to_string()))
            }
            OutputFormat::Pdf => {
                Err(OutputFormattingError::PdfGenerationFailed(
                    "PDF generation requires image data. Use generate_pdf instead.".to_string()
                ))
            }
            OutputFormat::Markdown => {
                format_markdown(result)
                    .map_err(|e| OutputFormattingError::FormattingFailed(e.to_string()))
            }
            OutputFormat::StructuredJson => {
                format_structured_json(result)
                    .map_err(|e| OutputFormattingError::FormattingFailed(e.to_string()))
            }
        }
    }

    pub fn write_output(
        &self,
        content: &str,
        output: Option<PathBuf>,
    ) -> Result<(), OutputFormattingError> {
        match output {
            Some(path) => {
                std::fs::write(&path, content)
                    .map_err(|e| OutputFormattingError::WriteFailed(format!("Failed to write to {:?}: {}", path, e)))?;
                println!("OCR results saved to: {:?}", path);
            }
            None => {
                if !content.is_empty() {
                    print!("{}", content);
                }
            }
        }
        Ok(())
    }

    pub fn generate_pdf(
        &self,
        image_path: &std::path::Path,
        result: &TextResult,
        output: PathBuf,
    ) -> Result<(), OutputFormattingError> {
        #[cfg(not(feature = "pdf-output"))]
        {
            let _ = (image_path, result, output);
            return Err(OutputFormattingError::PdfGenerationFailed(
                "PDF output requires the `pdf-output` feature".to_string(),
            ));
        }

        #[cfg(feature = "pdf-output")]
        {
            use image::GenericImageView;

            let img_data = std::fs::read(image_path).map_err(|e| {
                OutputFormattingError::WriteFailed(format!("Failed to read image: {}", e))
            })?;

            let dynamic_img = image::load_from_memory(&img_data).map_err(|e| {
                OutputFormattingError::WriteFailed(format!("Failed to load image: {}", e))
            })?;

            let (img_width_px, img_height_px) = dynamic_img.dimensions();

            let px_to_pt = |px: f32| printpdf::Pt(px * 72.0 / 300.0);
            let page_width_pt = px_to_pt(img_width_px as f32);
            let page_height_pt = px_to_pt(img_height_px as f32);

            let mut doc = printpdf::PdfDocument::new("OCR Output");

            let rgb_img = dynamic_img.to_rgb8();
            let (w, h) = rgb_img.dimensions();
            let raw_image = printpdf::RawImage {
                pixels: printpdf::RawImageData::U8(rgb_img.into_raw()),
                width: w as usize,
                height: h as usize,
                data_format: printpdf::RawImageFormat::RGB8,
                tag: Vec::new(),
            };
            let image_id = doc.add_image(&raw_image);

            let mut ops = Vec::new();

            ops.push(printpdf::Op::UseXobject {
                id: image_id,
                transform: printpdf::XObjectTransform {
                    translate_x: Some(printpdf::Pt(0.0)),
                    translate_y: Some(printpdf::Pt(0.0)),
                    scale_x: Some((page_width_pt.0 / img_width_px as f32) as f32),
                    scale_y: Some((page_height_pt.0 / img_height_px as f32) as f32),
                    rotate: None,
                    dpi: Some(300.0),
                },
            });

            for line in &result.lines {
                for word in &line.words {
                    let bbox = &word.bounding_box;
                    let x = px_to_pt(bbox.left as f32);
                    let y = page_height_pt - px_to_pt(bbox.bottom as f32);
                    let font_size = px_to_pt((bbox.bottom - bbox.top) as f32).0.max(1.0);

                    ops.push(printpdf::Op::StartTextSection);
                    ops.push(printpdf::Op::SetFont {
                        font: printpdf::PdfFontHandle::Builtin(printpdf::BuiltinFont::Helvetica),
                        size: printpdf::Pt(font_size),
                    });
                    ops.push(printpdf::Op::SetTextCursor {
                        pos: printpdf::Point { x, y },
                    });
                    ops.push(printpdf::Op::ShowText {
                        items: vec![printpdf::TextItem::Text(word.text.clone())],
                    });
                    ops.push(printpdf::Op::EndTextSection);
                }
            }

            let page = printpdf::PdfPage::new(
                printpdf::Mm(page_width_pt.0 * 25.4 / 72.0),
                printpdf::Mm(page_height_pt.0 * 25.4 / 72.0),
                ops,
            );
            doc.pages.push(page);

            let mut warnings = Vec::new();
            let pdf_bytes = doc.save(&printpdf::PdfSaveOptions::default(), &mut warnings);
            std::fs::write(&output, pdf_bytes).map_err(|e| {
                OutputFormattingError::WriteFailed(format!("Failed to write PDF: {}", e))
            })?;

            println!("Searchable PDF saved to: {:?}", output);
            Ok(())
        }
    }

    pub fn format_from_string(&self, format: &str) -> Result<OutputFormat, OutputFormattingError> {
        OutputFormat::from_str(format)
    }
}

impl Default for OutputFormattingService {
    fn default() -> Self {
        Self::new()
    }
}