Scanbot SDK has been acquired by Apryse! Learn more

Learn more
Skip to content

Building a Rust-based Web Document Scanner with Dioxus

Kevin August 28, 2025 14 mins read
Web Document Scanner SDK

Dioxus is a Rust-based UI framework for building fast, cross-platform apps targeting Web, Desktop, and Mobile, using a React-like component model while leveraging Rust’s safety and performance. It aims to provide Rust developers with a productive UI development experience with hot reload, live updates, and seamless state management, filling the gap in the Rust ecosystem for building user interfaces.

In this tutorial, we’ll create a web app for scanning documents and exporting them as PDFs using the Dioxus framework and a combination of Rust and JavaScript. To implement the scanning functionalities, we’ll use the Scanbot Web Document Scanner SDK.

Scanning a document and exporting it as a PDF with our Dioxus document scanner app

To achieve this, we’ll follow these steps:

  1. Creating our Dioxus project
  2. Adding the Scanbot SDK package to the assets directory
  3. Implementing the JavaScript integration layer
  4. Binding the JavaScript functions in Rust
  5. Loading the SDK dynamically and initializing it
  6. Adding the UI for scanning documents and exporting them as PDFs
  7. Running your Dioxus app and scanning a document
Want to see the final code right away? Click here.

main.rs:

use dioxus::prelude::*;
use js_sys::Promise;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen_futures::JsFuture;

fn main() {
    dioxus::launch(App);
}

#[component]
fn App() -> Element {
    const SCANBOT_SDK_JS: Asset = asset!("assets/scanbot-web-sdk/bundle/ScanbotSDK.min.js");
    const SCANBOT_UI2_JS: Asset = asset!("assets/scanbot-web-sdk/bundle/ScanbotSDK.ui2.min.js");
    let mut sdk_bundles_loaded = use_signal(|| 0);
    let mut sdk_ready = use_signal(|| false);
    use_effect(move || {
        if sdk_bundles_loaded() == 2 && !sdk_ready() {
            init_scanbot_sdk("");
            sdk_ready.set(true);
        }
    });

    rsx! {
        script { src: SCANBOT_SDK_JS, onload: move |_| { sdk_bundles_loaded += 1 } }
        script { src: SCANBOT_UI2_JS, onload: move |_| { sdk_bundles_loaded += 1 } }

        if sdk_ready() {
            Demo {}
        }
    }
}

#[wasm_bindgen(module = "/assets/demo.js")]
extern "C" {
    #[wasm_bindgen(js_name = initScanbotSDK)]
    fn init_scanbot_sdk(license_key: &str);

    #[wasm_bindgen(js_name = startDocumentScan)]
    fn start_document_scan() -> Promise;

    #[wasm_bindgen(js_name = exportPDF)]
    fn export_pdf() -> Promise;
}

#[component]
fn Demo() -> Element {
    let result = use_signal(|| String::new());
    let on_document_scan = move |_| {
        spawn({
            let mut result = result.clone();
            async move {
                match JsFuture::from(start_document_scan()).await {
                    Ok(_) => {}
                    Err(_) => {
                        result.set("Error during document scan".into());
                    }
                }
            }
        });
    };

    let on_pdf_export = move |_| {
        spawn({
            let mut result = result.clone();
            async move {
                match JsFuture::from(export_pdf()).await {
                    Ok(_) => {}
                    Err(_) => {
                        result.set("Error during PDF export".into());
                    }
                }
            }
        });
    };

    rsx! {
        div {
            style: "display: flex; flex-direction: column; align-items: center; gap: 1rem; padding: 1rem; max-width: 600px; margin: auto;",

            button {
                style: "background: #c8193c; color: white; border: none; padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 6px; cursor: pointer;",
                onclick: on_document_scan,
                "Scan Document"
            }

            button {
                style: "background: #c8193c; color: white; border: none; padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 6px; cursor: pointer;",
                onclick: on_pdf_export,
                "Export PDF"
            }

            if !result().is_empty() {
                pre {
                    style: "background: #1a1a1a; color: #00ff88; font-family: monospace; padding: 1rem; border-radius: 6px; width: 100%; max-width: 600px; max-height: 300px; overflow-y: auto; white-space: pre-wrap; word-break: break-word;",
                    "{result()}"
                }
            }
        }
    }
}

#[component]
pub fn Hero() -> Element {
    rsx! {}
}

Prerequisites

Step 1: Create your Dioxus project

Open your terminal and initialize a new project.

dx new dioxus_document_scanner

You will be presented with several options for setting up the project. For this tutorial, you can answer as follows:

  • Which sub-template should be expanded? –> Bare-Bones
  • Do you want to use Dioxus Fullstack? –> false
  • Do you want to use Dioxus Router? –> false
  • Do you want to use Tailwind CSS? –> false
  • Which platform do you want DX to serve by default? –> Web

Afterwards, navigate into your project directory.

cd dioxus_document_scanner

Step 2: Add the Scanbot SDK package to the assets directory

Next, you need to manually add the Scanbot Web SDK to your project, for which you can use the following commands:

# Download the SDK package as a tarball
npm pack scanbot-web-sdk

# Extract the tarball
tar -xf scanbot-web-sdk-*.tgz

# Move the package to the assets folder
mv package assets/scanbot-web-sdk

# Remove the tarball, as it is no longer needed
rm scanbot-web-sdk-*.tgz

💡 We use Scanbot SDK version 7.2.0 in this tutorial. You can find the latest version in the changelog.

Now configure Dioxus.toml to ensure that the assets directory (including the SDK) is included in your build and served correctly.

[application]
asset_dir = "assets"

This configuration ensures that all subfolders within assets/, including the SDK, are automatically bundled and served with your application during development and in production builds.

💡 For quick prototyping, you can also reference the SDK directly via a CDN instead of downloading and hosting the Scanbot Web SDK manually.

const SCANBOT_SDK_JS: Asset = asset!("https://cdn.jsdelivr.net/npm/scanbot-web-sdk@latest/bundle/ScanbotSDK.min.js");
const SCANBOT_UI2_JS: Asset = asset!("https://cdn.jsdelivr.net/npm/scanbot-web-sdk@latest/bundle/ScanbotSDK.ui2.min.js");
export async function initScanbotSDK(licenseKey) {
    await window.ScanbotSDK.initialize({
        licenseKey,
        enginePath: "https://cdn.jsdelivr.net/npm/scanbot-web-sdk@latest/bundle/bin/complete/",
    });
}

However, we advise against using jsDelivr or other CDNs in production due to reliability, security, and licensing concerns.

Add the required Rust dependencies

To enable asynchronous JavaScript interop within your Dioxus application, add the following dependencies to Cargo.toml:

wasm-bindgen = "0.2.100"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
  • wasm-bindgen enables Rust to call JavaScript functions and handle value conversions across the WebAssembly boundary.
  • wasm-bindgen-futures provides seamless async / await support, allowing Rust to work with JavaScript Promise objects natively.
  • js-sys supplies bindings for standard JavaScript types (such as Promise) for direct interoperation.

Step 3: Implement the JavaScript integration layer

To bridge the Scanbot Web SDK with your Dioxus application, create a new file assets/demo.js with the following code containing the core functionality of our app:

let scanbotSDKInstance = null;
let scanResult = null;

/**
 * Initialize the Scanbot SDK with your license key.
 */
export async function initScanbotSDK(licenseKey) {
    if (scanbotSDKInstance) return;

    scanbotSDKInstance = await window.ScanbotSDK.initialize({
        licenseKey,
        enginePath: "/assets/scanbot-web-sdk/bundle/bin/complete/",
    });
}

/**
 * Start the document scanning process.
 */
export async function startDocumentScan() {
    try {
        scanResult = await window.ScanbotSDK.UI.createDocumentScanner(
            new window.ScanbotSDK.UI.Config.DocumentScanningFlow()
        );
    } catch (error) {
        console.error("Error during document scan:", error);
        return Promise.reject(error);
    }
}

/**
 * Export the scanned document as a PDF.
 */
export async function exportPDF() {
    try {
        const pages = scanResult?.document?.pages;
        if (!pages || !pages.length) {
            alert("Please scan a document first.");
            return;
        }

        const options = { pageSize: "A4", pageDirection: "PORTRAIT", pageFit: "FIT_IN", dpi: 72, jpegQuality: 80 };
        const bytes = await scanResult?.document?.createPdf(options);

        function saveBytes(data, name) {
            const extension = name.split(".")[1];
            const a = document.createElement("a");
            document.body.appendChild(a);
            a.style = "display: none";
            const blob = new Blob([data], { type: `application/${extension}` });
            const url = window.URL.createObjectURL(blob);
            a.href = url;
            a.download = name;
            a.click();
            window.URL.revokeObjectURL(url);
        }

        saveBytes(bytes, "generated.pdf");
    } catch (error) {
        console.error("Error during PDF export:", error);
        return Promise.reject(error);
    }
}

In the next step, we will make the scanning functionalities available to our Rust code.

Step 4: Bind the JavaScript functions in Rust

Open src/main.rs and import the Promise type from the js-sys crate, as well as the wasm_bindgen macro, which is used to expose JavaScript functions and types to Rust (and vice versa) when compiling Rust code to WebAssembly.

use js_sys::Promise;
use wasm_bindgen::prelude::wasm_bindgen;

Then add the following code to the file:

#[wasm_bindgen(module = "/assets/demo.js")]
extern "C" {
    #[wasm_bindgen(js_name = initScanbotSDK)]
    fn init_scanbot_sdk(license_key: &str);

    #[wasm_bindgen(js_name = startDocumentScan)]
    fn start_document_scan() -> Promise;

    #[wasm_bindgen(js_name = exportPDF)]
    fn export_pdf() -> Promise;
}

This allows your Rust code to call the specified JavaScript functions from demo.js.

Step 5: Load the SDK dynamically and initialize it

Before we can use the Scanbot SDK, we need to initialize it, e.g., in the App component in main.rs.

#[component]
fn App() -> Element {
    const SCANBOT_SDK_JS: Asset = asset!("assets/scanbot-web-sdk/bundle/ScanbotSDK.min.js");
    const SCANBOT_UI2_JS: Asset = asset!("assets/scanbot-web-sdk/bundle/ScanbotSDK.ui2.min.js");

    use_effect(move || {
        init_scanbot_sdk("");
    });

    rsx! {
        script { src: SCANBOT_SDK_JS }
        script { src: SCANBOT_UI2_JS }
    }
}

However, at the time of writing, if you try to call init_scanbot_sdk("") immediately, it may fail due to the following reasons:

  • The JavaScript SDK bundles are loaded and executed independently of the order in which they are added.
  • wasm_bindgen expects the functions it binds to exist globally when called.
  • If the SDK scripts are not loaded in time, your call to init_scanbot_sdk("") will fail.

Still, we can ensure the initialization will succeed by including some checks in our code:

#[component]
fn App() -> Element {
    const SCANBOT_SDK_JS: Asset = asset!("assets/scanbot-web-sdk/bundle/ScanbotSDK.min.js");
    const SCANBOT_UI2_JS: Asset = asset!("assets/scanbot-web-sdk/bundle/ScanbotSDK.ui2.min.js");

    // Tracks how many SDK scripts have loaded
    let mut sdk_bundles_loaded = use_signal(|| 0);

    // Indicates that the SDK has been fully initialized
    let mut sdk_ready = use_signal(|| false);

    // Initialize Scanbot SDK after both scripts are loaded
    use_effect(move || {
        if sdk_bundles_loaded() == 2 && !sdk_ready() {
            init_scanbot_sdk("");
            sdk_ready.set(true);
        }
    });

    rsx! {
        // Dynamically inject SDK scripts and track when they load
        script { src: SCANBOT_SDK_JS, onload: move |_| { sdk_bundles_loaded += 1 } }
        script { src: SCANBOT_UI2_JS, onload: move |_| { sdk_bundles_loaded += 1 } }
    }
}

This way, you ensure init_scanbot_sdk is called only after all necessary scripts have been loaded.

💡 Without a license key, our SDK only runs for 60 seconds per session. This is more than enough for the purposes of our tutorial, but if you like, you can generate a free trial license.

Alternative method: Use a custom index.html

If you need fine-grained control (for example, to include SDK scripts statically or add additional meta tags), you can provide your own index.html. The Dioxus CLI will automatically use your custom file instead of the default template while still injecting the necessary code to load your WASM bundle and enabling hot reloading during development.

Note that Dioxus expects an index.html containing a <div id="main"></div>, which is required for mounting your Rust/WASM app into the DOM (as mentioned in this GitHub issue).

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dioxus Document Scanner</title>
</head>
<body>
    <div id="main"></div> <!-- Required for Dioxus to mount your app -->
    <script src="/assets/scanbot-web-sdk/bundle/ScanbotSDK.min.js"></script>
    <script src="/assets/scanbot-web-sdk/bundle/ScanbotSDK.ui2.min.js"></script>
</body>
</html>

Step 6: Add the UI for scanning documents and exporting them as PDFs

Now it’s time to build a simple user interface for our Rust-based document scanning web app.

At the top of main.rs, import JsFuture from the wasm-bindgen-futures crate to work with JavaScript promises as Rust futures.

use wasm_bindgen_futures::JsFuture;

Then create a Demo component with buttons for triggering document scans and the PDF export.

#[component]
fn Demo() -> Element {
    let result = use_signal(|| String::new());

    // Document scan handler
    let on_document_scan = move |_| {
        spawn({
            let mut result = result.clone();
            async move {
                match JsFuture::from(start_document_scan()).await {
                    Ok(_) => {}
                    Err(_) => {
                        result.set("Error during document scan".into());
                    }
                }
            }
        });
    };

    // PDF export handler
    let on_pdf_export = move |_| {
        spawn({
            let mut result = result.clone();
            async move {
                match JsFuture::from(export_pdf()).await {
                    Ok(_) => {}
                    Err(_) => {
                        result.set("Error during PDF export".into());
                    }
                }
            }
        });
    };

    rsx! {
        div {
            style: "display: flex; flex-direction: column; align-items: center; gap: 1rem; padding: 1rem; max-width: 600px; margin: auto;",

            button {
                style: "background: #c8193c; color: white; border: none; padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 6px; cursor: pointer;",
                onclick: on_document_scan,
                "Scan Document"
            }

            button {
                style: "background: #c8193c; color: white; border: none; padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 6px; cursor: pointer;",
                onclick: on_pdf_export,
                "Export PDF"
            }

            if !result().is_empty() {
                pre {
                    style: "background: #1a1a1a; color: #00ff88; font-family: monospace; padding: 1rem; border-radius: 6px; width: 100%; max-width: 600px; max-height: 300px; overflow-y: auto; white-space: pre-wrap; word-break: break-word;",
                    "{result()}"
                }
            }
        }
    }
}

Finally, we need to call the Demo component inside our App component while ensuring that the SDK has been initialized.

#[component]
fn App() -> Element {
    // ...
    rsx! {
        script { src: SCANBOT_SDK_JS, onload: move |_| { sdk_bundles_loaded += 1 } }
        script { src: SCANBOT_UI2_JS, onload: move |_| { sdk_bundles_loaded += 1 } }

        // Call the Demo component after the SDK has been initialized
        if sdk_ready() {
            Demo {}
        }
    }
}

Your main.rs will then look like this:

use dioxus::prelude::*;
use js_sys::Promise;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen_futures::JsFuture;

fn main() {
    dioxus::launch(App);
}

#[component]
fn App() -> Element {
    const SCANBOT_SDK_JS: Asset = asset!("assets/scanbot-web-sdk/bundle/ScanbotSDK.min.js");
    const SCANBOT_UI2_JS: Asset = asset!("assets/scanbot-web-sdk/bundle/ScanbotSDK.ui2.min.js");
    let mut sdk_bundles_loaded = use_signal(|| 0);
    let mut sdk_ready = use_signal(|| false);
    use_effect(move || {
        if sdk_bundles_loaded() == 2 && !sdk_ready() {
            init_scanbot_sdk("");
            sdk_ready.set(true);
        }
    });

    rsx! {
        script { src: SCANBOT_SDK_JS, onload: move |_| { sdk_bundles_loaded += 1 } }
        script { src: SCANBOT_UI2_JS, onload: move |_| { sdk_bundles_loaded += 1 } }

        if sdk_ready() {
            Demo {}
        }
    }
}

#[wasm_bindgen(module = "/assets/demo.js")]
extern "C" {
    #[wasm_bindgen(js_name = initScanbotSDK)]
    fn init_scanbot_sdk(license_key: &str);

    #[wasm_bindgen(js_name = startDocumentScan)]
    fn start_document_scan() -> Promise;

    #[wasm_bindgen(js_name = exportPDF)]
    fn export_pdf() -> Promise;
}

#[component]
fn Demo() -> Element {
    let result = use_signal(|| String::new());
    let on_document_scan = move |_| {
        spawn({
            let mut result = result.clone();
            async move {
                match JsFuture::from(start_document_scan()).await {
                    Ok(_) => {}
                    Err(_) => {
                        result.set("Error during document scan".into());
                    }
                }
            }
        });
    };

    let on_pdf_export = move |_| {
        spawn({
            let mut result = result.clone();
            async move {
                match JsFuture::from(export_pdf()).await {
                    Ok(_) => {}
                    Err(_) => {
                        result.set("Error during PDF export".into());
                    }
                }
            }
        });
    };

    rsx! {
        div {
            style: "display: flex; flex-direction: column; align-items: center; gap: 1rem; padding: 1rem; max-width: 600px; margin: auto;",

            button {
                style: "background: #c8193c; color: white; border: none; padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 6px; cursor: pointer;",
                onclick: on_document_scan,
                "Scan Document"
            }

            button {
                style: "background: #c8193c; color: white; border: none; padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 6px; cursor: pointer;",
                onclick: on_pdf_export,
                "Export PDF"
            }

            if !result().is_empty() {
                pre {
                    style: "background: #1a1a1a; color: #00ff88; font-family: monospace; padding: 1rem; border-radius: 6px; width: 100%; max-width: 600px; max-height: 300px; overflow-y: auto; white-space: pre-wrap; word-break: break-word;",
                    "{result()}"
                }
            }
        }
    }
}

#[component]
pub fn Hero() -> Element {
    rsx! {}
}

Step 7: Run your Dioxus app and scan a document

To test your document scanning app locally, run:

dx serve

💡 To test your web app on your phone, you have a few options. One option is to use a service like ngrok, which creates a tunnel to one of their SSL-certified domains. Their Quick Start guide will help you get up and running quickly.

Scanning a document and exporting it as a PDF with our Dioxus document scanner app

Conclusion

🎉 Congratulations! You’ve successfully built a browser-based document scanning app using Rust, JavaScript, and the Dioxus framework!

If this tutorial has piqued your interest in integrating document scanning functionalities into your web app or website, make sure to take a look at the SDK’s other neat features in the documentation – or run our example project for a more hands-on experience.

Integration guides are also available for the following Web frameworks:

Should you have questions about this tutorial or run into any issues, we’re happy to help! Just shoot us an email via tutorial-support@scanbot.io.

Happy scanning! 🤳

Related blog posts

Experience our demo apps

Barcode Icon Art

Barcode Scanner SDK

Scan 1D and 2D barcodes reliably in under 0.04s. Try features like Batch Scanning, Scan & Count, and our AR Overlays.

Launch Web Demo

Scan the code to launch the web demo on your phone.

Web QR Code

Also available to download from:

Document Icon Art

Document Scanner SDK

Scan documents quickly and accurately with our free demo app. Create crisp digital scans in seconds.

Launch Web Demo

Scan the code to launch the web demo on your phone.

Black and white QR code. Scan this code for quick access to information.

Also available to download from:

Data_capture Icon Art

Data Capture Modules

Try fast, accurate data capture with our demo app. Extract data from any document instantly – 100% secure.

Launch Web Demo

Scan the code to launch the web demo on your phone.

Black and white QR code. Scan this quick response code with your smartphone.

Also available to download from: