use image::RgbImage;
use crate::layout::Region;
use crate::pdfium_backend::TextCell;
use crate::{ocr, ocr_input, page_ocr_scale, EnrichSlot, PdfError, Pipeline};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PictureText {
pub text: String,
pub provenance: &'static str,
}
impl Pipeline {
pub fn decode_picture(&self, bytes: &[u8]) -> Result<RgbImage, PdfError> {
crate::decode_image_limited(bytes)
}
pub fn classify_picture(&mut self, img: &RgbImage) -> Option<Vec<docling_core::PictureClass>> {
let mut guard = self.classifier.lock().unwrap_or_else(|p| p.into_inner());
if matches!(*guard, EnrichSlot::Unloaded) {
*guard = match crate::enrich::PictureClassifier::load_with(crate::intra_threads()) {
Some(m) => EnrichSlot::Ready(m),
None => EnrichSlot::Missing,
};
}
let EnrichSlot::Ready(model) = &mut *guard else {
return None;
};
match model.classify(img) {
Ok(classes) => Some(classes),
Err(e) => {
eprintln!("docling-pdf: picture classifier: {e}");
None
}
}
}
pub fn ocr_picture(&mut self, img: &RgbImage) -> Result<Option<PictureText>, PdfError> {
let provenance = match self.ocr_engine {
ocr::OcrEngine::PpOcr => "ppocr",
ocr::OcrEngine::Tesseract => "tesseract",
};
let (w, h) = (img.width() as f32, img.height() as f32);
if w < 1.0 || h < 1.0 {
return Ok(None);
}
let ocr_scale = page_ocr_scale(self.ocr_scale, w, h, 1.0);
let mut cache = None;
let (view, scale) = ocr_input(&mut cache, img, 1.0, ocr_scale);
let region = Region {
label: "text",
score: 1.0,
l: 0.0,
t: 0.0,
r: w,
b: h,
};
let worker = self.primary()?;
let detected = match worker.det_model() {
Some(det) => Some(det.detect(view).map_err(PdfError::Ocr)?),
None => None,
};
let Some(model) = worker.ocr_model()? else {
return Ok(None);
};
let cells = model
.ocr_page_with(
view,
std::slice::from_ref(®ion),
scale,
detected.as_deref(),
)
.map_err(PdfError::Ocr)?;
let text = lines_text(cells.into_iter().map(|(c, _)| c).collect());
Ok((!text.is_empty()).then_some(PictureText { text, provenance }))
}
}
pub(crate) fn lines_text(mut cells: Vec<TextCell>) -> String {
cells.retain(|c| !c.text.trim().is_empty());
cells.sort_by(|a, b| a.t.total_cmp(&b.t).then(a.l.total_cmp(&b.l)));
let mut rows: Vec<(f32, f32, Vec<TextCell>)> = Vec::new();
for cell in cells {
let mid = (cell.t + cell.b) / 2.0;
match rows.last_mut() {
Some((t, b, row)) if mid >= *t && mid <= *b => {
*t = t.min(cell.t);
*b = b.max(cell.b);
row.push(cell);
}
_ => rows.push((cell.t, cell.b, vec![cell])),
}
}
rows.iter_mut()
.map(|(_, _, row)| {
row.sort_by(|a, b| a.l.total_cmp(&b.l));
row.iter()
.map(|c| c.text.trim())
.collect::<Vec<_>>()
.join(" ")
})
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
fn cell(text: &str, l: f32, t: f32, r: f32, b: f32) -> TextCell {
TextCell {
text: text.into(),
l,
t,
r,
b,
}
}
#[test]
fn lines_text_reads_rows_across_then_down() {
let cells = vec![
cell("right", 200.0, 10.0, 260.0, 30.0),
cell("below", 10.0, 40.0, 80.0, 60.0),
cell("left", 10.0, 12.0, 60.0, 32.0),
cell(" ", 10.0, 70.0, 20.0, 80.0),
];
assert_eq!(lines_text(cells), "left right\nbelow");
assert_eq!(lines_text(Vec::new()), "");
}
}