use ppocr_rs::{
DocOrientation, DocOrientationClassifier, LayoutAnalyzer,
ModelHub, OcrLite, OcrOptions, Point, PpOcrVersion, PpStructureModel,
};
use serde::Serialize;
use std::path::PathBuf;
#[derive(Serialize)]
struct CliOutput {
page_angle: u32,
page_width: u32,
page_height: u32,
layout_boxes: Vec<CliLayoutBox>,
words: Vec<CliWord>,
}
#[derive(Serialize)]
struct CliLayoutBox {
class: String,
semantic: String,
x1: u32, y1: u32, x2: u32, y2: u32,
reading_order: i32,
}
#[derive(Serialize)]
struct CliWord {
text: String,
x1: u32, y1: u32, x2: u32, y2: u32,
confidence: f32,
layout_idx: i32,
}
fn main() {
if let Err(e) = run() {
eprintln!("[ppocr-cli] {e}");
std::process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 || args[1] == "--help" || args[1] == "-h" {
print_help();
std::process::exit(0);
}
let img_path = &args[1];
let img = image::open(img_path)
.map_err(|e| format!("apertura immagine {img_path:?}: {e}"))?
.to_rgb8();
let tier = match std::env::var("PPOCR_TIER").as_deref() {
Ok("small") => PpOcrVersion::V6Small,
Ok("medium") => PpOcrVersion::V6Medium,
_ => PpOcrVersion::V6Tiny,
};
let num_threads: usize = std::env::var("PPOCR_NUM_THREADS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(4);
let hub = ModelHub::with_default_cache()?;
let ocr_p = hub.ensure(tier)?;
let ori_path = ori_model_path(&hub)?;
let lay_path = layout_model_path();
if !lay_path.exists() {
return Err(format!(
"PP-DocLayoutV3.onnx non trovato: {} \
(set PPOCR_LAYOUT_MODEL o PPOCR_MODELS_DIR)",
lay_path.display()
).into());
}
let ori_clf = DocOrientationClassifier::from_path(&ori_path)?;
let (orient, _conf) = ori_clf.classify(&img)?;
let page_angle = orient.degrees();
let upright = rotate_to_upright(img, orient);
let (out_w, out_h) = upright.dimensions();
let mut ocr = OcrLite::new();
ocr.init_models_no_angle(
ocr_p.det_onnx.to_str().unwrap(),
ocr_p.rec_onnx.to_str().unwrap(),
ocr_p.dict_txt.to_str().unwrap(),
num_threads,
)?;
let mut layout = LayoutAnalyzer::from_path(&lay_path)?;
let opts = OcrOptions {
return_word_box: true,
use_doc_orientation: false, ..OcrOptions::default()
};
let result = ocr.detect_with_layout(
&upright, &mut layout,
10, 960, 0.6, 0.3, 1.6,
false, false, opts,
)?;
let layout_boxes: Vec<CliLayoutBox> = result.layout_boxes.iter().map(|lb| {
CliLayoutBox {
class: format!("{:?}", lb.class),
semantic: format!("{:?}", lb.class.semantic()),
x1: lb.xmin(), y1: lb.ymin(),
x2: lb.xmax(), y2: lb.ymax(),
reading_order: lb.reading_order,
}
}).collect();
let mut words: Vec<CliWord> = Vec::new();
for blk in &result.blocks {
let layout_idx = blk.layout_index.map(|i| i as i32).unwrap_or(-1);
if blk.block.words.is_empty() {
let (x1, y1, x2, y2) = aabb_points(&blk.block.box_points);
words.push(CliWord {
text: blk.block.text.clone(),
x1, y1, x2, y2,
confidence: blk.block.text_score,
layout_idx,
});
} else {
for w in &blk.block.words {
let (x1, y1, x2, y2) = aabb_points(&w.box_points);
words.push(CliWord {
text: w.text.clone(),
x1, y1, x2, y2,
confidence: w.score,
layout_idx,
});
}
}
}
let output = CliOutput {
page_angle, page_width: out_w, page_height: out_h,
layout_boxes, words,
};
println!("{}", serde_json::to_string(&output)?);
Ok(())
}
fn rotate_to_upright(img: image::RgbImage, orient: DocOrientation) -> image::RgbImage {
match orient {
DocOrientation::Deg0 => img,
DocOrientation::Deg90 => image::imageops::rotate270(&img),
DocOrientation::Deg180 => image::imageops::rotate180(&img),
DocOrientation::Deg270 => image::imageops::rotate90(&img),
}
}
fn aabb_points(pts: &[Point]) -> (u32, u32, u32, u32) {
let x1 = pts.iter().map(|p| p.x).min().unwrap_or(0);
let y1 = pts.iter().map(|p| p.y).min().unwrap_or(0);
let x2 = pts.iter().map(|p| p.x).max().unwrap_or(0);
let y2 = pts.iter().map(|p| p.y).max().unwrap_or(0);
(x1, y1, x2, y2)
}
fn ori_model_path(hub: &ModelHub) -> Result<PathBuf, Box<dyn std::error::Error>> {
if let Ok(p) = std::env::var("PPOCR_ORI_MODEL") {
return Ok(PathBuf::from(p));
}
let paths = hub.ensure_single(PpStructureModel::DocOrientation)?;
Ok(paths.onnx)
}
fn layout_model_path() -> PathBuf {
if let Ok(p) = std::env::var("PPOCR_LAYOUT_MODEL") {
return PathBuf::from(p);
}
let base = std::env::var("PPOCR_MODELS_DIR")
.unwrap_or_else(|_| "models/paddleocr".to_string());
PathBuf::from(base).join("layout").join("PP-DocLayoutV3.onnx")
}
fn print_help() {
eprintln!("ppocr-cli <image_path>");
eprintln!();
eprintln!("Env vars:");
eprintln!(" ORT_DYLIB_PATH path a onnxruntime.dll (ARM64 / load-dynamic)");
eprintln!(" PPOCR_LAYOUT_MODEL path completo PP-DocLayoutV3.onnx");
eprintln!(" PPOCR_ORI_MODEL path completo orientation inference.onnx (default: ModelHub)");
eprintln!(" PPOCR_MODELS_DIR dir base modelli (default: models/paddleocr)");
eprintln!(" PPOCR_TIER tiny|small|medium (default: tiny)");
eprintln!(" PPOCR_NUM_THREADS thread inference (default: 4)");
}