ruvector_scipix/cli/commands/
ocr.rs1use anyhow::{Context, Result};
2use clap::Args;
3use std::path::PathBuf;
4use std::time::Instant;
5use tracing::{debug, info};
6
7use crate::cli::{output, Cli, OutputFormat};
8use super::{OcrConfig, OcrResult};
9
10#[derive(Args, Debug, Clone)]
12pub struct OcrArgs {
13 #[arg(value_name = "FILE", help = "Path to the image file")]
15 pub file: PathBuf,
16
17 #[arg(
19 short = 't',
20 long,
21 default_value = "0.7",
22 help = "Minimum confidence threshold for results"
23 )]
24 pub threshold: f64,
25
26 #[arg(
28 short,
29 long,
30 value_name = "OUTPUT",
31 help = "Save output to file instead of stdout"
32 )]
33 pub output: Option<PathBuf>,
34
35 #[arg(
37 short,
38 long,
39 help = "Pretty-print JSON output (only with --format json)"
40 )]
41 pub pretty: bool,
42
43 #[arg(
45 short,
46 long,
47 help = "Include processing metadata in output"
48 )]
49 pub metadata: bool,
50
51 #[arg(
53 short = 'f',
54 long,
55 help = "Force processing even if confidence is below threshold"
56 )]
57 pub force: bool,
58}
59
60pub async fn execute(args: OcrArgs, cli: &Cli) -> Result<()> {
61 info!("Processing file: {}", args.file.display());
62
63 if !args.file.exists() {
65 anyhow::bail!("File not found: {}", args.file.display());
66 }
67
68 if !args.file.is_file() {
69 anyhow::bail!("Not a file: {}", args.file.display());
70 }
71
72 let config = load_config(cli.config.as_ref())?;
74
75 if let Some(ext) = args.file.extension() {
77 let ext_str = ext.to_string_lossy().to_lowercase();
78 if !config.supported_extensions.contains(&ext_str) {
79 anyhow::bail!(
80 "Unsupported file extension: {}. Supported: {}",
81 ext_str,
82 config.supported_extensions.join(", ")
83 );
84 }
85 } else {
86 anyhow::bail!("File has no extension");
87 }
88
89 let metadata = std::fs::metadata(&args.file)
91 .context("Failed to read file metadata")?;
92
93 if metadata.len() as usize > config.max_image_size {
94 anyhow::bail!(
95 "File too large: {} bytes (max: {} bytes)",
96 metadata.len(),
97 config.max_image_size
98 );
99 }
100
101 let start = Instant::now();
103 let result = process_file(&args.file, &config).await?;
104 let processing_time = start.elapsed();
105
106 debug!("Processing completed in {:?}", processing_time);
107
108 if result.confidence < args.threshold && !args.force {
110 anyhow::bail!(
111 "Confidence {} is below threshold {} (use --force to override)",
112 result.confidence,
113 args.threshold
114 );
115 }
116
117 let output_content = format_result(&result, &cli.format, args.pretty, args.metadata)?;
119
120 if let Some(output_path) = &args.output {
121 std::fs::write(output_path, &output_content)
122 .context("Failed to write output file")?;
123 info!("Output saved to: {}", output_path.display());
124 } else {
125 println!("{}", output_content);
126 }
127
128 if !cli.quiet {
130 output::print_ocr_summary(&result);
131 }
132
133 Ok(())
134}
135
136async fn process_file(file: &PathBuf, _config: &OcrConfig) -> Result<OcrResult> {
137 let start = Instant::now();
141
142 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
144
145 let processing_time = start.elapsed().as_millis() as u64;
146
147 Ok(OcrResult {
148 file: file.clone(),
149 text: "Sample OCR text from image".to_string(),
150 latex: Some(r"\int_0^1 x^2 \, dx = \frac{1}{3}".to_string()),
151 confidence: 0.95,
152 processing_time_ms: processing_time,
153 errors: Vec::new(),
154 })
155}
156
157fn format_result(
158 result: &OcrResult,
159 format: &OutputFormat,
160 pretty: bool,
161 include_metadata: bool,
162) -> Result<String> {
163 match format {
164 OutputFormat::Json => {
165 if include_metadata {
166 if pretty {
167 serde_json::to_string_pretty(result)
168 } else {
169 serde_json::to_string(result)
170 }
171 } else {
172 let simple = serde_json::json!({
173 "text": result.text,
174 "latex": result.latex,
175 "confidence": result.confidence,
176 });
177 if pretty {
178 serde_json::to_string_pretty(&simple)
179 } else {
180 serde_json::to_string(&simple)
181 }
182 }
183 .context("Failed to serialize to JSON")
184 }
185 OutputFormat::Text => Ok(result.text.clone()),
186 OutputFormat::Latex => {
187 Ok(result.latex.clone().unwrap_or_else(|| result.text.clone()))
188 }
189 OutputFormat::Markdown => {
190 let mut md = format!("# OCR Result\n\n{}\n", result.text);
191 if let Some(latex) = &result.latex {
192 md.push_str(&format!("\n## LaTeX\n\n```latex\n{}\n```\n", latex));
193 }
194 if include_metadata {
195 md.push_str(&format!(
196 "\n---\n\nConfidence: {:.2}%\nProcessing time: {}ms\n",
197 result.confidence * 100.0,
198 result.processing_time_ms
199 ));
200 }
201 Ok(md)
202 }
203 OutputFormat::MathMl => {
204 Ok(format!(
206 "<math xmlns=\"http://www.w3.org/1998/Math/MathML\">\n {}\n</math>",
207 result.text
208 ))
209 }
210 }
211}
212
213fn load_config(config_path: Option<&PathBuf>) -> Result<OcrConfig> {
214 if let Some(path) = config_path {
215 let content = std::fs::read_to_string(path)
216 .context("Failed to read config file")?;
217 toml::from_str(&content)
218 .context("Failed to parse config file")
219 } else {
220 Ok(OcrConfig::default())
221 }
222}