use std::sync::mpsc::{sync_channel, Receiver};
use std::thread::JoinHandle;
use docling_core::{ImageMode, MarkdownStreamer};
use crate::converter::DocumentConverter;
use crate::error::ConversionError;
use crate::format::InputFormat;
use crate::source::SourceDocument;
const CHANNEL_DEPTH: usize = 8;
pub struct MarkdownStream {
rx: Option<Receiver<Result<String, ConversionError>>>,
handle: Option<JoinHandle<()>>,
}
impl Iterator for MarkdownStream {
type Item = Result<String, ConversionError>;
fn next(&mut self) -> Option<Self::Item> {
match self.rx.as_ref()?.recv() {
Ok(item) => Some(item),
Err(_) => {
if let Some(h) = self.handle.take() {
let _ = h.join();
}
None
}
}
}
}
impl Drop for MarkdownStream {
fn drop(&mut self) {
self.rx = None;
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
pub(crate) struct StreamSettings {
pub strict: bool,
pub no_table_former: bool,
pub no_ocr: bool,
pub force_full_page_ocr: bool,
pub enrich: docling_pdf::EnrichmentOptions,
pub page_range: Option<(usize, usize)>,
pub ocr_lang: Option<docling_pdf::OcrLang>,
pub artifacts_dir: String,
}
pub(crate) fn spawn(
converter: DocumentConverter,
source: SourceDocument,
image_mode: ImageMode,
) -> MarkdownStream {
let (tx, rx) = sync_channel::<Result<String, ConversionError>>(CHANNEL_DEPTH);
let handle = std::thread::spawn(move || match source.format {
InputFormat::Pdf => run_pdf(converter.stream_settings(), &source, image_mode, &tx),
_ => run_buffered(converter, source, image_mode, &tx),
});
MarkdownStream {
rx: Some(rx),
handle: Some(handle),
}
}
fn write_artifacts(artifacts: Vec<(String, Vec<u8>)>) -> Result<(), ConversionError> {
for (rel, bytes) in artifacts {
let path = std::path::Path::new(&rel);
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(parent)
.map_err(|e| ConversionError::Streaming(format!("creating {parent:?}: {e}")))?;
}
std::fs::write(path, bytes)
.map_err(|e| ConversionError::Streaming(format!("writing image {rel}: {e}")))?;
}
Ok(())
}
fn run_pdf(
settings: StreamSettings,
source: &SourceDocument,
image_mode: ImageMode,
tx: &std::sync::mpsc::SyncSender<Result<String, ConversionError>>,
) {
let mut streamer = MarkdownStreamer::with_artifacts(
settings.strict,
image_mode,
false,
&settings.artifacts_dir,
);
let mut pipeline = match docling_pdf::Pipeline::new().map(|p| {
p.no_table_former(settings.no_table_former)
.no_ocr(settings.no_ocr)
.force_full_page_ocr(settings.force_full_page_ocr)
.ocr_lang(settings.ocr_lang)
.enrichments(settings.enrich)
.pages(settings.page_range)
}) {
Ok(p) => p,
Err(e) => {
let _ = tx.send(Err(ConversionError::Parse(e.to_string())));
return;
}
};
let result = pipeline.convert_streaming(&source.bytes, None, &source.name, |nodes, links| {
let chunk = streamer.push(&nodes, &links);
if let Err(e) = write_artifacts(streamer.take_artifacts()) {
return Err(docling_pdf::PdfError::Pdfium(e.to_string()));
}
if !chunk.is_empty() && tx.send(Ok(chunk)).is_err() {
return Err(docling_pdf::PdfError::Pdfium(
"markdown stream consumer dropped".into(),
));
}
Ok(())
});
match result {
Ok(()) => {
let tail = streamer.finish();
if !tail.is_empty() {
let _ = tx.send(Ok(tail));
}
}
Err(e) => {
let _ = tx.send(Err(ConversionError::Parse(e.to_string())));
}
}
}
fn run_buffered(
converter: DocumentConverter,
source: SourceDocument,
image_mode: ImageMode,
tx: &std::sync::mpsc::SyncSender<Result<String, ConversionError>>,
) {
let settings = converter.stream_settings();
let doc = match converter.convert(source) {
Ok(result) => result.document,
Err(e) => {
let _ = tx.send(Err(e));
return;
}
};
let mut streamer = MarkdownStreamer::with_artifacts(
settings.strict,
image_mode,
doc.compact_tables,
&settings.artifacts_dir,
);
let chunk = streamer.push(&doc.nodes, &doc.links);
if let Err(e) = write_artifacts(streamer.take_artifacts()) {
let _ = tx.send(Err(e));
return;
}
if !chunk.is_empty() && tx.send(Ok(chunk)).is_err() {
return;
}
let tail = streamer.finish();
if !tail.is_empty() {
let _ = tx.send(Ok(tail));
}
}