#![cfg_attr(not(feature = "ml"), allow(dead_code))]
mod assemble;
mod dp_lines;
#[cfg(feature = "ml")]
pub mod enrich;
#[cfg(feature = "ml")]
pub mod ep;
pub mod layout;
#[cfg(feature = "ml")]
mod mets;
#[cfg(feature = "ml")]
mod ocr;
pub mod pdfium_backend;
mod reading_order;
#[cfg(feature = "ml")]
pub mod resample;
#[cfg(feature = "ml")]
pub mod tableformer;
pub mod textparse;
#[cfg(feature = "ml")]
mod tf_match;
pub mod timing;
#[cfg(feature = "ml")]
use std::collections::BTreeMap;
use std::fmt;
#[cfg(feature = "ml")]
use std::sync::mpsc::{sync_channel, Receiver};
#[cfg(feature = "ml")]
use std::sync::{Arc, Mutex};
use docling_core::DoclingDocument;
#[cfg(feature = "ml")]
use docling_core::Node;
#[cfg(feature = "ml")]
pub use mets::{convert_mets_gbs, convert_mets_gbs_with_options};
#[cfg(feature = "ml")]
pub use pdfium_backend::PdfDocument;
pub use pdfium_backend::{PdfPage, TextCell};
#[derive(Debug)]
pub enum PdfError {
Pdfium(String),
Layout(String),
Ocr(String),
}
impl fmt::Display for PdfError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PdfError::Pdfium(m) => write!(f, "pdf: pdfium error: {m}"),
PdfError::Layout(m) => write!(f, "pdf: {m}"),
PdfError::Ocr(m) => write!(f, "pdf: {m}"),
}
}
}
impl std::error::Error for PdfError {}
#[cfg(feature = "ml")]
impl From<pdfium_render::prelude::PdfiumError> for PdfError {
fn from(e: pdfium_render::prelude::PdfiumError) -> Self {
PdfError::Pdfium(e.to_string())
}
}
pub fn convert_text_layer(bytes: &[u8], name: &str) -> Result<DoclingDocument, PdfError> {
let mut doc = DoclingDocument::new(name);
for page in textparse::pdf_text_pages(bytes) {
let mut regions = Vec::new();
assemble::add_orphan_regions(&mut regions, &page.cells);
let table_rows = vec![None; regions.len()];
let enrich_out = vec![None; regions.len()];
let (nodes, links) = assemble::assemble_page(&page, regions, &table_rows, &enrich_out);
doc.nodes.extend(nodes);
doc.links.extend(links);
}
assemble::merge_continuations(&mut doc.nodes);
Ok(doc)
}
#[cfg(feature = "ml")]
pub(crate) fn intra_threads() -> usize {
if let Some(n) = std::env::var("DOCLING_RS_PDF_THREADS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&n| n > 0)
{
return n;
}
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
#[cfg(feature = "ml")]
pub(crate) fn fp32_forced() -> bool {
std::env::var("DOCLING_RS_FP32")
.map(|v| v != "0")
.unwrap_or(false)
}
#[cfg(feature = "ml")]
pub(crate) fn prefer_fp32() -> bool {
fp32_forced() || ep::prefers_fp32()
}
#[cfg(feature = "ml")]
pub(crate) fn resolve_asset(rel: &str) -> String {
if std::path::Path::new(rel).exists() {
return rel.to_string();
}
if let Some(dir) = std::env::current_exe()
.ok()
.and_then(|p| p.canonicalize().ok())
.and_then(|p| p.parent().map(std::path::Path::to_path_buf))
{
for base in [Some(dir.as_path()), dir.parent()].into_iter().flatten() {
let p = base.join(rel);
if p.exists() {
return p.to_string_lossy().into_owned();
}
}
}
rel.to_string()
}
#[cfg(feature = "ml")]
pub(crate) fn model_path(env: &str, fp32_default: &str, int8_default: &str) -> String {
if let Ok(p) = std::env::var(env) {
return p;
}
if !prefer_fp32() {
let p = resolve_asset(int8_default);
if std::path::Path::new(&p).exists() {
return p;
}
}
resolve_asset(fp32_default)
}
#[cfg(feature = "ml")]
pub(crate) fn decode_image_limited(bytes: &[u8]) -> Result<image::RgbImage, PdfError> {
let max_side: u32 = std::env::var("DOCLING_RS_MAX_IMAGE_PIXELS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(30_000);
decode_image_with_max_side(bytes, max_side)
}
#[cfg(feature = "ml")]
fn decode_image_with_max_side(bytes: &[u8], max_side: u32) -> Result<image::RgbImage, PdfError> {
use image::ImageReader;
use std::io::Cursor;
let mut limits = image::Limits::default();
limits.max_image_width = Some(max_side);
limits.max_image_height = Some(max_side);
limits.max_alloc = Some(256 * 1024 * 1024);
let mut reader = ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|e| PdfError::Pdfium(format!("image: {e}")))?;
reader.limits(limits);
Ok(reader
.decode()
.map_err(|e| PdfError::Pdfium(format!("image: {e}")))?
.into_rgb8())
}
#[cfg(feature = "ml")]
type PageOut = (Vec<Node>, Vec<(String, String)>);
#[cfg(feature = "ml")]
enum TfSlot {
Unloaded,
Missing,
Ready(tableformer::TableFormer),
}
#[cfg(feature = "ml")]
type SharedTables = Arc<Mutex<TfSlot>>;
#[cfg(feature = "ml")]
enum EnrichSlot<T> {
Unloaded,
Missing,
Ready(T),
}
#[cfg(feature = "ml")]
type SharedClassifier = Arc<Mutex<EnrichSlot<enrich::PictureClassifier>>>;
#[cfg(feature = "ml")]
type SharedCodeFormula = Arc<Mutex<EnrichSlot<enrich::CodeFormula>>>;
#[cfg(feature = "ml")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct EnrichmentOptions {
pub picture_classification: bool,
pub code: bool,
pub formula: bool,
}
#[cfg(feature = "ml")]
impl EnrichmentOptions {
fn any(&self) -> bool {
self.picture_classification || self.code || self.formula
}
}
#[cfg(feature = "ml")]
struct Worker {
layout: Option<layout::LayoutModel>,
ocr: Option<ocr::OcrModel>,
tables: Option<SharedTables>,
classifier: Option<SharedClassifier>,
code_formula: Option<SharedCodeFormula>,
enrich: EnrichmentOptions,
no_ocr: bool,
}
#[cfg(feature = "ml")]
impl Worker {
fn load(
intra: usize,
tables: Option<SharedTables>,
enrich_slots: (Option<SharedClassifier>, Option<SharedCodeFormula>),
enrich: EnrichmentOptions,
no_ocr: bool,
) -> Result<Self, PdfError> {
Ok(Self {
layout: if no_ocr {
None
} else {
Some(layout::LayoutModel::load_with(intra).map_err(PdfError::Layout)?)
},
ocr: None,
tables,
classifier: enrich_slots.0,
code_formula: enrich_slots.1,
enrich,
no_ocr,
})
}
fn process(&mut self, n: usize, page: &mut PdfPage) -> Result<PageOut, PdfError> {
if self.no_ocr {
let mut regions = Vec::new();
assemble::add_orphan_regions(&mut regions, &page.cells);
let table_rows = vec![None; regions.len()];
let enrich_out = vec![None; regions.len()];
return Ok(timing::timed("assemble_page", || {
assemble::assemble_page(page, regions, &table_rows, &enrich_out)
}));
}
let regions = timing::timed("layout.predict", || {
self.layout
.as_mut()
.expect("layout model loaded unless no_ocr")
.predict(&page.image, page.width, page.height)
})
.map_err(|e| PdfError::Layout(format!("page {}: {e}", n + 1)))?;
self.finish_page(n, page, regions)
}
fn process_batch(&mut self, items: &mut [(usize, PdfPage)]) -> Vec<Result<PageOut, PdfError>> {
if self.no_ocr {
return items
.iter_mut()
.map(|(n, page)| {
let n = *n;
self.process(n, page)
})
.collect();
}
let inputs: Vec<(&image::RgbImage, f32, f32)> = items
.iter()
.map(|(_, page)| (&page.image, page.width, page.height))
.collect();
let batched = timing::timed("layout.predict", || {
self.layout
.as_mut()
.expect("layout model loaded unless no_ocr")
.predict_batch(&inputs)
});
match batched {
Ok(all) => items
.iter_mut()
.zip(all)
.map(|((n, page), regions)| self.finish_page(*n, page, regions))
.collect(),
Err(e) => items
.iter()
.map(|(n, _)| Err(PdfError::Layout(format!("page {}: {e}", n + 1))))
.collect(),
}
}
fn finish_page(
&mut self,
n: usize,
page: &mut PdfPage,
regions: Vec<layout::Region>,
) -> Result<PageOut, PdfError> {
let mut regions = regions;
regions.retain(|r| r.score >= layout::label_threshold(r.label));
let mut regions = assemble::resolve(regions);
assemble::add_orphan_regions(&mut regions, &page.cells);
assemble::drop_false_pictures(&mut regions, &page.cells, page.width, page.height);
assemble::drop_contained_regulars(&mut regions);
if page.cells.is_empty() {
if self.ocr.is_none() {
self.ocr = Some(ocr::OcrModel::load().map_err(PdfError::Ocr)?);
}
let cells = timing::timed("ocr.page", || {
self.ocr
.as_mut()
.unwrap()
.ocr_page(&page.image, ®ions, page.scale)
})
.map_err(|e| PdfError::Ocr(format!("page {}: {e}", n + 1)))?;
page.cells = cells;
}
let mut table_rows: Vec<Option<Vec<Vec<String>>>> = vec![None; regions.len()];
if let Some(slot) = self.tables.as_ref() {
if regions.iter().any(|r| assemble::is_table_like(r.label)) {
timing::timed("tableformer", || {
let mut guard = slot.lock().unwrap();
if matches!(*guard, TfSlot::Unloaded) {
*guard = match tableformer::TableFormer::load_with(intra_threads()) {
Some(tf) => TfSlot::Ready(tf),
None => TfSlot::Missing,
};
}
if let TfSlot::Ready(tf) = &mut *guard {
for (i, r) in regions.iter().enumerate() {
if assemble::is_table_like(r.label) {
table_rows[i] = tf.predict_table_rows(
&page.image,
[r.l, r.t, r.r, r.b],
&page.word_cells,
);
}
}
}
});
}
}
let mut enrich_out: Vec<Option<assemble::Enrichment>> = vec![None; regions.len()];
if let Some(slot) = self.classifier.as_ref() {
if regions.iter().any(|r| r.label == "picture") {
timing::timed("picture_classifier", || {
let mut guard = slot.lock().unwrap();
if matches!(*guard, EnrichSlot::Unloaded) {
*guard = match enrich::PictureClassifier::load_with(intra_threads()) {
Some(m) => EnrichSlot::Ready(m),
None => EnrichSlot::Missing,
};
}
if let EnrichSlot::Ready(model) = &mut *guard {
for (i, r) in regions.iter().enumerate() {
if r.label != "picture" {
continue;
}
let Some(crop) = assemble::crop_region_scaled(
page,
[r.l, r.t, r.r, r.b],
enrich::CLASSIFIER_SCALE,
) else {
continue;
};
match model.classify(&crop) {
Ok(classes) => {
enrich_out[i] =
Some(assemble::Enrichment::PictureClasses(classes));
}
Err(e) => eprintln!("docling-pdf: page {}: {e}", n + 1),
}
}
}
});
}
}
if let Some(slot) = self.code_formula.as_ref() {
let wants = |label: &str| {
(label == "code" && self.enrich.code) || (label == "formula" && self.enrich.formula)
};
if regions.iter().any(|r| wants(r.label)) {
timing::timed("code_formula", || {
let mut guard = slot.lock().unwrap();
if matches!(*guard, EnrichSlot::Unloaded) {
*guard = match enrich::CodeFormula::load_with(intra_threads()) {
Some(m) => EnrichSlot::Ready(m),
None => EnrichSlot::Missing,
};
}
if let EnrichSlot::Ready(model) = &mut *guard {
for (i, r) in regions.iter().enumerate() {
if !wants(r.label) {
continue;
}
let [bl, bt, br, bb] = assemble::region_cell_bbox(r, &page.cells)
.unwrap_or([r.l, r.t, r.r, r.b]);
let (w, h) = (br - bl, bb - bt);
let ex = enrich::CODE_FORMULA_EXPANSION;
let bbox = [bl - w * ex, bt - h * ex, br + w * ex, bb + h * ex];
let Some(crop) = assemble::crop_region_scaled(
page,
bbox,
enrich::CODE_FORMULA_SCALE,
) else {
continue;
};
let kind = if r.label == "code" {
enrich::CodeFormulaKind::Code
} else {
enrich::CodeFormulaKind::Formula
};
match model.predict(&crop, kind) {
Ok(text) => {
enrich_out[i] = Some(match kind {
enrich::CodeFormulaKind::Code => {
let (code, language) =
enrich::extract_code_language(&text);
assemble::Enrichment::Code {
language,
text: code,
}
}
enrich::CodeFormulaKind::Formula => {
assemble::Enrichment::Formula { latex: text }
}
});
}
Err(e) => eprintln!("docling-pdf: page {}: {e}", n + 1),
}
}
}
});
}
}
Ok(timing::timed("assemble_page", || {
assemble::assemble_page(page, regions, &table_rows, &enrich_out)
}))
}
}
#[cfg(feature = "ml")]
fn pdf_intra() -> usize {
if let Some(n) = std::env::var("DOCLING_RS_PDF_INTRA")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&n| n > 0)
{
return n;
}
if intra_threads() >= 2 {
2
} else {
1
}
}
#[cfg(feature = "ml")]
fn pdf_worker_count() -> usize {
if let Some(n) = std::env::var("DOCLING_RS_PDF_WORKERS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&n| n > 0)
{
return n;
}
(intra_threads() / pdf_intra()).clamp(1, 4)
}
#[cfg(feature = "ml")]
fn pdf_layout_batch() -> usize {
std::env::var("DOCLING_RS_PDF_LAYOUT_BATCH")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or_else(|| if intra_threads() >= 8 { 4 } else { 1 })
}
#[cfg(feature = "ml")]
fn pdf_parallel_min() -> usize {
std::env::var("DOCLING_RS_PDF_PARALLEL_MIN")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or(6)
}
#[cfg(feature = "ml")]
pub struct Pipeline {
primary: Option<Worker>,
pool: Vec<Worker>,
tables: SharedTables,
classifier: SharedClassifier,
code_formula: SharedCodeFormula,
target_workers: usize,
parallel_min: usize,
no_table_former: bool,
no_ocr: bool,
enrich: EnrichmentOptions,
}
#[cfg(feature = "ml")]
impl Pipeline {
pub fn new() -> Result<Self, PdfError> {
Ok(Self {
primary: None,
pool: Vec::new(),
tables: Arc::new(Mutex::new(TfSlot::Unloaded)),
classifier: Arc::new(Mutex::new(EnrichSlot::Unloaded)),
code_formula: Arc::new(Mutex::new(EnrichSlot::Unloaded)),
target_workers: pdf_worker_count(),
parallel_min: pdf_parallel_min(),
no_table_former: false,
no_ocr: false,
enrich: EnrichmentOptions::default(),
})
}
pub fn enrichments(mut self, opts: EnrichmentOptions) -> Self {
self.enrich = opts;
self
}
pub fn no_table_former(mut self, disable: bool) -> Self {
self.no_table_former = disable;
self
}
pub fn no_ocr(mut self, disable: bool) -> Self {
self.no_ocr = disable;
self
}
fn tables_slot(&self) -> Option<SharedTables> {
if self.no_table_former || self.no_ocr {
None
} else {
Some(Arc::clone(&self.tables))
}
}
fn enrich_slots(&self) -> (Option<SharedClassifier>, Option<SharedCodeFormula>) {
if self.no_ocr || !self.enrich.any() {
return (None, None);
}
(
self.enrich
.picture_classification
.then(|| Arc::clone(&self.classifier)),
(self.enrich.code || self.enrich.formula).then(|| Arc::clone(&self.code_formula)),
)
}
pub fn warm_up(&mut self) -> Result<(), PdfError> {
self.primary()?;
Ok(())
}
fn primary(&mut self) -> Result<&mut Worker, PdfError> {
if self.primary.is_none() {
self.primary = Some(Worker::load(
intra_threads(),
self.tables_slot(),
self.enrich_slots(),
self.enrich,
self.no_ocr,
)?);
}
Ok(self.primary.as_mut().unwrap())
}
pub fn convert(
&mut self,
bytes: &[u8],
password: Option<&str>,
name: &str,
) -> Result<DoclingDocument, PdfError> {
let pages = pdfium_backend::page_count(bytes, password)?;
let doc = if self.target_workers >= 2 && pages >= self.parallel_min {
self.convert_parallel(bytes, password, name)?
} else {
self.convert_serial(bytes, password, name)?
};
timing::report();
Ok(doc)
}
fn convert_serial(
&mut self,
bytes: &[u8],
password: Option<&str>,
name: &str,
) -> Result<DoclingDocument, PdfError> {
let mut doc = DoclingDocument::new(name);
let render_image = !self.no_ocr;
let worker = self.primary()?;
pdfium_backend::for_each_page(bytes, password, render_image, |n, _total, mut page| {
let (nodes, links) = worker.process(n, &mut page)?;
doc.nodes.extend(nodes);
doc.links.extend(links);
Ok::<(), PdfError>(())
})?;
assemble::merge_continuations(&mut doc.nodes);
Ok(doc)
}
fn convert_parallel(
&mut self,
bytes: &[u8],
password: Option<&str>,
name: &str,
) -> Result<DoclingDocument, PdfError> {
self.ensure_pool()?;
let n_workers = self.pool.len();
let render_image = !self.no_ocr;
let layout_batch = pdf_layout_batch();
let (work_tx, work_rx) = sync_channel::<(usize, PdfPage)>(n_workers * layout_batch.max(2));
let work_rx: Arc<Mutex<Receiver<(usize, PdfPage)>>> = Arc::new(Mutex::new(work_rx));
let results: Arc<Mutex<Vec<(usize, PageOut)>>> = Arc::new(Mutex::new(Vec::new()));
let first_err: Arc<Mutex<Option<PdfError>>> = Arc::new(Mutex::new(None));
let mut workers = std::mem::take(&mut self.pool);
std::thread::scope(|s| {
for worker in workers.iter_mut() {
let work_rx = Arc::clone(&work_rx);
let results = Arc::clone(&results);
let first_err = Arc::clone(&first_err);
s.spawn(move || loop {
let mut batch = Vec::new();
{
let rx = work_rx.lock().unwrap();
match rx.recv() {
Ok(item) => {
batch.push(item);
while batch.len() < layout_batch {
match rx.try_recv() {
Ok(item) => batch.push(item),
Err(_) => break,
}
}
}
Err(_) => break,
}
}
let outs = worker.process_batch(&mut batch);
for ((idx, _), out) in batch.iter().zip(outs) {
match out {
Ok(out) => results.lock().unwrap().push((*idx, out)),
Err(e) => {
let mut slot = first_err.lock().unwrap();
if slot.is_none() {
*slot = Some(e);
}
}
}
}
});
}
let render =
pdfium_backend::for_each_page(bytes, password, render_image, |i, _total, page| {
work_tx
.send((i, page))
.map_err(|_| PdfError::Pdfium("page-worker channel closed".into()))
});
drop(work_tx);
if let Err(e) = render {
let mut slot = first_err.lock().unwrap();
if slot.is_none() {
*slot = Some(e);
}
}
});
self.pool = workers;
if let Some(e) = first_err.lock().unwrap().take() {
return Err(e);
}
let mut results = Arc::try_unwrap(results)
.unwrap_or_else(|arc| Mutex::new(arc.lock().unwrap().clone()))
.into_inner()
.unwrap();
results.sort_by_key(|(idx, _)| *idx);
let mut doc = DoclingDocument::new(name);
for (_, (nodes, links)) in results {
doc.nodes.extend(nodes);
doc.links.extend(links);
}
assemble::merge_continuations(&mut doc.nodes);
Ok(doc)
}
pub fn convert_streaming<F>(
&mut self,
bytes: &[u8],
password: Option<&str>,
name: &str,
emit: F,
) -> Result<(), PdfError>
where
F: FnMut(Vec<Node>, Vec<(String, String)>) -> Result<(), PdfError>,
{
let _ = name; let pages = pdfium_backend::page_count(bytes, password)?;
let r = if self.target_workers >= 2 && pages >= self.parallel_min {
self.convert_streaming_parallel(bytes, password, emit)
} else {
self.convert_streaming_serial(bytes, password, emit)
};
timing::report();
r
}
fn convert_streaming_serial<F>(
&mut self,
bytes: &[u8],
password: Option<&str>,
mut emit: F,
) -> Result<(), PdfError>
where
F: FnMut(Vec<Node>, Vec<(String, String)>) -> Result<(), PdfError>,
{
let mut asm = assemble::StreamAssembler::new();
let render_image = !self.no_ocr;
let worker = self.primary()?;
pdfium_backend::for_each_page(bytes, password, render_image, |n, _total, mut page| {
let (nodes, links) = worker.process(n, &mut page)?;
emit(asm.push(nodes), links)
})?;
emit(asm.finish(), Vec::new())
}
fn convert_streaming_parallel<F>(
&mut self,
bytes: &[u8],
password: Option<&str>,
mut emit: F,
) -> Result<(), PdfError>
where
F: FnMut(Vec<Node>, Vec<(String, String)>) -> Result<(), PdfError>,
{
self.ensure_pool()?;
let n_workers = self.pool.len();
let render_image = !self.no_ocr;
let layout_batch = pdf_layout_batch();
let (work_tx, work_rx) = sync_channel::<(usize, PdfPage)>(n_workers * layout_batch.max(2));
let work_rx: Arc<Mutex<Receiver<(usize, PdfPage)>>> = Arc::new(Mutex::new(work_rx));
let (res_tx, res_rx) = sync_channel::<Result<(usize, PageOut), PdfError>>(n_workers * 2);
let mut workers = std::mem::take(&mut self.pool);
let mut asm = assemble::StreamAssembler::new();
let mut first_err: Option<PdfError> = None;
std::thread::scope(|s| {
for worker in workers.iter_mut() {
let work_rx = Arc::clone(&work_rx);
let res_tx = res_tx.clone();
s.spawn(move || 'outer: loop {
let mut batch = Vec::new();
{
let rx = work_rx.lock().unwrap();
match rx.recv() {
Ok(item) => {
batch.push(item);
while batch.len() < layout_batch {
match rx.try_recv() {
Ok(item) => batch.push(item),
Err(_) => break,
}
}
}
Err(_) => break,
}
}
let outs = worker.process_batch(&mut batch);
for ((idx, _), out) in batch.iter().zip(outs) {
if res_tx.send(out.map(|o| (*idx, o))).is_err() {
break 'outer; }
}
});
}
{
let res_tx = res_tx.clone();
s.spawn(move || {
let render = pdfium_backend::for_each_page(
bytes,
password,
render_image,
|i, _total, page| {
work_tx
.send((i, page))
.map_err(|_| PdfError::Pdfium("page-worker channel closed".into()))
},
);
drop(work_tx); if let Err(e) = render {
let _ = res_tx.send(Err(e));
}
});
}
drop(res_tx);
let mut buffer: BTreeMap<usize, PageOut> = BTreeMap::new();
let mut next = 0usize;
for msg in res_rx.iter() {
match msg {
Err(e) => {
if first_err.is_none() {
first_err = Some(e);
}
}
Ok((idx, out)) => {
buffer.insert(idx, out);
if first_err.is_some() {
continue; }
while let Some((nodes, links)) = buffer.remove(&next) {
if let Err(e) = emit(asm.push(nodes), links) {
first_err = Some(e);
break;
}
next += 1;
}
}
}
}
});
self.pool = workers;
if let Some(e) = first_err {
return Err(e);
}
emit(asm.finish(), Vec::new())
}
fn ensure_pool(&mut self) -> Result<(), PdfError> {
let need = self.target_workers.saturating_sub(self.pool.len());
if need == 0 {
return Ok(());
}
let intra = pdf_intra();
let no_ocr = self.no_ocr;
let enrich = self.enrich;
let tables = self.tables_slot();
let enrich_slots = self.enrich_slots();
let loaded: Vec<Result<Worker, PdfError>> = std::thread::scope(|s| {
let handles: Vec<_> = (0..need)
.map(|_| {
let tables = tables.clone();
let enrich_slots = enrich_slots.clone();
s.spawn(move || Worker::load(intra, tables, enrich_slots, enrich, no_ocr))
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
for w in loaded {
self.pool.push(w?);
}
Ok(())
}
pub fn convert_image(&mut self, bytes: &[u8], name: &str) -> Result<DoclingDocument, PdfError> {
let image = decode_image_limited(bytes)?;
let (w, h) = image.dimensions();
let page = PdfPage {
width: w as f32,
height: h as f32,
scale: 1.0,
cells: Vec::new(),
code_cells: Vec::new(),
word_cells: Vec::new(),
image,
links: Vec::new(),
};
self.process_pages(vec![page], name)
}
fn process_pages(
&mut self,
mut pages: Vec<PdfPage>,
name: &str,
) -> Result<DoclingDocument, PdfError> {
let mut doc = DoclingDocument::new(name);
let worker = self.primary()?;
for (n, page) in pages.iter_mut().enumerate() {
let (nodes, links) = worker.process(n, page)?;
doc.nodes.extend(nodes);
doc.links.extend(links);
}
assemble::merge_continuations(&mut doc.nodes);
Ok(doc)
}
}
#[cfg(feature = "ml")]
pub fn convert(
bytes: &[u8],
password: Option<&str>,
name: &str,
) -> Result<DoclingDocument, PdfError> {
convert_with_options(
bytes,
password,
name,
false,
false,
EnrichmentOptions::default(),
)
}
#[cfg(feature = "ml")]
pub fn convert_with_options(
bytes: &[u8],
password: Option<&str>,
name: &str,
no_table_former: bool,
no_ocr: bool,
enrich: EnrichmentOptions,
) -> Result<DoclingDocument, PdfError> {
Pipeline::new()?
.no_table_former(no_table_former)
.no_ocr(no_ocr)
.enrichments(enrich)
.convert(bytes, password, name)
}
#[cfg(feature = "ml")]
pub fn convert_image(bytes: &[u8], name: &str) -> Result<DoclingDocument, PdfError> {
convert_image_with_options(bytes, name, false, false, EnrichmentOptions::default())
}
#[cfg(feature = "ml")]
pub fn convert_image_with_options(
bytes: &[u8],
name: &str,
no_table_former: bool,
no_ocr: bool,
enrich: EnrichmentOptions,
) -> Result<DoclingDocument, PdfError> {
Pipeline::new()?
.no_table_former(no_table_former)
.no_ocr(no_ocr)
.enrichments(enrich)
.convert_image(bytes, name)
}
#[cfg(feature = "ml")]
pub fn convert_pages(pages: Vec<PdfPage>, name: &str) -> Result<DoclingDocument, PdfError> {
convert_pages_with_options(pages, name, false, false, EnrichmentOptions::default())
}
#[cfg(feature = "ml")]
pub fn convert_pages_with_options(
pages: Vec<PdfPage>,
name: &str,
no_table_former: bool,
no_ocr: bool,
enrich: EnrichmentOptions,
) -> Result<DoclingDocument, PdfError> {
Pipeline::new()?
.no_table_former(no_table_former)
.no_ocr(no_ocr)
.enrichments(enrich)
.process_pages(pages, name)
}
#[cfg(feature = "ml")]
#[cfg(all(test, feature = "ml"))]
mod image_limit_tests {
use super::decode_image_with_max_side;
fn png_bytes(w: u32, h: u32) -> Vec<u8> {
use std::io::Cursor;
let img = image::RgbImage::new(w, h);
let mut out = Vec::new();
img.write_to(&mut Cursor::new(&mut out), image::ImageFormat::Png)
.unwrap();
out
}
#[test]
fn normal_image_decodes_under_the_cap() {
let img = decode_image_with_max_side(&png_bytes(8, 8), 30_000).expect("8x8 decodes");
assert_eq!(img.dimensions(), (8, 8));
}
#[test]
fn dimensions_over_the_cap_are_rejected_not_aborted() {
let r = decode_image_with_max_side(&png_bytes(8, 8), 4);
assert!(
r.is_err(),
"decode must fail under the pixel cap, not abort"
);
}
}
#[cfg(test)]
mod median_tests {
#[test]
fn median_of_empty_is_zero_not_a_panic() {
assert_eq!(super::tf_match::median_for_test(&mut []), 0.0);
assert_eq!(super::tf_match::median_for_test(&mut [4.0, 2.0]), 3.0);
assert_eq!(super::tf_match::median_for_test(&mut [5.0, 1.0, 3.0]), 3.0);
}
}
#[cfg(test)]
mod send_check {
fn assert_send<T: Send>() {}
#[test]
fn pipeline_is_send() {
assert_send::<super::Pipeline>();
}
}