How to Build a Manifest v3 Chrome Extension to Add Document Scanning Function to Your Web Pages

Chrome extensions are software programs, built on web technologies (such as HTML, CSS, and JavaScript) that enable users to customize the Chrome browsing experience.1

In this article, we are going to write a Chrome extension to add the document scanning function to web pages. Dynamic Web TWAIN is used to provide the ability to scan documents via scanners or cameras.

The extension adds a floating action button in the bottom-left of web pages. If it is clicked, a modal will appear for the users to scan documents. We can copy the scanned document into web apps like Microsoft Office, Gmail, etc. You can check out the video to see how it works.

You can follow this guide to install it.

Write a Chrome Extension to Scan Documents

A Chrome extension has a manifest file in JSON format. A basic manifest looks like the following:

{
  "name": "Document Scanner",
  "version": "1.0",
  "manifest_version": 3
}

In the manifest file, we can define things like metadata, permissions, resources and which files to use.

In addition to manifest, there are service workers which handle and listen for browser events, content scripts which execute Javascript in the context of a web page and other pages like popup and options pages.

Here, we have to use content scripts to load scripts to add the document scanning function.

For convenience, we can write the code in a normal HTML file first and then adapt it as a Chrome extension.

Write a Document Scanning Page

  1. Create a new HTML file with the following template:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Dynamic Web TWAIN Sample</title>
        <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
        <style>
        </style>
    </head>
    <body>
      <script type="text/javascript">
      //your scripts here
      </script>
    </body>
    </html>
    
  2. Download the SDK of Dynamic Web TWAIN. Copy its Resources folder into the root. Create a dwt.js file in Resources for codes related to operating Web TWAIN.

  3. Modify dynamsoft.webtwain.config.js to disable auto loading of the library.

    - Dynamsoft.DWT.AutoLoad = true;
    + Dynamsoft.DWT.AutoLoad = false;
    
  4. Load the library of Dynamic Web TWAIN.

    init();
    async function init(){
      await loadLibrary("Resources/dynamsoft.webtwain.initiate.js","text/javascript");
      await loadLibrary("Resources/dynamsoft.webtwain.config.js","text/javascript");
      await loadLibrary("Resources/addon/dynamsoft.webtwain.addon.camera.js","text/javascript");
      await loadLibrary("Resources/addon/dynamsoft.webtwain.addon.pdf.js","text/javascript");
      await loadLibrary("Resources/dwt.js","text/javascript");
    }
       
    function loadLibrary(src,type){
      return new Promise(function (resolve, reject) {
        let scriptEle = document.createElement("script");
        scriptEle.setAttribute("type", type);
        scriptEle.setAttribute("src", src);
        document.body.appendChild(scriptEle);
        scriptEle.addEventListener("load", () => {
          console.log(src+" loaded")
          resolve(true);
        });
        scriptEle.addEventListener("error", (ev) => {
          console.log("Error on loading "+src, ev);
          reject(ev);
        });
      });
    }
    
  5. In dwt.js, add scripts to add a floating action button in the bottom-left. Clicking the button will reveal a modal for document scanning.

    JavaScript:

    window.onload = function () {
      addButton();
    }
    
    function addButton(){
      const button = document.createElement("div");
      button.className = "dwt-fab";
      const a = document.createElement("a")
      a.href = "javascript:void(0)";
      const icon = document.createElement("img")
      icon.src = "https://tony-xlh.github.io/document-scanner-userscript/scanner-scan.svg"
      a.appendChild(icon);
      button.appendChild(a);
      document.body.appendChild(button);
      button.addEventListener("click", () => {
        showModal();
      });
    }
    

    CSS:

    .dwt-fab {
      position:fixed;
      bottom:0;
      width: 50px;
      height: 50px;
      left: 0;
      margin: 10px;
      border-radius: 50%;
      background: #2196f3;
      pointer-events: auto;
      z-index: 9999;
    }
    
    .dwt-fab:hover {
      background: #7db1d4;
    }
    
    .dwt-fab img {
      padding: 10px;
    }
    
  6. In dwt.js, add scripts to add or show the modal in the showModal function and add a button to hide the modal.

    let modal;
    function showModal(){
      if (!modal) {
        modal = document.createElement("div");
        modal.className = "dwt-modal";
        document.body.appendChild(modal);
        const header = document.createElement("div");
        const closeBtn = document.createElement("div");
        closeBtn.className = "dwt-close-btn";
        closeBtn.innerText = "x";
        header.appendChild(closeBtn);
        header.className = "dwt-header";
        closeBtn.addEventListener("click", () => {
          hideModal();
        });
           
        modal.appendChild(header);
      }
      document.querySelector(".dwt-fab").style.display = "none";
      modal.style.display = "";
    }
       
    function hideModal(){
      modal.style.display = "none";
      document.querySelector(".dwt-fab").style.display = "";
    }
    

    CSS:

    .dwt-modal {
      position:fixed;
      left: 20px;
      top: 20px;
      width: calc(100% - 40px);
      height: calc(100% - 40px);
      border: 1px solid gray;
      border-radius: 5px;
      background: white;
      z-index: 9999;
    }
    
    .dwt-header {
      height: 25px;
    }
    
    .dwt-close-btn {
      float: right;
      margin-right: 5px;
    }
    
  7. In the modal, add a body container which contains a container for the viewer control of Dynamic Web TWAIN and initialize Web TWAIN if it has not been initialized. You may need to apply for a license to use Web TWAIN.

    let DWObject;
    function showModal(){
       //...
       const body = document.createElement("div");
       body.className = "dwt-body";
       const viewer = document.createElement("div");
       viewer.id = "dwtcontrolContainer";
       body.appendChild(viewer);
       modal.appendChild(body);
       if (!DWObject) {
         initDWT();
       }
    }
       
    function initDWT(){
      Dynamsoft.DWT.Containers = [{ ContainerId: 'dwtcontrolContainer',Width: 270, Height: 350 }];
      //Dynamsoft.DWT.ProductKey = "<your license key>"; //a public trial will be used if the key is not specified
      Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', function () {
        console.log("ready");
        DWObject = Dynamsoft.DWT.GetWebTwain('dwtcontrolContainer');
        DWObject.Viewer.width = "100%";
        DWObject.Viewer.height = "100%";
        DWObject.SetViewMode(2,2);
      });
      Dynamsoft.DWT.Load();
    }
    
  8. Add a container for controls. There is a scan button, a copy button, a save button and a status bar.

    function showModal(){
      //...
      const controls = document.createElement("div");
      controls.className = "dwt-controls";
      const scanBtn = document.createElement("button");
      scanBtn.innerText = "Scan";
      scanBtn.addEventListener("click", () => {
        scan();
      });
         
      const copyBtn = document.createElement("button");
      copyBtn.innerText = "Copy selected";
      copyBtn.addEventListener("click", () => {
        copy();
      });
    
      const saveBtn = document.createElement("button");
      saveBtn.innerText = "Save";
      saveBtn.addEventListener("click", () => {
        save();
      });
    
      const status = document.createElement("div");
      status.className="dwt-status";
    
      controls.appendChild(scanBtn);
      controls.appendChild(copyBtn);
      controls.appendChild(saveBtn);
      controls.appendChild(status);
    }
       
    function initDWT(){
      console.log("initDWT");
      const status = document.querySelector(".dwt-status"); //add status info
      Dynamsoft.DWT.Containers = [{ ContainerId: 'dwtcontrolContainer',Width: 270, Height: 350 }];
      Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', function () {
        console.log("ready");
        status.innerText = "";  //add status info
        DWObject = Dynamsoft.DWT.GetWebTwain('dwtcontrolContainer');
        DWObject.Viewer.width = "100%";
        DWObject.Viewer.height = "100%";
        DWObject.SetViewMode(2,2);
      });
      status.innerText = "Loading...";  //add status info
      Dynamsoft.DWT.Load();
    }
    

    CSS:

    .dwt-body {
      text-align: center;
      height: calc(100% - 25px);
    }
    
    .dwt-controls {
      text-align: center;
      height: 50px;
    }
    
    #dwtcontrolContainer {
      width: 100%;
      height: calc(100% - 50px);
    }
    

    The scan function scans documents from scanners on desktop devices or cameras on mobile devices:

    function scan(){
      if (DWObject) {
        if (Dynamsoft.Lib.env.bMobile) {
          DWObject.Addon.Camera.scanDocument();
        }else {
          DWObject.SelectSource(function () {
            DWObject.OpenSource();
            DWObject.AcquireImage();
          },
            function () {
              console.log("SelectSource failed!");
            }
          );
        }
      }
    }
    

    The save function saves scanned documents into a PDF file.

    function save(){
      if (DWObject) {
        DWObject.SaveAllAsPDF("Scanned");
      }
    }
    

    The copy function copies the selected image into the clipboard. It uses the Web TWAIN’s built-in API for desktop browsers and the Clipboard API for mobile browsers.

    function copy(){
      if (DWObject) {
        if (Dynamsoft.Lib.env.bMobile) {
          DWObject.ConvertToBlob(
            [DWObject.CurrentImageIndexInBuffer],
            Dynamsoft.DWT.EnumDWT_ImageType.IT_PNG,
            function(result) {
              CopyBlobToClipboard(result);
            },
            function(errorCode,errorString) {
              console.log("convert failed");
              console.log(errorString);
              alert("Failed");
            });
        }else{
          DWObject.CopyToClipboard(DWObject.CurrentImageIndexInBuffer);
          alert("Copied");
        }
      }
    }
    
    function CopyBlobToClipboard(blob) {
      var data = [new ClipboardItem({ "image/png": blob})];
      navigator.clipboard.write(data).then(function() {
        alert("Copied");
      }, function() {
        alert("Failed");
      });
    }
    

All right, we’ve now finished writing the page. We can then adapt it into a Chrome extension.

Adapt the Web Page into a Chrome Extension

  1. Put the JavaScript we write for the page into content.js and the styles into style.css and define them in the manifest as content script. It matches all urls.

    "content_scripts": [
      {
        "matches": ["<all_urls>"],
        "js": ["content.js"],
        "css": ["style.css"]
      }
    ],
    
  2. Declare the Resources as web-accessible resources so that the web page can access them.

    "web_accessible_resources": [
      {
        "resources": ["Resources/*"],
        "matches": ["<all_urls>"]
      }
    ]
    
  3. Modify content.js to load the JavaScript files using the Chrome URL.

    const resourcesURL = new URL(chrome.runtime.getURL("/Resources/"));
    await loadLibrary(resourcesURL+"/dynamsoft.webtwain.initiate.js","text/javascript");
    await loadLibrary(resourcesURL+"/dynamsoft.webtwain.config.js","text/javascript");
    await loadLibrary(resourcesURL+"/addon/dynamsoft.webtwain.addon.camera.js","text/javascript");
    await loadLibrary(resourcesURL+"/addon/dynamsoft.webtwain.addon.pdf.js","text/javascript");
    await loadLibrary(resourcesURL+"/dwt.js","text/javascript");
    

    The reason why we put the scripts accessing Dynamic Web TWAIN’s API into dwt.js instead of directly in the content script is that the content script does not have access to the APIs in the web context.

  4. We also need to set the resources path for Dynamic Web TWAIN using the Chrome URL.

    In content.js, pass the resources Chrome URL to dwt.js via its attributes.

    async function init(){
       //...
       await loadLibrary(resourcesURL+"/dwt.js","text/javascript","dwt",{"resourcesURL":resourcesURL});
    }
    function loadLibrary(src,type,id,data){
      return new Promise(function (resolve, reject) {
        let scriptEle = document.createElement("script");
        scriptEle.setAttribute("type", type);
        scriptEle.setAttribute("src", src);
        if (id) {
          scriptEle.id = id;
        }
        if (data) {
          for (let key in data) {
            scriptEle.setAttribute(key, data[key]);
          }
        }
        document.body.appendChild(scriptEle);
        scriptEle.addEventListener("load", () => {
          console.log(src+" loaded")
          resolve(true);
        });
        scriptEle.addEventListener("error", (ev) => {
          console.log("Error on loading "+src, ev);
          reject(ev);
        });
      });
    }
    

    In dwt.js, use the attribute to set the resources path.

    function load() {
      const resourcesURL = document.getElementById("dwt").getAttribute("resourcesURL");
      Dynamsoft.DWT.ResourcesPath = resourcesURL;
      addButton(resourcesURL);
    }
    
  5. Namespace protection. Since the extension injects scripts into other web pages, we need to isolate our code in dwt.js in an object like the following:

    let DWTChromeExtension = {
      modal:undefined,
      DWObject:undefined,
      load: function(){},
      scan: function(){}
    }
    
  6. Add an options page for users to set the license.

    By default, a one-day public trial license will be used. We can give users the option to use their own license.

    1. Create a new file named options.html.

      <!DOCTYPE html>
      <html>
      <head><title>Options</title></head>
      <body>
      License:
      <input type="text" id="license"/>
      <button id="save">Save</button>
      <div>
        <a href="https://www.dynamsoft.com/customer/license/trialLicense?product=dwt&source=codepool">Apply for a license.</a>
      </div>
      <script src="options.js"></script>
      </body>
      </html>
      
    2. Create a new file named options.js for the JavaScript part. It loads previously set license from the storage or set the license in the storage.

      function save() {
        const license = document.getElementById("license");
        chrome.storage.sync.set({
          license: license.value
        }, function() {
          // Update status to let user know options were saved.
          alert("saved");
        });
      }
      
      function load() {
        // Use default value color = 'red' and likesColor = true.
        chrome.storage.sync.get({
          license: ''
        }, function(items) {
          document.getElementById("license").value = items.license;
        });
      }
      
      document.getElementById("save").addEventListener("click", () => {
        save();
      });
      document.addEventListener('DOMContentLoaded', load);
      
    3. Declare the options page and the storage permission in the manifest.

      "permissions": ["storage"],
      "options_page": "options.html"
      
    4. In content.js, pass the license to dwt.js via its attributes.

      chrome.storage.sync.get({
        license: ''
      }, async function(items) {
        await loadLibrary(resourcesURL+"/dwt.js","text/javascript","dwt",{"resourcesURL":resourcesURL,"license":items.license});
      });
      
    5. In dwt.js, set the license if it is not empty.

      function initDWT(){
        //...
        const license = document.getElementById("dwt").getAttribute("license");
        if (license) {
          Dynamsoft.DWT.ProductKey = license;
        }
      }
      

Source Code

Get the source code of the extension to have a try:

https://github.com/tony-xlh/document-scanner-chrome-extension

References