use image::RgbImage;
use ort::session::Session;
use ort::value::Tensor;
use crate::layout::Region;
use crate::ocr_prep::{
batch_input, decode_row_scored, dict_chars, prep_region_lines, prep_table_words, 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 Some(raw) = docling_core::env::nonempty("DOCLING_RS_OCR_LANG") else {
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()
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OcrMode {
#[default]
Default,
FullPage,
LayoutRegions,
PdfAwareLayoutRegions,
}
impl OcrMode {
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"default" => Some(Self::Default),
"full_page" => Some(Self::FullPage),
"layout_regions" => Some(Self::LayoutRegions),
"pdf_aware_layout_regions" => Some(Self::PdfAwareLayoutRegions),
_ => None,
}
}
pub fn from_env() -> Self {
let Some(raw) = docling_core::env::nonempty("DOCLING_RS_OCR_MODE") else {
return Self::default();
};
Self::parse(&raw).unwrap_or_else(|| {
eprintln!(
"docling-pdf: DOCLING_RS_OCR_MODE={raw:?} is not \
default|full_page|layout_regions|pdf_aware_layout_regions; using default"
);
Self::default()
})
}
pub fn forces_full_page(self) -> bool {
matches!(self, Self::FullPage | Self::LayoutRegions)
}
}
pub fn scale_from_env() -> Option<f32> {
let raw = docling_core::env::nonempty("DOCLING_RS_OCR_SCALE")?;
match raw.parse::<f32>() {
Ok(s) if s > 0.0 && s.is_finite() => Some(s),
_ => {
eprintln!(
"docling-pdf: DOCLING_RS_OCR_SCALE={raw:?} is not a positive number; ignored"
);
None
}
}
}
pub(crate) 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);
}
}
(
docling_core::env::nonempty("DOCLING_OCR_REC_ONNX").unwrap_or(rec),
docling_core::env::nonempty("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, f32)>, 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_scored(
&self.chars,
&probs[i * t_len * nc..(i + 1) * t_len * nc],
nc,
)
})
.collect())
}
pub(crate) fn score_lines(&mut self, lines: &[PrepLine]) -> Result<(f32, usize), String> {
let mut weighted = 0.0f32;
let mut chars = 0usize;
for (w, chunk) in width_batches(lines) {
for (text, conf) in self.recognize_batch(w, &chunk, lines)? {
let n = text.trim().chars().count();
weighted += conf * n as f32;
chars += n;
}
}
Ok((weighted, chars))
}
pub fn ocr_page(
&mut self,
img: &RgbImage,
regions: &[Region],
scale: f32,
) -> Result<Vec<(TextCell, f32)>, String> {
let (bboxes, lines) = prep_region_lines(img, regions, scale);
let mut texts = vec![(String::new(), 0.0f32); 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, conf)) 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 }, conf));
}
Ok(cells)
}
pub fn ocr_table_words(
&mut self,
img: &RgbImage,
regions: &[Region],
scale: f32,
) -> Result<Vec<(TextCell, f32)>, String> {
let (bboxes, lines) = prep_table_words(img, regions, scale);
let mut texts = vec![(String::new(), 0.0f32); 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, conf)) 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 }, conf));
}
Ok(cells)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ocr_mode_ids_parse_and_map_to_forcing() {
for (id, mode, forces) in [
("default", OcrMode::Default, false),
("full_page", OcrMode::FullPage, true),
("layout_regions", OcrMode::LayoutRegions, true),
(
"pdf_aware_layout_regions",
OcrMode::PdfAwareLayoutRegions,
false,
),
] {
assert_eq!(OcrMode::parse(id), Some(mode));
assert_eq!(mode.forces_full_page(), forces, "{id}");
}
assert_eq!(OcrMode::parse(" Full_Page "), Some(OcrMode::FullPage));
assert_eq!(OcrMode::parse("easyocr"), None);
assert_eq!(OcrMode::parse(""), None);
}
}