use image::RgbImage;
use ort::session::Session;
use ort::value::Tensor;
use crate::layout::Region;
use crate::ocr_prep::{
batch_input, decode_row, dict_chars, prep_region_lines, width_batches, PrepLine, REC_HEIGHT,
};
use crate::pdfium_backend::TextCell;
pub struct OcrModel {
rec: Session,
chars: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OcrLang {
#[default]
En,
Ch,
}
impl OcrLang {
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"en" => Some(Self::En),
"ch" => Some(Self::Ch),
_ => None,
}
}
pub fn from_env() -> Self {
let raw = std::env::var("DOCLING_RS_OCR_LANG").unwrap_or_default();
if raw.trim().is_empty() {
return Self::default();
}
Self::parse(&raw).unwrap_or_else(|| {
eprintln!("docling-pdf: DOCLING_RS_OCR_LANG={raw:?} is not en|ch; using en");
Self::default()
})
}
}
fn resolve_rec_pair(lang: OcrLang) -> (String, String) {
const CH: (&str, &str) = ("models/ocr_rec.onnx", "models/ppocr_keys_v1.txt");
const EN: (&str, &str) = ("models/ocr_rec_en.onnx", "models/en_dict.txt");
let want_ch = lang == OcrLang::Ch;
let pick = if want_ch { CH } else { EN };
let (mut rec, mut dict) = (crate::resolve_asset(pick.0), crate::resolve_asset(pick.1));
if !want_ch && (!std::path::Path::new(&rec).exists() || !std::path::Path::new(&dict).exists()) {
let (ch_rec, ch_dict) = (crate::resolve_asset(CH.0), crate::resolve_asset(CH.1));
if std::path::Path::new(&ch_rec).exists() && std::path::Path::new(&ch_dict).exists() {
eprintln!(
"docling-pdf: English OCR model not found ({rec}); falling back to the \
multilingual ch_ model — expect weak Latin word spacing. Fetch it with \
scripts/install/download_dependencies.sh"
);
(rec, dict) = (ch_rec, ch_dict);
}
}
(
std::env::var("DOCLING_OCR_REC_ONNX").unwrap_or(rec),
std::env::var("DOCLING_OCR_DICT").unwrap_or(dict),
)
}
impl OcrModel {
pub fn load(lang: OcrLang) -> Result<Self, String> {
let (rec_path, dict_path) = resolve_rec_pair(lang);
let builder = Session::builder()
.map_err(|e| format!("ocr: builder: {e}"))?
.with_intra_threads(1)
.map_err(|e| format!("ocr: intra_threads: {e}"))?;
let rec = crate::ep::apply(builder)
.map_err(|e| format!("ocr: {e}"))?
.commit_from_file(&rec_path)
.map_err(|e| format!("ocr: load {rec_path}: {e}"))?;
let dict = std::fs::read_to_string(&dict_path)
.map_err(|e| format!("ocr: read dict {dict_path}: {e}"))?;
Ok(Self {
rec,
chars: dict_chars(&dict),
})
}
fn recognize_batch(
&mut self,
w: usize,
chunk: &[usize],
lines: &[PrepLine],
) -> Result<Vec<String>, String> {
let n = chunk.len();
let data = batch_input(w, chunk, lines);
let input = Tensor::from_array(([n, 3, REC_HEIGHT as usize, w], data))
.map_err(|e| format!("ocr: input tensor: {e}"))?;
let outputs = self
.rec
.run(ort::inputs!["x" => input])
.map_err(|e| format!("ocr: rec inference: {e}"))?;
let (shape, probs) = outputs[0]
.try_extract_tensor::<f32>()
.map_err(|e| format!("ocr: extract rec: {e}"))?;
let t_len = shape[1] as usize;
let nc = shape[2] as usize;
Ok((0..n)
.map(|i| {
decode_row(
&self.chars,
&probs[i * t_len * nc..(i + 1) * t_len * nc],
nc,
)
})
.collect())
}
pub fn ocr_page(
&mut self,
img: &RgbImage,
regions: &[Region],
scale: f32,
) -> Result<Vec<TextCell>, String> {
let (bboxes, lines) = prep_region_lines(img, regions, scale);
let mut texts = vec![String::new(); lines.len()];
for (w, chunk) in width_batches(&lines) {
for (&i, text) in chunk.iter().zip(self.recognize_batch(w, &chunk, &lines)?) {
texts[i] = text;
}
}
let mut cells = Vec::new();
for ((l, t, r, b), text) in bboxes.into_iter().zip(texts) {
let text = text.trim().to_string();
if text.is_empty() {
continue;
}
cells.push(TextCell { text, l, t, r, b });
}
Ok(cells)
}
}