Build a Mobile Document Scanner with HTML5 and JavaScript

Dynamic Web TWAIN has added some exciting features related to the mobile platform since its version 16.

  • Document capturing via mobile cameras, 16.0
  • PDF Rasterizer mobile edition, 16.0
  • LoadImageEx supports mobile platform, 16.1
  • Acquiring images from a remote scanner, 16.1
  • The scanDocument API which makes it easy to call a built-in document scanner to scan documents using mobile cameras in a mobile-friendly interface with features like automatic capture, document borders detection and image filter, 17.2

All of these make it possible to build a mobile document scanning and capture app in HTML5 with ease. Such a mobile document scanner can not only utilize high-resolution cameras equipped on modern mobile devices but also interact with connected scanners. Your phone can be the hub of the entire document scanning process.

Deprecation Note: the document scanning via camera feature in Dynamic Web TWAIN has been moved to a new product called Mobile Web Capture. Please use this instead.

What you’ll build: A mobile-friendly web document scanner that captures images via the device camera, edits them in-browser, and exports to JPEG, TIFF, or PDF — all using HTML5 and the Dynamic Web TWAIN JavaScript SDK.

Mobile Document Scanner Preview

Key Takeaways

  • Dynamic Web TWAIN’s scanDocument API turns any mobile browser into a document scanner with automatic capture, border detection, and image filtering.
  • The SDK works entirely in-browser via HTML5 and MediaDevices — no native app installation required.
  • Scanned documents can be exported as JPEG, multi-page TIFF, or PDF directly from the mobile device.
  • The same codebase runs on both desktop and mobile browsers, simplifying cross-platform deployment.

Common Developer Questions

  • How do I scan documents from a mobile camera using HTML5 and JavaScript?
  • Can I build a mobile document scanner web app without a native SDK?
  • How do I save scanned images as PDF from a mobile browser?

This article is Part 1 in a 1-Part Series.

Prerequisites

Build the Mobile Document Scanner Step by Step

Let’s implement the scanner in steps.

Preview the Final Result

Our goal is to build a web document scanner that mobile devices can use. The layout should be optimized. Here is a preview of the final result:

Home Preview Save Preview

On the top is a viewer of scanned documents. Below the viewer, there is an actions tab. Since there is not much screen space for mobile devices, using a tab is a good choice if there are many actions available.

After pressing the Scan Documents button, a built-in scanner will appear. Users can take a photo, edit the result and then save it.

Scanner Cropper

Step 1: Create a Minimum Camera-Based Scanner

Here, we first create a minimum version which captures images with the camera and save them to image or PDF files. Scanned documents can later be exported to image or PDF files, as well. The entire HTML file is as below.

<!DOCTYPE html>
<html>

<head>
    <title>Dynamic Web TWAIN Mobile Sample</title>
    <script type="text/javascript" src="Resources/dynamsoft.webtwain.initiate.js"></script>
    <script type="text/javascript" src="Resources/dynamsoft.webtwain.config.js"></script>
    <script type="text/javascript" src="Resources/addon/dynamsoft.webtwain.addon.camera.js"></script>
    <script type="text/javascript" src="Resources/addon/dynamsoft.webtwain.addon.pdf.js"></script>
    <script type="text/javascript" src="common.js"></script>
    <script type="text/javascript" src="tabs.js"></script>
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
    <link rel="stylesheet" href="css/normalize.css">
    <link rel="stylesheet" href="css/skeleton.css">
    <link rel="stylesheet" href="css/tabs.css">
    <link rel="stylesheet" href="css/index.css">
</head>

<body>
    <div style="height:100%;width:100%;position:absolute;overflow:hidden;">
        <div id="dwtcontrolContainer" style="width:100%;height:60%"></div>
        <div id="action" style="height:40%;">
            <div class="tab" style="display:flex;overflow-x:auto;height:50px;">
                <button class="tablinks" onclick="switchTab(event, 'camera')">Camera</button>
                <button class="tablinks" onclick="switchTab(event, 'save')">Save</button>
            </div>
            <div style="height:calc(100% - 50px);overflow-y: auto;">
                <div id="camera" class="tabcontent">
                    <input id="scanDocumentButton" type="button" value="Scan Documents" onclick="ScanDocuments();" />
                </div>
                <div id="save"  class="tabcontent">
                    Default Filename: <input type="text" id="filename" value="DynamicWebTWAIN"/>
                    <input onclick="SaveWithFileDialog();" type="button" value="Save">
                    <div>
                        <label>
                            <input type="radio" value="jpg" name="ImageType" id="imgTypejpeg" />JPEG</label>
                        <label>
                            <input type="radio" value="tif" name="ImageType" id="imgTypetiff" />TIFF</label>
                        <label>
                            <input type="radio" value="pdf" name="ImageType" id="imgTypepdf" checked="checked" />PDF</label>
                    </div>
                </div>                
           </div>
        </div>
    </div>
    <script type="text/javascript">
        Dynamsoft.DWT.UseLocalService = false;
        Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', Dynamsoft_OnReady); // Register OnWebTwainReady event. This event fires as soon as Dynamic Web TWAIN is initialized and ready to be used
        var DWObject;
        switchTab(null, 'camera')
        
        function Dynamsoft_OnReady() {
            DWObject = Dynamsoft.DWT.GetWebTwain('dwtcontrolContainer'); // Get the Dynamic Web TWAIN object that is embeded in the div with id 'dwtcontrolContainer'
            if (DWObject) {
                DWObject.Viewer.width="100%";
                DWObject.Viewer.height="100%";
                DWObject.SetViewMode(2, 2);
            }
        }
        

        function ScanDocuments() {
            if (DWObject) {
                let cameraContainer = document.createElement("div");
                cameraContainer.className = "fullscreen";
                document.body.appendChild(cameraContainer);
                function funcConfirmExit () {
                    cameraContainer.remove();
                    return true;
                }
                let scanOptions = {};
                scanOptions.element = cameraContainer
                scanOptions.scannerViewer = {funcConfirmExit: funcConfirmExit}
                DWObject.Addon.Camera.scanDocument(scanOptions).then(
                    function(){
                        console.log("OK");
                    }, 
                    function(error){
                        console.log(error.message);
                    }
                );
            }
        }


        //Callback functions for async APIs
        function OnSuccess() {
            console.log('successful');
        }

        function OnFailure(errorCode, errorString) {
            alert(errorString);
        }

        function SaveWithFileDialog() {
            if (DWObject) {
                if (DWObject.HowManyImagesInBuffer > 0) {
                    DWObject.IfShowFileDialog = true;
                    var filename=document.getElementById("filename").value;
                    if (document.getElementById("imgTypejpeg").checked == true) {
                        //If the current image is B&W
                        //1 is B&W, 8 is Gray, 24 is RGB
                        if (DWObject.GetImageBitDepth(DWObject.CurrentImageIndexInBuffer) == 1)
                            //If so, convert the image to Gray
                            DWObject.ConvertToGrayScale(DWObject.CurrentImageIndexInBuffer);
                        //Save image in JPEG
                        DWObject.SaveAsJPEG(filename+".jpg", DWObject.CurrentImageIndexInBuffer);
                    }
                    else if (document.getElementById("imgTypetiff").checked == true)
                        DWObject.SaveAllAsMultiPageTIFF(filename+".tiff", OnSuccess, OnFailure);
                    else if (document.getElementById("imgTypepdf").checked == true)
                        DWObject.SaveAllAsPDF(filename+".pdf", OnSuccess, OnFailure);
                }
            }
        }
    </script>
</body>
</html>

Here are some points worth mentioning:

  • We use the Skeleton CSS framework and add the viewport meta to make the page responsive and suitable for mobile devices.

      <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
      <link rel="stylesheet" href="css/normalize.css">
      <link rel="stylesheet" href="css/skeleton.css">
      <link rel="stylesheet" href="css/tabs.css">
    
  • The tabs are created according to How To Create Tabs. The tab’s style is modified so that the tab is horizontally scrollable.
  • The camera addon’s scanDocument API is used to access mobile cameras which is through MediaDevices. MediaDevices requires HTTPS. If you don’t have an HTTPS server, you can create a local server using Python 3 following this post.
  • You need to copy the Resources folder from Dynamsoft\Dynamic Web TWAIN SDK <version>\Resources to the project’s root directory.

Step 2: Add More Input Sources

Now we can add new functions based on the minimum implementation. Basically, there are three input sources: scanner, camera, local files.

Load Local Files into the Scanner

Loading local files is very simple with the LoadImageEx method. It can load images as well as PDFs.

JavaScript:

//Callback functions for async APIs
function OnSuccess() {
    console.log('successful');
}

function OnFailure(errorCode, errorString) {
    alert(errorString);
}

function LoadLocal(){
    DWObject.IfShowFileDialog = true;
    // PDF Rasterizer Addon is used here to ensure PDF support
    DWObject.Addon.PDF.SetResolution(200);
    DWObject.Addon.PDF.SetConvertMode(Dynamsoft.EnumDWT_ConvertMode.CM_RENDERALL);
    DWObject.LoadImageEx("", Dynamsoft.EnumDWT_ImageType.IT_ALL, OnSuccess, OnFailure);
}

HTML:

<button class="tablinks" onclick="switchTab(event, 'local')">Local</button>
......
<div id="local" class="tabcontent">            
    <input onclick="LoadLocal();" type="button" value="Load Images">                
</div>

Connect to Remote Scanners

Since mobile devices normally cannot connect to scanners physically, we can only use the RemoteScan capability. How to Build a Universal Document Scanning App in HTML5 explains how to do this in detail.

Step 3: Enable Image Editing

We may also need to remove, reorder and edit images. Dynamic Web TWAIN provides rich APIs to do this.

It also has a powerful image editor which can adapt to mobile devices. Functions like rotation, deskewing, flipping, mirroring, cropping and cutting are supported.

Image Editor

JavaScript:

function ShowImageEditor(){
    var editorSettings = {
        /*element: document.getElementById("imageEditor"),
        width: 600,
        height: 400,*/
        border: '1px solid rgb(204, 204, 204);',
        topMenuBorder: '',
        innerBorder: '',
        background: "rgb(255, 255, 255)",
        promptToSaveChange: true
    };
    var imageEditor = DWObject.Viewer.createImageEditor(editorSettings);
    imageEditor.show();
}

HTML:

<button class="tablinks" onclick="switchTab(event, 'edit')">Edit</button>
......
<div id="edit" class="tabcontent">
    <input onclick="ShowImageEditor()" type="button" value="Show Editor" />
</div>

Common Issues & Edge Cases

  • Camera access blocked on HTTP: The scanDocument API relies on MediaDevices, which requires HTTPS. If you test on plain HTTP, the browser will deny camera access silently. Use a local HTTPS server or deploy behind an HTTPS reverse proxy.
  • Fullscreen overlay not dismissed: If the funcConfirmExit callback is not wired correctly, the camera overlay may stay on screen after scanning. Always ensure the callback removes the container element and returns true.
  • PDF export fails on large batches: Exporting many high-resolution scans to a single PDF can exhaust browser memory on low-end mobile devices. Consider reducing resolution via SetResolution or splitting into smaller batches.

Source Code

Get the source code and have a try! The final version can run on both desktop and mobile.

https://github.com/Dynamsoft/mobile-document-scanning