Convert a Mobile Camera into a Document Scanner | Mobile Capture

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

  • 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

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.

Requirements

Dynamic Web TWAIN SDK

Mobile Document Scanning Implementation

Let’s implement the scanner in steps.

Preview

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:

Scanner 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.

A Minimum Implementation: Snapshot from MediaDevices Cameras

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 to 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">
</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">
                    <select id="camerasource"></select>
                    <input id="captureButton" type="button" value="Capture Images" onclick="CaptureImages();" />
                    <input id="switchButton" type="button" value="Hide Video" onclick="SwitchViews();" />
                </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">
        var console = window['console'] ? window['console'] : { 'log': function () { } };
        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);
   
                SetIfWebcamPlayVideo(false);    
                DWObject.Addon.Camera.getSourceList().then(function (sourceName) {
                    var iCount = sourceName.length;
                    for (var i = 0; i < iCount; i++) {
                        document.getElementById("camerasource").options.add(new Option(sourceName[i].label, sourceName[i].deviceId));
                    }
                });
                document.getElementById('camerasource').onchange = function () {
                    var deviceId = document.getElementById("camerasource").options[document.getElementById("camerasource").selectedIndex].value;
                    DWObject.Addon.Camera.selectSource(deviceId).then(function () {
                        SetIfWebcamPlayVideo(true);
                        document.getElementById("switchButton").style.display = '';
                        document.getElementById("captureButton").disabled = "";
                    }, function (ex) { console.log(ex.message); });
                }
            }
        }
        
        function CaptureImages() {
            if (DWObject) {
                DWObject.Addon.Camera.capture().then(function () {
                    DWObject.Viewer.render();
                    SetIfWebcamPlayVideo(false);
                }, function (ex) {
                    console.log(ex.message);
                    SetIfWebcamPlayVideo(false);
                });
            }
        }

        function SetIfWebcamPlayVideo(bShow) {
            if (bShow) {
                DWObject.Addon.Camera.play(null, { width: 2560, height: 1440 },true).then(function (r) {
                    isVideoOn = true;
                    document.getElementById("captureButton").style.backgroundColor = "";
                    document.getElementById("captureButton").disabled = "";
                    document.getElementById("switchButton").value = "Hide Video";
                }, function (ex) {
                    console.log(ex.message);
                    DWObject.Addon.Camera.stop();
                });
            }
            else {
                DWObject.Addon.Camera.stop();
                isVideoOn = false;
                document.getElementById("captureButton").style.backgroundColor = "#aaa";
                document.getElementById("captureButton").disabled = "disabled";
                document.getElementById("switchButton").value = "Show Video";

            }
        }

        function SwitchViews() {
            if (isVideoOn == false) {
                // continue the video
                SetIfWebcamPlayVideo(true);
            } else {
                // stop the video
                SetIfWebcamPlayVideo(false);
            }
        }

        //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 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.

Build the Muscle: Add More Inputs

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

Enable Loading Local Files

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>

Interact with 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.

Build the Muscle: Image Editing and Viewer

The viewer should be adjustable and 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>

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

Related Article

Scan with Document Cameras using Dynamic Web TWAIN. This article uses the camera addon’s showVideo API which supports continuous scanning, edge detection and perspective adjustment.

Search Blog Posts