DEV Community

Alain Airom (Ayrom)
Alain Airom (Ayrom)

Posted on

Elevating Document Parsing to Warp Speed: Introducing Docling-Rust and a Native Desktop GUI

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 libpdfium for PDF rasterization). In our Rust implementation, these dependencies are safely bootstrapped at application initialization inside src/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"));
}
Enter fullscreen mode Exit fullscreen mode

  • 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 the DocumentConverter, 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();
    });
}
Enter fullscreen mode Exit fullscreen mode
  • Polling and Non-blocking Main UI Integration: Every single frame, the immediate-mode UI calls poll_worker() inside its update loop. Because it utilizes try_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;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode


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

Top comments (0)