How to Scan Documents in an Oracle APEX Application

Oracle APEX (also known as APEX or Oracle Application Express) is an enterprise low-code development platform from Oracle Corporation that is used to develop and deploy web applications on Oracle databases. APEX provides a web-based integrated development environment (IDE) that uses wizards, drag-and-drop layout, and property editors to build applications and pages. APEX intends to simplify the process of creating web applications that serve as a frontend to a database server, among other things.1

In this article, we are going to create an Oracle APEX application which has the ability to scan documents using Dynamic Web TWAIN.

You can check out the video to see what it does.

Build an Oracle APEX Application to Scan Documents

Let’s do this in steps.

Start a New Application in App Builder

Open APEX’s online IDE and use its App Builder to create a new app with a new blank page named document scanner.

App Builder

Build an APEX Plugin for Dynamic Web TWAIN

In order to use Dynamic Web TWAIN to scan documents, we need to create an APEX plugin first.

New Plugin

  1. On the app’s page, click shared components. Then click plug-ins in the other components section.
  2. Create a new plugin from scratch. Here, we select the region type as we need to display something and run actions on the page.

    New Plugin

  3. Define the f_render function in the PL/SQL code.

    function f_render (
        p_region              in apex_plugin.t_region,
        p_plugin              in apex_plugin.t_plugin,
        p_is_printer_friendly in boolean
    ) return apex_plugin.t_region_render_result is
    

    Then set Render Procedure/Function Name to F_RENDER in the callbacks section.

Next, we are going to put the plugin aside, try to create a document scanning web page first and then adapt it as a plugin.

Build a Document Scanning App

The web page loads the libraries of Dynamic Web TWAIN via CDN. It can display a modal to scan documents and upload the scanned document image.

  1. Create a new HTML file with the following content.

    <!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>
          img {
            max-height: 200px;
          }
        </style>
        <script src="script.js"></script>
    </head>
    <body>
      <script>
        window.onload = function(){
        }
      </script>
    </body>
    </html>
    
  2. Create a new script.js file. Define a DWTExtension object in it.

    let DWTExtension = {}
    
  3. Add functions to load external styles and JavaScript files.

    let DWTExtension = {
      loadLibrary: function (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);
          });
        });
      },
      loadStyle: function (url) {
        return new Promise(function (resolve, reject) {
          let linkEle = document.createElement('link')
          linkEle.type = 'text/css'
          linkEle.rel = 'stylesheet'
          linkEle.href = url
          let head = document.getElementsByTagName('head')[0]
          head.appendChild(linkEle)
          linkEle.addEventListener("load", () => {
            console.log(url+" loaded")
            resolve(true);
          });
          linkEle.addEventListener("error", (ev) => {
            console.log("Error on loading "+url, ev);
            reject(ev);
          });
        });
      }
    }
    
  4. Add a load function to load needed styles and JavaScript files. It also sets some properties for Dynamic Web TWAIN.

    let DWTExtension = {
      load: async function(pConfig){
        await this.loadLibrary("https://unpkg.com/dwt@18.0.0/dist/dynamsoft.webtwain.min.js","text/javascript");
        await this.loadStyle("https://tony-xlh.github.io/APEX-Dynamic-Web-TWAIN/style.css");
        if (pConfig.license) {
          Dynamsoft.DWT.ProductKey = pConfig.license;
        }
        Dynamsoft.DWT.AutoLoad = false;
        Dynamsoft.DWT.ResourcesPath = "https://unpkg.com/dwt@18.0.0/dist";
      },
    }
    
  5. Add functions to display and hide a modal for scanning. The specific functions for operations are talked about in the following steps.

    let DWTExtension = {
      modal:undefined,
      img:undefined,
      showModal: function(){
        if (('Dynamsoft' in window) === false) {
          alert("Please wait for the loading of DWT.");
          return;
        }
        if (!this.modal) {
          this.modal = document.createElement("div");
          this.modal.className = "dwt-modal";
          document.body.appendChild(this.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", () => {
            this.hideModal();
          });
          const body = document.createElement("div");
          body.className = "dwt-body";
          const viewer = document.createElement("div");
          viewer.id = "dwtcontrolContainer";
          const controls = document.createElement("div");
          controls.className = "dwt-controls";
          const scanBtn = document.createElement("button");
          scanBtn.innerText = "Scan";
          scanBtn.addEventListener("click", () => {
            this.scan();
          });
    
          const editBtn = document.createElement("button");
          editBtn.innerText = "Edit";
          editBtn.addEventListener("click", () => {
            this.edit();
          });
             
          const copyBtn = document.createElement("button");
          copyBtn.innerText = "Copy selected";
          copyBtn.addEventListener("click", () => {
            this.copy();
          });
    
          const useBtn = document.createElement("button");
          useBtn.innerText = "Use selected";
          useBtn.addEventListener("click", () => {
            this.useImage();
          });
         
         
          const saveBtn = document.createElement("button");
          saveBtn.innerText = "Save";
          saveBtn.addEventListener("click", () => {
            this.save();
          });
         
          const status = document.createElement("div");
          status.className="dwt-status";
         
          controls.appendChild(scanBtn);
          controls.appendChild(editBtn);
          controls.appendChild(copyBtn);
          controls.appendChild(useBtn);
          controls.appendChild(saveBtn);
          controls.appendChild(status);
         
          body.appendChild(viewer);
          body.appendChild(controls);
          this.modal.appendChild(header);
          this.modal.appendChild(body);
          if (!this.DWObject) {
            this.initDWT();
          }
        }
        this.modal.style.display = "";
      },
      hideModal: function() {
        this.modal.style.display = "none";
      },
    }
    
  6. Add a function to initialize Dynamic Web TWAIN.

    let DWTExtension = {
      DWObject:undefined,
      initDWT: function(){
        const status = document.querySelector(".dwt-status");
        Dynamsoft.DWT.Containers = [{ ContainerId: 'dwtcontrolContainer',Width: 270, Height: 350 }];
        Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', function () {
          status.innerText = "";
          DWTExtension.DWObject = Dynamsoft.DWT.GetWebTwain('dwtcontrolContainer');
          DWTExtension.DWObject.Viewer.width = "100%";
          DWTExtension.DWObject.Viewer.height = "100%";
          DWTExtension.DWObject.SetViewMode(2,2);
        });
        status.innerText = "Loading...";
        Dynamsoft.DWT.Load();
      }
    }
    
  7. Add a function to scan documents. For desktop devices, it scans documents from physical document scanners. For mobile devices, it uses the cameras.

    let DWTExtension = {
      scan: function(){
        if (this.DWObject) {
          if (Dynamsoft.Lib.env.bMobile) {
            this.DWObject.Addon.Camera.scanDocument();
          }else {
            this.DWObject.SelectSource(function () {
              DWTExtension.DWObject.OpenSource();
              DWTExtension.DWObject.AcquireImage();
            },
              function () {
                console.log("SelectSource failed!");
              }
            );
          }
        }
      }
    }
    
  8. Add a function to edit the documents.

    let DWTExtension = {
      edit: function(){
        if (this.DWObject) {
          let imageEditor = this.DWObject.Viewer.createImageEditor();
          imageEditor.show();
        }
      }
    }
    
  9. Add a function to copy the selected document image into the clipboard.

    let DWTExtension = {
      copy: function(){
        if (this.DWObject) {
          if (Dynamsoft.Lib.env.bMobile) {
            this.DWObject.ConvertToBlob(
              [this.DWObject.CurrentImageIndexInBuffer],
              Dynamsoft.DWT.EnumDWT_ImageType.IT_PNG,
              function(result) {
                DWTExtension.CopyBlobToClipboard(result);
              },
              function(errorCode,errorString) {
                console.log("convert failed");
                console.log(errorString);
                alert("Failed");
              });
          }else{
            this.DWObject.CopyToClipboard(this.DWObject.CurrentImageIndexInBuffer);
            alert("Copied");
          }
        }
      },
      CopyBlobToClipboard: function(blob){
        var data = [new ClipboardItem({ "image/png": blob})];
        navigator.clipboard.write(data).then(function() {
          alert("Copied");
        }, function() {
          alert("Failed");
        });
      }
    }
    
  10. Add a function to save the scanned documents as a PDF file.

    let DWTExtension = {
      save: function () {
        if (this.DWObject) {
          this.DWObject.SaveAllAsPDF("Scanned");
        }
      },
    }
    
  11. Add a function to display the selected image in an img element and upload to a remote server. If it is running as an APEX plugin, append the img element to the plugin’s region. Otherwise, append it to the body. The ConvertToBase64] API is used to convert the selected image to dataURL for display and the HTTPUploadThroughPost is used to upload the image.

    let DWTExtension = {
      host:undefined,
      port:undefined,
      useImage: function() {
        if (!this.img) {
          this.img = document.createElement("img");
          if (this.width) {
            this.img.style.width = this.width;
          }
          if (this.height) {
            this.img.style.height = this.height;
          }
          this.img.style.objectFit = "contain";
          if ('apex' in window) {
            const region = document.getElementById(this.regionID);
            region.appendChild(this.img);
          }else{
            document.body.appendChild(this.img);
          }
        }
        let success = function (result, indices, type) {
          console.log("success");
          const base64 = result.getData(0, result.getLength());
          DWTExtension.img.src = "data:image/jpeg;base64,"+base64;
          DWTExtension.upload();
        };
    
        let error = function (errorCode, errorString) {
          console.log(errorString);
        };
        //1 is B&W, 8 is Gray, 24 is RGB
        if (this.DWObject.GetImageBitDepth(this.DWObject.CurrentImageIndexInBuffer) == 1) {
          this.DWObject.ConvertToGrayScale(this.DWObject.CurrentImageIndexInBuffer);
        }
              
        this.DWObject.ConvertToBase64(
          [this.DWObject.CurrentImageIndexInBuffer],
          Dynamsoft.DWT.EnumDWT_ImageType.IT_JPG,
          success,
          error
        );
      },
      upload: function() {
        const server = this.host;
        const endPoint = "UploadFile"
        this.DWObject.IfSSL = true; // Set whether SSL is used
        this.DWObject.HTTPPort = this.port;
        if (!this.host) {
          return;
        }
        let OnEmptyResponse = function(){
          console.log("empty response");
        }
        let OnServerReturnedSomething = function(errorCode, errorString, sHttpResponse){
          let response = JSON.parse(sHttpResponse);
          if (response.status === "success") {
            DWTExtension.img.setAttribute("data-filename",response["filename"]);
            console.log("Uploaded");
          }
        }
        // Upload the image(s) to the server asynchronously    
        //If the current image is B&W
        //1 is B&W, 8 is Gray, 24 is RGB
        if (this.DWObject.GetImageBitDepth(this.DWObject.CurrentImageIndexInBuffer) == 1) {
          //If so, convert the image to Gray
          this.DWObject.ConvertToGrayScale(this.DWObject.CurrentImageIndexInBuffer);
        }
        //Upload image in JPEG
        this.DWObject.HTTPUploadThroughPost(server, this.DWObject.CurrentImageIndexInBuffer, endPoint, "scanned.jpg", OnEmptyResponse, OnServerReturnedSomething);
      },
    }
    
  12. Create a Python Flask app as the backend HTTP server to receive and serve the scanned documents. In order to access it from APEX via an intranet IP, we have to enable CORS and HTTPS.

    from flask import Flask, request, send_file
    from flask_cors import CORS, cross_origin
    from PIL import Image
    import base64
    from io import BytesIO
    import base64
    import os
    import time
    import json
    app = Flask(__name__, static_url_path='/', static_folder='./')
    cors = CORS(app)
    app.config['CORS_HEADERS'] = 'Content-Type'
    
    @app.route('/UploadFile', methods=['POST','GET'])
    @cross_origin()
    def upload_file():
        if request.method == 'POST':
            f = request.files['RemoteFile']
            path = './uploaded/'
            if os.path.exists(path)==False:
                os.makedirs(path)
            filename = str(int(time.time()*1000))+'.jpg'
            f.save(os.path.join(path,filename))
            response={"status": "success", "filename": filename}
            return json.dumps(response)
        else:
            return ""
    
    @app.route('/Get', methods=['GET'])
    def get():
        filename = request.args.get('filename', '')
        path = os.path.join('./uploaded/',filename)
        if os.path.exists(path):
            return send_file(path,as_attachment=True, attachment_filename=filename)
    
    
    if __name__ == '__main__':
       app.run(host = "0.0.0.0", port = 8888, ssl_context='adhoc')
    
  13. Add the following scripts in the HTML file to start the scanner modal.

    window.onload = async function(){
      await DWTExtension.load({});
      DWTExtension.host = "192.168.8.65";
      DWTExtension.port = "8888";
      DWTExtension.showModal();
    }
    

All right, we have finished writing the web page. Next, we are going to adapt it for the APEX plugin.

Adapt the Web Page for APEX

  1. Add two functions in script.js to get the base64 of the scanned image and get the filename of the uploaded image on the server.

    let DWTExtension = {
      getFilename: function(){
        if (this.img) {
          return this.img.getAttribute("data-filename");
        }else{
          return "";
        }
      },
      getBase64: function(){
        if (this.img) {
          return this.img.src.replace("data:image/jpeg;base64,","");
        } else {
          return "";
        }
      }
    }
    
  2. Add an init function in script.js. It can accept parameters passed from the APEX plugin and create a region with several functions we can call.

    let DWTExtension = {
      init: function(pConfig){
        this.regionID = pConfig.regionID;
        this.width = pConfig.width;
        this.height = pConfig.height;
        this.host = pConfig.host;
        this.port = pConfig.port;
        if ('apex' in window) {
          apex.region.create(
            pConfig.regionID,
            {                
              type: 'Dynamic Web TWAIN',
              getFilename: function(){
                return DWTExtension.getFilename();
              },
              getBase64: function(){
                return DWTExtension.getBase64();
              },
              showModal: function() {
                DWTExtension.showModal();
              },
              hideModal: function() {
                DWTExtension.hideModal();
              }
            }
          );
        }
      },
    }
    
  3. On the setup page of the plugin, add several custom attributes.

    Attributes

    • width: width for the img element.
    • height: height for the img element.
    • host: host of the remote server.
    • port: port of the remote server.
    • license: license for Dynamic Web TWAIN. You can apply for a license here.
  4. Upload the script.js file and add it in the list of file URLs to load.

    Script files

  5. In the PL/SQL code, run the functions for initialization.

    begin
      apex_javascript.add_onload_code (
          p_code => 'DWTExtension.load({' ||
              apex_javascript.add_attribute(p_name => 'license', p_value => p_region.attribute_05, p_add_comma => false ) ||
            '});
            DWTExtension.init({' ||
              apex_javascript.add_attribute(p_name => 'width', p_value => p_region.attribute_01, p_add_comma => true ) ||
              apex_javascript.add_attribute(p_name => 'height', p_value => p_region.attribute_02, p_add_comma => true ) ||
              apex_javascript.add_attribute(p_name => 'host', p_value => p_region.attribute_03, p_add_comma => true ) ||
              apex_javascript.add_attribute(p_name => 'port', p_value => p_region.attribute_04, p_add_comma => true ) ||
              apex_javascript.add_attribute(p_name => 'regionID', p_value => p_region.static_id, p_add_comma => false ) ||
            '});',
          p_key  => null );
      return null;
    end;
    

Use the Plugin to Scan Documents

  1. Open the page designer. Drag the document scanner region into the app and add three buttons. Set the region’s static ID to DWT.

    Page designer

  2. Set the attributes for the region.

    Set attributes

  3. Add a dynamic action for the ShowScanner button. When it is clicked, execute the following JavaScript code:

    apex.region("DWT").showModal();
    
  4. Add a dynamic action for the ShowBase64 button. When it is clicked, execute the following JavaScript code:

    alert(apex.region('DWT').getBase64());
    
  5. Add a dynamic action for the Insert button. When it is clicked, execute the following JavaScript code:

    const filename = apex.region("DWT").getFilename();
    if (!filename) {
      alert("The image has not been uploaded");
    }else{
      apex.server.process("INSERT_IMAGE", {x01:filename}, {dataType: "text", success: function(){alert("Inserted")}});
    }
    

    Then define an ajax callback named INSERT_IMAGE to run the following PL/SQL code to insert a row to the database:

    declare
    
      file_name varchar2(32767);
    
    begin
    
      apex_debug.message('DEBUGMSG: process executed');
    
      file_name := apex_application.g_x01;
      INSERT INTO DOCUMENTS
      (FILENAME)
      VALUES
      (file_name);
    
      apex_debug.message('DEBUGMSG: filename is: %s', file_name);
    
    end;
    

    ajax callback

    A table named DOCUMENTS is created beforehand with a filename column.

Create a Page to Display Scanned Documents

We can create a new page to display the scanned documents.

  1. Add a card to the page.
  2. In the card’s media attribute, enable Advanced Formatting to use the following HTML for display.

    <img src="https://192.168.8.65:8888/Get?filename=&FILENAME!ATTR." alt="&FILENAME!ATTR." style="object-fit:contain;width:128px;max-height:200px;">
    

    Card

  3. Then, we can see the scanned documents listed in the card.

    Scanned documents

Source Code

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

https://github.com/tony-xlh/APEX-Dynamic-Web-TWAIN

References