Testing the brand new implementation of Docling in Rust
Introduction
In the enterprise Artificial Intelligence and Retrieval-Augmented Generation (RAG) landscape, the phrase “garbage in, garbage out” remains an absolute truth. Before an LLM can reason over corporate knowledge, documents must be ingested, cleaned, structural elements like tables and charts recognized, and the output exported into structured Markdown or JSON.
For a long time, Python has been the undisputed king of this pipeline, dominated by libraries like IBM’s powerful layout-parsing and open-sources engine, Docling. However, as operational scale increases, the deployment complexity, massive memory footprints, and raw latency of Python runtime environments present significant engineering bottlenecks.
Enter Docling-Rust (docling.rs), the official, high-performance Rust port of IBM's/ Linux Foundation’s open-source Docling document-conversion pipeline. In this post, Bob and I explore why Rust is changing the document processing landscape, dive deep into the newly unveiled docling.rs ecosystem, and unpack a production-ready, multi-threaded native desktop GUI application built entirely in Rust.
Introducing Docling-Rust (docling.rs)
Docling-Rust is the native port designed to address these deployment challenges directly. Maintained under the Docling project organization (github.com/docling-project/docling.rs), it brings the full multi-format parsing capabilities of IBM’s core document layout analysis algorithms into the Rust ecosystem.
With docling.rs, developers can process an incredible variety of enterprise formats out of the box, with absolutely no Python runtime required:
Document TypeSupported ExtensionsFixed Layout.pdfWord Processors.docx, .doc, .odt Presentations.pptx, .ppt Spreadsheets.xlsx, .xls, .csv Web & Structures .html, .htm, .mhtml, .mht, .md E-Books & Mail.epub, .eml Interchange .json (Docling Core Schema 1.10.0)
The engine extracts structural layout elements — hierarchical headings, ordered/unordered lists, complex mathematical equations, and nested data tables — and marshals them into a strongly typed DoclingDocument object model, ready to be serialized to strict spec-conformant Markdown or schema-validated JSON.
Deep Dive: Building a Multi-Threaded Desktop GUI App
To illustrate the capabilities of docling.rs, let’s analyze a production-grade, multi-threaded desktop GUI utility: Docling GUI. Built using eframe and egui (the leading immediate-mode Rust GUI framework), this application provides a sleek, drag-and-drop desktop workflow for batch document conversion.
Core Architectural Blueprint
The application follows an asynchronous worker architecture designed to keep the GUI thread rendering fluidly at 60 FPS, even when processing heavy, multi-page PDFs or multi-tab Excel files.
Technical Code Breakdown
-
Bootstrap and Environment Provisioning: Document parsers frequently require heavy underlying execution engines (such as ONNX Runtime weights for layout modeling and vision tasks, or dynamic libraries like
libpdfiumfor PDF rasterization). In our Rust implementation, these dependencies are safely bootstrapped at application initialization insidesrc/app.rs:
/// Called once at startup. If `PDFIUM_DYNAMIC_LIB_PATH` is not already set,
/// look for `.pdfium/lib` relative to the current working directory and set
/// the required env vars so that `docling-pdf` can find them.
fn bootstrap_pdf_env() {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
// Provision pdfium dynamic library paths
if env::var_os("PDFIUM_DYNAMIC_LIB_PATH").is_none() {
let pdfium_lib_dir = cwd.join(".pdfium").join("lib");
if pdfium_lib_dir.exists() {
env::set_var("PDFIUM_DYNAMIC_LIB_PATH", &pdfium_lib_dir);
log::info!("Set PDFIUM_DYNAMIC_LIB_PATH = {}", pdfium_lib_dir.display());
}
}
let models_dir = cwd.join("models");
macro_rules! set_if_missing {
($var:expr, $path:expr) => {
if env::var_os($var).is_none() {
let p = $path;
if p.exists() {
env::set_var($var, &p);
log::info!("Set {} = {}", $var, p.display());
}
}
};
}
// Map structural ONNX runtime models
set_if_missing!("DOCLING_LAYOUT_ONNX", models_dir.join("layout_heron.onnx"));
set_if_missing!("DOCLING_OCR_REC_ONNX", models_dir.join("ocr_rec.onnx"));
set_if_missing!("DOCLING_OCR_DICT", models_dir.join("ppocr_keys_v1.txt"));
set_if_missing!("DOCLING_TABLEFORMER_ENCODER", models_dir.join("tableformer").join("encoder.onnx"));
set_if_missing!("DOCLING_TABLEFORMER_DECODER", models_dir.join("tableformer").join("decoder.onnx"));
set_if_missing!("DOCLING_TABLEFORMER_BBOX", models_dir.join("tableformer").join("bbox.onnx"));
}
-
Fearless Background Conversion Worker: To ensure the immediate-mode rendering frame rate doesn’t stutter, the conversion processing is sent to a dedicated OS thread. Communication between the main GUI loop and the worker thread utilizes a thread-safe bounded
crossbeam-channel. The snippet which follows, shows how the worker consumes the selected documents, configures theDocumentConverter, parses the data, writes the output files to disk, and sends progress updates back to the UI:
fn start_conversion(&mut self, ctx: egui::Context) {
if self.picked_files.is_empty() { return; }
// Clear output logs and switch UI view to Log tab
self.reset_outputs_and_set_tab(ActiveTab::Log);
// Instantiate communication channel: Worker -> UI Thread
let (msg_tx, msg_rx) = crossbeam_channel::bounded::<WorkerMsg>(64);
self.result_rx = Some(msg_rx);
// Clone worker configurations
let files = self.picked_files.clone();
let out_dir = self.settings.output_dir.clone();
let strict = self.settings.strict_mode;
let export_md = self.settings.export_markdown;
let export_json = self.settings.export_json;
let job_state = Arc::clone(&self.job_state);
*job_state.lock().unwrap() = JobState::Running;
// Spawn native OS background thread
thread::spawn(move || {
if let Err(e) = std::fs::create_dir_all(&out_dir) {
let _ = msg_tx.send(WorkerMsg::Error(format!("Cannot create output directory: {e}")));
return;
}
let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
let (mut combined_md, mut combined_json) = (String::new(), String::new());
let (mut last_md_path, mut last_json_path) = (None, None);
for file_path in &files {
let file_name = file_path.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
let _ = msg_tx.send(WorkerMsg::Progress(format!("Converting: {}", file_name)));
// 1. Marshall the file into a Docling SourceDocument
let source = match docling::SourceDocument::from_file(file_path) {
Ok(s) => s,
Err(e) => {
let _ = msg_tx.send(WorkerMsg::Progress(format!(" ✗ Failed to load {file_name}: {e}")));
continue;
}
};
// 2. Configure converter settings (e.g., Strict Markdown Mode toggle)
let converter = docling::DocumentConverter::new().strict(strict);
// 3. Execute the core compilation/parsing engine
let result = match converter.convert(source) {
Ok(r) => r,
Err(e) => {
let _ = msg_tx.send(WorkerMsg::Progress(format!(" ✗ Conversion error for {file_name}: {e}")));
continue;
}
};
let stem = file_path.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| "document".into());
// 4. Export Markdown AST structures
if export_md {
let md = result.document.export_to_markdown();
combined_md.push_str(&format!("# {file_name}\n\n{md}\n\n---\n\n"));
let md_path = out_dir.join(format!("{stem}_{timestamp}.md"));
if std::fs::write(&md_path, &md).is_ok() {
let _ = msg_tx.send(WorkerMsg::Progress(format!(" ✓ Markdown saved: {}", md_path.display())));
last_md_path = Some(md_path);
}
}
// 5. Export JSON Document Core Schema structures
if export_json {
let json = result.document.export_to_json();
combined_json.push_str(&format!("{json}\n"));
let json_path = out_dir.join(format!("{stem}_{timestamp}.json"));
if std::fs::write(&json_path, &json).is_ok() {
let _ = msg_tx.send(WorkerMsg::Progress(format!(" ✓ JSON saved: {}", json_path.display())));
last_json_path = Some(json_path);
}
}
}
// Notify the UI that processing is complete
let _ = msg_tx.send(WorkerMsg::Done {
markdown: combined_md,
json: combined_json,
output_md_path: last_md_path,
output_json_path: last_json_path,
});
// Request an immediate frame repaint on the main loop
ctx.request_repaint();
});
}
-
Polling and Non-blocking Main UI Integration: Every single frame, the immediate-mode UI calls p
oll_worker()inside itsupdateloop. Because it utilizestry_recv(), the operation is completely non-blocking, checking for new messages and instantly updating internal state wrappers for rendering:
fn poll_worker(&mut self) {
let rx = match &self.result_rx {
Some(r) => r.clone(),
None => return,
};
// Non-blocking drain of the worker channel
while let Ok(msg) = rx.try_recv() {
match msg {
WorkerMsg::Progress(text) => {
self.progress_msg = text.clone();
self.log_lines.push(text);
}
WorkerMsg::Done { markdown, json, output_md_path, output_json_path } => {
self.markdown_output = markdown;
self.json_output = json;
self.output_md_path = output_md_path;
self.output_json_path = output_json_path;
self.log_lines.push("✅ Conversion complete.".into());
self.progress_msg.clear();
*self.job_state.lock().unwrap() = JobState::Done;
self.result_rx = None;
// Automatically route focus to the optimal layout tab
if !self.markdown_output.is_empty() {
self.active_tab = ActiveTab::Markdown;
} else if !self.json_output.is_empty() {
self.active_tab = ActiveTab::Json;
}
}
WorkerMsg::Error(err) => {
self.log_lines.push(format!("❌ Error: {err}"));
self.progress_msg = format!("Error: {err}");
*self.job_state.lock().unwrap() = JobState::Failed(err);
self.result_rx = None;
}
}
}
}
Conclusion
The official launch of Docling-Rust represents a massive leap forward for production engineering teams building RAG pipelines, data ingestion workflows, or desktop tools. By replacing heavy Python environments with a lean, blazing-fast, compiled Rust crate, you can maximize multi-core hardware density, eliminate deployment friction, and guarantee predictability at scale.
Thanks for reading 🤗
Links
- Docling Project: https://github.com/docling-project
- Docling Rust: https://github.com/docling-project/docling.rs
- Github repo for this post: https://github.com/aairom/docling-rust-project






Top comments (0)