How to Scan and Print a Document on Your Phone with JavaScript

Using a phone to scan and print (copy) documents is ideal for convenience, portability, and quick tasks, making it a versatile and practical choice for many users. In this article, we are going to build a web app to scan and print a document on a mobile phone.

The following SDKs by Dynamsoft are used:

The web app can capture a document image, resize it, and make it black-white for printing.

Capturing:

capture viewer

Scanned image (cropped):

scanned

Black and white image for printing:

black and white

Online demo

Prerequisites

Get your trial key.

New HTML File

Create a new HTML with the following content.

<!DOCTYPE html>
<html>
<head>
  <title>Document Scanning via Camera</title>
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
  <style>
    #container {
      max-width: 100%;
      height: 480px;
    }
  </style>
</head>
<body>
  <h2>Document Scanning via Camera</h2>
  <label>
    Camera:
    <select id="select-camera"></select>
  </label>
  <label>
    Resolution:
    <select id="select-resolution">
      <option value="640x480">640x480</option>
      <option value="1280x720">1280x720</option>
      <option value="1920x1080" selected>1920x1080</option>
      <option value="3840x2160">3840x2160</option>
    </select>
  </label>
  <button onclick="startScanning();">Start</button>
  <div id="container"></div>
  <script type="text/javascript">
  </script>
</body>
</html>

Include Dependencies

  1. Add Dynamsoft Document Viewer.

    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@latest/dist/ddv.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@latest/dist/ddv.css">
    
  2. Add Dynamsoft Document Normalizer by including the Capture Vision bundle.

    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-bundle@3.0.3001/dist/dcv.bundle.js"></script>
    

Initialize the SDKs

Initialize the SDKs with a license.

async function init(){
  // Initialize license
  await Dynamsoft.License.LicenseManager.initLicense(
    "LICENSE-KEY",
    true
  );
  // Initialize DDN
  Dynamsoft.Core.CoreModule.loadWasm(["DDN"]);
  Dynamsoft.DDV.Core.loadWasm();
  // Initialize DDV
  await Dynamsoft.DDV.Core.init();
}

Scan Documents

Dynamsoft Document Viewer provides a set of viewers for document scanning.

We are going to use its Capture Viewer to capture documents via the camera, its Perspective Viewer to adjust the detected boundaries and its Edit Viewer to view, edit and save the scanned documents.

Capture Viewer:

capture viewer

Perspective Viewer:

perspective viewer

Edit Viewer:

edit viewer

The following is the code to set the viewers up.

  1. Initialize Capture Viewer and bind it to the container.

    let captureViewer = new Dynamsoft.DDV.CaptureViewer({
        container: "container",
        uiConfig: captureViewerUiConfig,
        viewerConfig: {
            acceptedPolygonConfidence: 60,
            enableAutoDetect: false,
        }
    });
    
  2. Initialize Perspective Viewer and bind it to the container.

    let perspectiveViewer = new Dynamsoft.DDV.PerspectiveViewer({
        container: "container",
        groupUid: captureViewer.groupUid,
        uiConfig: perspectiveUiConfig,
        viewerConfig: {
            scrollToLatest: true,
        }
    });
    perspectiveViewer.hide();
    
  3. Initialize Edit Viewer and bind it to the container.

    editViewer = new Dynamsoft.DDV.EditViewer({
        container: "container",
        groupUid: captureViewer.groupUid,
        uiConfig: editViewerUiConfig
    });
    editViewer.hide();
    
  4. Configure the UIs of the viewers using config objects.

    const captureViewerUiConfig = {
        type: Dynamsoft.DDV.Elements.Layout,
        flexDirection: "column",
        children: [
            {
                type: Dynamsoft.DDV.Elements.Layout,
                className: "ddv-capture-viewer-header-mobile",
                children: [
                    {
                        type: "CameraResolution",
                        className: "ddv-capture-viewer-resolution",
                    },
                    Dynamsoft.DDV.Elements.Flashlight,
                ],
            },
            Dynamsoft.DDV.Elements.MainView,
            {
                type: Dynamsoft.DDV.Elements.Layout,
                className: "ddv-capture-viewer-footer-mobile",
                children: [
                    Dynamsoft.DDV.Elements.AutoDetect,
                    Dynamsoft.DDV.Elements.AutoCapture,
                    {
                        type: "Capture",
                        className: "ddv-capture-viewer-captureButton",
                    },
                    {
                        // Bind click event to "ImagePreview" element
                        // The event will be registered later.
                        type: Dynamsoft.DDV.Elements.ImagePreview,
                        events:{ 
                            click: "showPerspectiveViewer"
                        }
                    },
                    Dynamsoft.DDV.Elements.CameraConvert,
                ],
            },
        ],
    };
    
    
    const perspectiveUiConfig = {
        type: Dynamsoft.DDV.Elements.Layout,
        flexDirection: "column",
        children: [
            {
                type: Dynamsoft.DDV.Elements.Layout,
                className: "ddv-perspective-viewer-header-mobile",
                children: [
                    {
                        // Add a "Back" button in perspective viewer's header and bind the event to go back to capture viewer.
                        // The event will be registered later.
                        type: Dynamsoft.DDV.Elements.Button,
                        className: "ddv-button-back",
                        events:{
                            click: "backToCaptureViewer"
                        }
                    },
                    Dynamsoft.DDV.Elements.Pagination,
                    {   
                        // Bind event for "PerspectiveAll" button to show the edit viewer
                        // The event will be registered later.
                        type: Dynamsoft.DDV.Elements.PerspectiveAll,
                        events:{
                            click: "showEditViewer"
                        }
                    },
                ],
            },
            Dynamsoft.DDV.Elements.MainView,
            {
                type: Dynamsoft.DDV.Elements.Layout,
                className: "ddv-perspective-viewer-footer-mobile",
                children: [
                    Dynamsoft.DDV.Elements.FullQuad,
                    Dynamsoft.DDV.Elements.RotateLeft,
                    Dynamsoft.DDV.Elements.RotateRight,
                    Dynamsoft.DDV.Elements.DeleteCurrent,
                    Dynamsoft.DDV.Elements.DeleteAll,
                ],
            },
        ],
    };
    
    const editViewerUiConfig = {
        type: Dynamsoft.DDV.Elements.Layout,
        flexDirection: "column",
        className: "ddv-edit-viewer-mobile",
        children: [
            {
                type: Dynamsoft.DDV.Elements.Layout,
                className: "ddv-edit-viewer-header-mobile",
                children: [
                    {
                        // Add a "Back" buttom to header and bind click event to go back to the perspective viewer
                        // The event will be registered later.
                        type: Dynamsoft.DDV.Elements.Button,
                        className: "ddv-button-back",
                        events:{
                            click: "backToPerspectiveViewer"
                        }
                    },
                    Dynamsoft.DDV.Elements.Pagination,
                    {
                        type: Dynamsoft.DDV.Elements.Button,
                        className: "ddv-button-menu",
                        events:{
                            click: "menu"
                        }
                    }
                ],
            },
            Dynamsoft.DDV.Elements.MainView,
            {
                type: Dynamsoft.DDV.Elements.Layout,
                className: "ddv-edit-viewer-footer-mobile",
                children: [
                    Dynamsoft.DDV.Elements.DisplayMode,
                    Dynamsoft.DDV.Elements.RotateLeft,
                    Dynamsoft.DDV.Elements.Crop,
                    Dynamsoft.DDV.Elements.Filter,
                    Dynamsoft.DDV.Elements.Undo,
                    Dynamsoft.DDV.Elements.Delete,
                    Dynamsoft.DDV.Elements.Load,
                    Dynamsoft.DDV.Elements.AnnotationSet,
                ],
            },
        ],
    };
    
  5. Add a helper function to switch the viewers.

    // Define a function to control the viewers' visibility
    const switchViewer = (c,p,e) => {
      captureViewer.hide();
      perspectiveViewer.hide();
      editViewer.hide();
    
      if(c) {
        captureViewer.show();
      } else {
        captureViewer.stop();
      }
         
      if(p) perspectiveViewer.show();
      if(e) editViewer.show();
    };
    
  6. Register events for the buttons defined to control the workflow.

    // Register an event in `perspectiveViewer` to go back the capture viewer
    perspectiveViewer.on("backToCaptureViewer",() => {
        switchViewer(1,0,0);
        captureViewer.play().catch(err => {alert(err.message)});
    });
    
    // Register an event in `perspectiveViewer` to show the edit viewer
    perspectiveViewer.on("showEditViewer",() => {
        switchViewer(0,0,1)
    });
    // Register an event in `editViewer` to go back to the perspective viewer
    editViewer.on("backToPerspectiveViewer",() => {
        switchViewer(0,1,0);
    });
    
  7. Use Dynamsoft Document Normalizer for edge detection by defining the detection handler.

    // Configure document boundaries function
    await initDocDetectModule(Dynamsoft.DDV, Dynamsoft.CVR);
       
    async function initDocDetectModule(DDV, CVR) {
      const router = await CVR.CaptureVisionRouter.createInstance();
      class DDNNormalizeHandler extends DDV.DocumentDetect {
        async detect(image, config) {
          if (!router) {
            return Promise.resolve({
              success: false
            });
          };
    
          let width = image.width;
          let height = image.height;
          let ratio = 1;
          let data;
    
          if (height > 720) {
            ratio = height / 720;
            height = 720;
            width = Math.floor(width / ratio);
            data = compress(image.data, image.width, image.height, width, height);
          } else {
            data = image.data.slice(0);
          }
    
    
          // Define DSImage according to the usage of DDN
          const DSImage = {
            bytes: new Uint8Array(data),
            width,
            height,
            stride: width * 4, //RGBA
            format: 10 // IPF_ABGR_8888
          };
    
          // Use DDN normalized module
          const results = await router.capture(DSImage, 'DetectDocumentBoundaries_Default');
    
          // Filter the results and generate corresponding return values
          if (results.items.length <= 0) {
            return Promise.resolve({
              success: false
            });
          };
    
          const quad = [];
          results.items[0].location.points.forEach((p) => {
            quad.push([p.x * ratio, p.y * ratio]);
          });
    
          const detectResult = this.processDetectResult({
            location: quad,
            width: image.width,
            height: image.height,
            config
          });
          return Promise.resolve(detectResult);
        }
      }
      DDV.setProcessingHandler('documentBoundariesDetect', new DDNNormalizeHandler())
    }
       
    //compress the video frames
    function compress(
        imageData,
        imageWidth,
        imageHeight,
        newWidth,
        newHeight,
    ) {
      let source = null;
      try {
          source = new Uint8ClampedArray(imageData);
      } catch (error) {
          source = new Uint8Array(imageData);
      }
    
      const scaleW = newWidth / imageWidth;
      const scaleH = newHeight / imageHeight;
      const targetSize = newWidth * newHeight * 4;
      const targetMemory = new ArrayBuffer(targetSize);
      let distData = null;
    
      try {
          distData = new Uint8ClampedArray(targetMemory, 0, targetSize);
      } catch (error) {
          distData = new Uint8Array(targetMemory, 0, targetSize);
      }
    
      const filter = (distCol, distRow) => {
          const srcCol = Math.min(imageWidth - 1, distCol / scaleW);
          const srcRow = Math.min(imageHeight - 1, distRow / scaleH);
          const intCol = Math.floor(srcCol);
          const intRow = Math.floor(srcRow);
    
          let distI = (distRow * newWidth) + distCol;
          let srcI = (intRow * imageWidth) + intCol;
    
          distI *= 4;
          srcI *= 4;
    
          for (let j = 0; j <= 3; j += 1) {
              distData[distI + j] = source[srcI + j];
          }
      };
    
      for (let col = 0; col < newWidth; col += 1) {
          for (let row = 0; row < newHeight; row += 1) {
              filter(col, row);
          }
      }
    
      return distData;
    }
    
  8. Start the document scanning workflow using a selected camera and a specified resolution.

    async function startScanning(){
      let selectedCamera = cameras[document.getElementById("select-camera").selectedIndex];
      await captureViewer.selectCamera(selectedCamera.deviceId);
      let selectedResolution = document.getElementById("select-resolution").selectedOptions[0].value;
      let width = parseInt(selectedResolution.split("x")[0]);
      let height = parseInt(selectedResolution.split("x")[1]);
      captureViewer.play({
        resolution: [width,height],
      }).catch(err => {
        alert(err.message)
      });
    }
    

Adjust the Image Size

The size of the scanned document image may not conform to the ratio of a standard like A4 (210 x 297 mm).

We can resize the image with canvas.

let pageWidth = 210; //mm
let pageHeight = 297; //mm
let DPI = 300;
let pageWidthPx = pageWidth * DPI / 25.4; //convert to pixel
let pageHeightPx = pageHeight * DPI / 25.4;
let docImageBlob = (await editViewer.currentDocument.getPageData(pageUid)).display.data;
let img = document.createElement("img");
img.onload = async function(){
  let resizedDocBlob = await resizeImage(img,docWidthPx,docHeightPx);
  await editViewer.currentDocument.updatePage(pageUid, resizedDocBlob);
};
img.src = URL.createObjectURL(docImageBlob);

function resizeImage(image, width, height) {
  return new Promise((resolve, reject) => {
    let canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    let ctx = canvas.getContext("2d");
    ctx.drawImage(image, 0, 0, width, height);
    canvas.toBlob(blob => {
      resolve(blob);
    });
  });
}

In some cases, the scanned document is small, like a business card and we want to print it on a piece of A4 paper. Then, we can create an empty page first and then add the resized document image on it.

let docWidth = 89; //mm
let docHeight = 51; //mm
let pageWidth = 210; //mm
let pageHeight = 297; //mm
let DPI = 300;
let pageWidthPx = pageWidth * DPI / 25.4; //convert to pixel
let pageHeightPx = pageHeight * DPI / 25.4;
let docWidthPx = docWidth * DPI / 25.4;
let docHeightPx = docHeight * DPI / 25.4;
let pageBlob = await createEmptyPage(pageWidthPx,pageHeightPx);
let docImageBlob = (await editViewer.currentDocument.getPageData(pageUid)).display.data;
let img = document.createElement("img");
img.onload = async function(){
  let resizedDocBlob = await resizeImage(img,docWidthPx,docHeightPx);
  await editViewer.currentDocument.updatePage(pageUid, pageBlob);
  let newPageData = await editViewer.currentDocument.getPageData(pageUid);
  let ratio = newPageData.cropBox.width / newPageData.display.width;
  const rect = {
    x: 50,
    y: 50,
    width: docWidthPx * ratio,
    height: docHeightPx * ratio
  };
  const options = {
    x: rect.x,
    y: rect.y,
    width: rect.width,
    height: rect.height,
    stamp: resizedDocBlob 
  };
  const stamp = await Dynamsoft.DDV.annotationManager.createAnnotation(pageUid, "stamp", options); //insert the document image as an annotation so that we can move it around
}
img.src = URL.createObjectURL(docImageBlob);

function createEmptyPage(width,height){
  return new Promise((resolve, reject) => {
    let canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    let ctx = canvas.getContext("2d");
    ctx.fillStyle = "white";
    ctx.fillRect(0,0,width,height);
    canvas.toBlob(blob => {
      resolve(blob);
    });  
  })
}

resized image

Apply an Image Filter

We have to whiten the image for printing. We can apply filters using Dynamsoft Document Viewer’s Edit Viewer.

image filters

Here is the code to set up the image filter:

// Configure image filter feature which is in edit viewer
Dynamsoft.DDV.setProcessingHandler("imageFilter", new Dynamsoft.DDV.ImageFilter());

You can also define your own image filters. Check out this blog to learn more.

Save as PDF

Save the document as PDF with a specified page type:

HTML:

<label>
  Page Size:
  <select id="page-size-select">
    <option value="page/default">Default</option>
    <option value="page/a4">A4</option>
    <option value="page/a3">A3</option>
    <option value="page/letter">Letter</option>
  </select>
</label>

JavaScript:

async function downloadAsPDF(){
  let pageSize = document.getElementById("page-size-select").selectedOptions[0].value;
  const pdfSettings = {
    compression: "pdf/jpeg",
    pageType: pageSize
  };
  const blob = await editViewer.currentDocument.saveToPdf(pdfSettings);
  downloadBlob(blob,"document.pdf");
};

function downloadBlob(blob,filename){
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  a.click();
}

Print the PDF document using a connected printer.

On mobile phones, we mostly use wireless printing.

On iOS, we can use AirPrint.

airprint

On Android, we can use Mopria.

mopria

Source Code

Get the source code and have a try!

https://github.com/tony-xlh/scan-and-print-document-javascript/