How to Make a Vue Component-based Document Scanning App

Wrapping an HTML element as a component is a new fashion of front-end development. In this article, we will take a tour of building a component-based document scanning app using Vue.js and Dynamic Web TWAIN. To make our application look modern, we hire Vuetify, one of the most popular libraries, to build our UI.

Note:

This article uses old versions of Vue and Dynamic Web TWAIN. Check out this article for Vue 3 + Dynamic Web TWAIN 18 usage.

Preparation

Beforehand, we should install Dynamic Web TWAIN plus some tools to support our development. Dynamic Web TWAIN can be downloaded from https://www.dynamsoft.com/web-twain/downloads/

Modern web application development requires a Node.js environment and its accompanying package manager: NPM. If you have not installed them yet, referring to https://nodejs.dev/ and follow their tutorial to finalize your preparation work. 

Once you finalized your development environment, let’s install vue-cli to help us create a project with templates: npm install -g @vue/cli

Starting The Project

Vue-CLI makes rapid prototyping easily. We use the command “vue create dwt-vue” to create a project named dwt-vue. Vue-CLI provides presets of initializing a project, or you can select the features manually. 

Once this step has been done, a Vue project is successfully created. 

Next, we are going to import vuetify and Dynamic Web TWAIN:

vue add vuetify
npm install dwt

Finally, run the app:

npm run serve

If you see the information page of Vuetify or Vue.js, you have successfully initialized the project.

Integrating with Dynamic Web TWAIN

Configure The Project

We installed dwt in the previous step. But that is far from ready for development. In fact, Dynamic Web TWAIN requires extra steps to configure it properly.

Firstly, we still have to install an extra package called ncp. It helps us copy files from one folder to another. In your terminal, enter:

npm install ncp

Secondly, we need to modify the command script in package.json. Since Dynamic Web TWAIN relies on the path, we need to ensure the scripts are placed under the proper folder. Open package.json with an editor and make the following changes:

"scripts": {
    "serve": "ncp node _modules/dwt/dist public/resources/dwt && vue-cli-service serve",
    "build": "vue-cli-service build && ncp node _modules/dwt/dist public/resources/dwt",
    "lint": "vue-cli-service lint"
  }

The command “ncp node_modules/dwt/dist public/resources/dwt” tells webpack to copy the resources file to the correct path. Subsequently, make the folder specified in the script:

cd public
mkdir resources
cd resources
mkdir dwt

Apply For a Trial License

Dynamic WebTWAIN is a commercial SDK that features with scanner control API. To utilize it, a 30-day trial license is compulsory. Visit https://www.dynamsoft.com/ and register your account. Then, you would receive your license.

Development

Using Dynamic WebTWAIN

We are now well-prepared for development. It is time to write down the code. 

In src/components, we create a vue template DWT.vue. Then, put the following code in the file:

<template>
  <div id="dwt-container">
  </div>
</template>
 
<script>
import dwt from 'dwt'
 
export default {
  name: 'dwt',
  data () {
    return {
      dwt: {
        obj: null,
        id: 'dwtObject',
        licenseKey: ''  // Your license key here
      }
    }
  },
  mounted () {
    this.mountDWT()
  },
  methods: {
    mountDWT () {
      return new Promise((res, rej) => {
        this.unmountDWT()
          .then(() => {
            dwt.WebTwainEnv.UseLocalService = true;
            dwt.WebTwainEnv.ResourcesPath = "resources/dwt";
            dwt.WebTwainEnv.ProductKey = this.dwt.licenseKey
            dwt.WebTwainEnv.AutoLoad = false;
            dwt.WebTwainEnv.Containers = [];
            dwt.WebTwainEnv.IfAddMD5InUploadHeader = false;
            dwt.WebTwainEnv.IfConfineMaskWithinTheViewer = false;
            let dwtConfig = { WebTwainId: this.dwt.id }
            // By default, use local service is true
            dwt.WebTwainEnv.CreateDWTObjectEx(
                  dwtConfig, 
                  (dwtObject) => { this.dwt.obj = dwtObject; res(true);},
                  (errStr) => { console.log(`failed to initialize dwt, message: ${errStr}`); rej(false);}
            )
          })
      })
    },
    /**
     * Delete dwt instance
     */
    unmountDWT () {
      return new Promise((res, rej) => {
        if (dwt.WebTwainEnv.DeleteDWTObject(this.dwt.id)) {
          res(true)
        } else {
          rej(false)
        }
      })
    }
  }
}
</script>
 
<style scoped>
 
</style> 

Next, import the component you just created into App.vue.

In the

<script>
import HelloWorld from './components/HelloWorld';
import DWT from './components/DWT';
 
export default {
  name: 'App',
 
  components: {
    HelloWorld,
    DWT
  },
 
  data: () => ({
    //
  }),
};
</script> 

In the template section, we add <DWT />:

<v-main>
    <HelloWorld/>
    <DWT />
</v-main>

Save and run the serve command. Afterward, open the browser to check our webpage with the inspect tool. If you have the Vue debugging tool, you would see the instance is created successfully and mounted to the component.

DWT successfully initiated and mouted to DWT instance.

DWT successfully initiated and mouted to DWT instance.

Implementing Scanning Feature

Now. Let’s implement our first feature, which is the essential feature of Dynamic Web TWAIN. We need a button to invoke the scan operation. Meanwhile, we delete the information page of Vuetify or Vue.js. 

Add a button in the template:

<div id="dwt-control">
  <v-btn color="primary" large depressed @click="acquireImage">
    Scan
  </v-btn>
</div>

Add corresponding functions:

<div id="dwt-control">
  <v-btn color="primary" large depressed @click="acquireImage">
    Scan
  </v-btn>
</div>

Add corresponding functions:

acquireImage () {
      const DWObject = this.dwtObj;
      if (DWObject) {
        if (DWObject.UseLocalService) {
          let configure = {
            IfShowUI: this.showUI,
            PixelType: this.colorMode,
            Resolution: this.resolution,
            IfFeederEnabled: this.autoFeeder,
            IfDuplexEnabled: this.duplex,
            IfDisableSourceAfterAcquire: true,
            // Advance settings
            IfGetImageInfo: true,
            IfGetExtImageInfo: true,
            extendedImageInfoQueryLevel: 0
          }
          DWObject.SelectSourceByIndex(this.selectScanner)
          DWObject.AcquireImage(
            configure,
            () => { DWObject.CloseSource() },
            () => { DWObject.CloseSource() }
          )
        }
      }
    }

Functional WebTWAIN app: source selection

Functional WebTWAIN app: source selection

Functional WebTWAIN app:  scanner setting

Functional WebTWAIN app: scanner setting

Binding a Viewer

We can scan the document with a scanner now, but how to preview the result? We need a viewer!

In

<div 
    :id="this.viewer.id" 
    :style="{ width: this.viewer.width, height: this.viewer.height }"
>
    <!-- DWT viewer -->
</div>

In

viewer: {
    id: 'dwtViewer',
    obj: null,
    width: '100%',
    height: '400px'
}

Then, add the initialization script in methods:

bindViewer () {
      if (!this.dwt.obj) {
        alert('WebTwain Object has not been initialized yet.')
      } else {
        const dwtObj = this.dwt.obj
        let viewOptions = {
          width: this.viewer.width,
          height: this.viewer.height,
          view: {
            bShow: true,
            Width: '100%',
          }
        }
        if (dwtObj.BindViewer(this.viewer.id, viewOptions)) {
          this.viewer.obj = this.dwt.obj.Viewer
        }
      }

Finally, make changes in mounted hook:

mounted () {
    this.mountDWT()
      .then(() => {
        this.bindViewer()
      })
  }

We have finished binding the viewer.

Functional WebTWAIN app:  result display

WebTWAIN app: Bind & display result

Making Components

Though the app could work normally when mixing the code into a single file, it could be messy as the demands changed or features added. A better way is to split our view into several pieces of components. The benefit of making components is straightforward. When a file contains hundreds of lines of code, the maintainability decreases. 

We plan two subcomponents so far, according to the features we have.

Vue component-based document scanning app design: Component diagram

Component-based scanning app design: Component diagram

Let’s kick off by creating a folder panelunder components.

Scan Panel

We extend the scan features to make our app do more for our users. Currently, users select the source with a popup selector. However, we need an integrated selector to guarantee the user experience is unique and consistent. Furthermore, users may hope some settings to control the scanned image is the one they want. Therefore, we list out the following features that we should do at the same time.

  1. Scanner sources selector
  2. Resolution selector
  3. Color type selector
  4. ShowUI, Duplex, and AutoFeeder switches.

We create a file (Scan.vue) to contain our scan panel. The code is shown below:

<template>
  <div id="scan-panel">
    <!-- Source selection -->
    <v-select
      v-model="selectScanner"
      :items="scanners"
      item-value="id"
      item-text="text"
      label="Scanner Sources"
      outlined
    ></v-select>
    <!-- Resolution setting -->
    <v-select
      v-model="resolution"
      :items="resolutionOptions"
      item-text="text"
      item-value="val"
      label="Resolution"
      outlined
      return-object
    ></v-select>
    <!-- Color Mode -->
    <v-select
      v-model="colorMode"
      :items="colorModeOptions"
      item-text="text"
      item-value="val"
      label="Color Mode"
      outlined
      return-object
    ></v-select>
    <div class="flex">
      <!-- Show UI Control -->
      <v-switch
        v-model="showUI"
        label="Show UI"
      ></v-switch>
      <!-- Auto Feeder Control -->
      <v-switch
        v-model="autoFeeder"
        label="Auto Feeder"
      ></v-switch>
      <!-- Duplex Control -->
      <v-switch
        v-model="duplex"
        label="Duplex"
      ></v-switch>
    </div>
    <!-- Scan button -->
    <v-btn color="primary" depressed large @click="acquireImage">Scan</v-btn>
  </div>
</template>
 
<script>
export default {
  name: 'scan-panel',
  props: [''],
  data () {
    return {
      dwtObj: null,
      selectScanner: null,
      scanners: [],
      colorMode: { val: 2, text: 'Color' },
      colorModeOptions: [
        { val: 0, text: 'Black & White' }, 
        { val: 1, text: 'Grayscale' },
        { val: 2, text: 'Color' }
      ],
      resolution: { val: 300, text: '300' },
      resolutionOptions: [
        { val: 100, text: '100' }, 
        { val: 150, text:'150' }, 
        { val: 300, text: '300' }
      ],
      showUI: true,
      autoFeeder: false,
      duplex: false
    }
  },
  methods: {
    acquireImage () {
      const DWObject = this.dwtObj;
      if (DWObject) {
        if (DWObject.UseLocalService) {
          let configure = {
            IfShowUI: this.showUI,
            PixelType: this.colorMode,
            Resolution: this.resolution,
            IfFeederEnabled: this.autoFeeder,
            IfDuplexEnabled: this.duplex,
            IfDisableSourceAfterAcquire: true,
            // Advance settings
            IfGetImageInfo: true,
            IfGetExtImageInfo: true,
            extendedImageInfoQueryLevel: 0
          }
          DWObject.SelectSourceByIndex(this.selectScanner)
          DWObject.AcquireImage(
            configure,
            () => { DWObject.CloseSource() },
            () => { DWObject.CloseSource() }
          )
        }
      }
    },
    setupScan (dwtObj) {
      this.dwtObj = dwtObj
      this.scanners = dwtObj.GetSourceNames().map((scanner, idx) => { return { id: idx, text: scanner } })
    }
  }
}
</script>
 
<style scoped>
.flex {
  display: flex;
  justify-content: left;
}
.flex > * {
  margin-right: 12px;;
}
</style>

Viewer Component

We wrap the viewer into an independent component. Create a vue file and name it as `Viewer.vue’. Afterwards, copy the <div> element of the viewer container from DWT, then paste it into the newly created file. It needs extra manipulation to turn into a component. 

In the

  1. Rotate image
  2. Call up a full-featured editor

The updated code is shown as follows:

<template>
  <div id="viewer-container" :style="{ width:width, height: height }">
    <div :id="id" style="height: inherit;"></div>
    <div id="edit-button-group">
      <v-btn outlined tile color="grey darken-1" small @click="rotate(-1)">Rotate Left</v-btn>
      <v-btn outlined tile color="grey darken-1" small @click="rotate(1)">Rotate Right</v-btn>
      <v-btn outlined tile color="grey darken-1" small @click="flip()">Flip</v-btn>
      <v-btn outlined tile color="grey darken-1" small @click="mirror()">Mirror</v-btn>
      <v-btn tile dark depressed small @click="openEditor">Open Editor</v-btn>
    </div>
  </div>
</template>
 
<script>
export default {
  name:'dwt-viewer',
  props: ['id', 'width', 'height', 'dwtRef'],
  data () {
    return {
      dwtObj: null,
      viewerObj: null,  // reference of viewer instance
      currIdx: 0,  // indicator of selected image
    }
  },
  methods: {
    mountViewer (dwtObj) {
      if (!dwtObj) {
        alert('WebTwain Object has not been initialized yet.')
      } else {
        let viewOptions = {
          width: this.width,
          height: this.height,
          view: {
            bShow: true,
            Width: this.width,
            Height: this.height
          }
        }
        if (dwtObj.BindViewer(this.id, viewOptions)) {
          this.dwtObj = dwtObj
          this.viewerObj = this.dwtRef.Viewer
          dwtObj.RegisterEvent('OnMouseClick', (idx) => { this.currIdx = idx })
        }
      }
    },
    rotate (direction) {
      switch (direction) {
        case -1:
        case '-1': {
          this.dwtObj.Rotate(this.currIdx, -90, true);
          break;
        }
        case 1:
        case '1': {
          this.dwtObj.Rotate(this.currIdx, 90, true);
          break;
        }
      }
    },
    flip: function() {
      this.dwtObj.Flip(this.currIdx);
    },
    mirror: function() {
      this.dwtObj.Mirror(this.currIdx);
    },
    openEditor: function () {
      if (this.dwtObj) {
        this.dwtObj.ShowImageEditor()
      }
    }
  }
}
</script>
 
<style scoped>
#viewer-container {
  box-sizing: content-box;
  text-align: center;
  /* width: 100%;
  height: inherit; */
}
#edit-button-group {
  display: inline-block;
}
.inherit-width {
  width: inherit;
}
.inherit-height {
  height: inherit;
}
</style> 

Scan Panel

We extend the scan features to make our app do more for our users. Currently, users select the source with a popup selector. However, we need an integrated selector to guarantee the user experience is unique and consistent. Furthermore, users may hope some settings to control the scanned image is the one they want. Therefore, we list out the following features that we should do at the same time.

We introduce a switch widget to jump between different feature panels. It makes our extending work easier by adding new switch options and corresponding panel components.

Assembling Components Into a View

We have finished the implementation of components. Now we can assemble them altogether. In `DWT.vue’, we completely rewrite the code into the following one:

<template>
  <div id="dwt-container">
    <Viewer 
      :id="this.viewer.id" 
      :ref="this.viewer.id"
      :dwtRef="this.dwt.obj"
      :width="this.viewer.width"
      :height="this.viewer.height"
    >
      <!-- DWT viewer -->
    </Viewer>
    <v-tabs id="dwt-control">
      <v-tab :key="0">Scan</v-tab>
      <v-tab :key="1">Camera Capture</v-tab>
      <v-tab :key="2">OCR</v-tab>
      <v-tab-item :key="0">
        <Scan :ref="'scan-panel'"></Scan>
      </v-tab-item>
    </v-tabs>
  </div>
</template>
 
<script>
import dwt from 'dwt'
import Viewer from '@/components/panel/Viewer'
import Scan from '@/components/panel/Scan'
 
export default {
  name: 'dwt',
  components: {
    Viewer,
    Scan
  },
  data () {
    return {
      dwt: {
        obj: null,
        id: 'dwtObject',
        licenseKey: ''	// Your licenseKey
      },
      viewer: {
        id: 'dwtViewer',
        width: '100%',
        height: '600px'
      }
    }
  },
  mounted () {
    this.mountDWT()
      .then(() => {
        this.initPanels()
        this.bindViewer()
      })
  },
  methods: {
    bindViewer () {
      this.$refs[this.viewer.id].mountViewer(this.dwt.obj)
    },
    mountDWT () {
      return new Promise((res, rej) => {
        this.unmountDWT()
          .then(() => {
            dwt.WebTwainEnv.UseLocalService = true;
            dwt.WebTwainEnv.ResourcesPath = "resources/dwt";
            dwt.WebTwainEnv.ProductKey = this.dwt.licenseKey
            dwt.WebTwainEnv.AutoLoad = false;
            dwt.WebTwainEnv.Containers = [];
            dwt.WebTwainEnv.IfAddMD5InUploadHeader = false;
            dwt.WebTwainEnv.IfConfineMaskWithinTheViewer = false;
            let dwtConfig = { WebTwainId: this.dwt.id }
            // By default, use local service is true
            dwt.WebTwainEnv.CreateDWTObjectEx(
                  dwtConfig, 
                  (dwtObject) => { this.dwt.obj = dwtObject; res(true);},
                  (errStr) => { console.log(`failed to initialize dwt, message: ${errStr}`); rej(false);}
            )
          })
      })
    },
    /**
     * Delete dwt instance
     */
    unmountDWT () {
      return new Promise((res, rej) => {
        if (dwt.WebTwainEnv.DeleteDWTObject(this.dwt.id)) {
          res(true)
        } else {
          rej(false)
        }
      })
    },
    initPanels () {
      this.$refs['scan-panel'].setupScan(this.dwt.obj)
    }
  },
}
</script>
 
<style scoped>
#dwt-container {
  display: flex;
  height: inherit;
  width: inherit;
}
#dwt-control {
  max-width: 400px;
}
</style> 

Implementing More Features

Webcam Capture

We can implement other feature panels in the same way. Now, let us implement webcam capture. We define its layout and expose a method named setupWebcam. In this method, it accepts several arguments. It is free to determine what arguments to pass. Here, we specify sslrotateMode, and dwtObj as the required arguments. The method retrieves the webcam addon from dwtObj, video rotation parameters from rotateMode, and the SSL status from ssl: 

<template>
  <div id="webcam-panel">
    <!-- Source selection -->
    <v-select
      v-model="selectedWebcam"
      :items="webcamSources"
      item-value="val"
      item-text="text"
      label="Webcam Sources"
      outlined
    ></v-select>
    <!-- Resolution setting -->
    <v-select
      v-model="resolution"
      :items="resolutionOptions"
      item-text="text"
      item-value="val"
      label="Resolution"
      outlined
    ></v-select>
    <!-- Framerates -->
    <v-select
      v-model="framerate"
      :items="framerateOptions"
      item-text="text"
      item-value="val"
      label="Frame Rate"
      outlined
    ></v-select>
    <!-- Color Mode -->
    <v-select
      v-model="mediaType"
      :items="mediaTypeOptions"
      item-text="text"
      item-value="val"
      label="Media Type"
      outlined
    ></v-select>
    <!-- Rotation -->
    <v-select
      v-model="rotate"
      :items="rotationOptions"
      item-text="text"
      item-value="val"
      label="Rotation"
      outlined
    ></v-select>
    <!-- Operation button group -->
    <v-btn color="primary" depressed large @click="capture">Capture</v-btn>
    <v-btn depressed large @click="toggleLiveView">Live Viewing {{ liveViewing ? 'OFF':'ON' }}</v-btn>
  </div>
</template>
 
<script>
export default {
  name: 'webcam-panel',
  props: ['ssl', 'rotateMode'],
  data () {
    return {
      dwtObj: null,
      webcamObj: null,
      selectedWebcam: null,
      webcamSources: [],
      resolution: null,
      resolutionOptions: [],
      mediaType: null,
      mediaTypeOptions: [],
      framerate: null,
      framerateOptions: [],
      rotate: 0,
      rotationOptions: [],
      liveViewing: false
    }
  },
  watch: {
    selectedWebcam () {
        this.updateWebcamOptions()
    }
  },
  methods: {
    setupWebcam (ssl, rotateMode, dwtObj) {
      this.dwtObj = dwtObj
      this.webcamObj = dwtObj.Addon.Webcam
      // Set rotate mode
      const rotation = [
        { text: 'OFF', val: rotateMode.VRM_NONE },
        { text: 'CLOCKWISE 90°', val: rotateMode.VRM_90_DEGREES_CLOCKWISE },
        { text: 'CLOCKWISE 180°', val: rotateMode.VRM_180_DEGREES_CLOCKWISE },
        { text: 'CLOCKWISE 270°', val: rotateMode.VRM_270_DEGREES_CLOCKWISE },
        { text: 'FLIP VERTICAL', val: rotateMode.VRM_FLIP_VERTICAL },
        { text: 'FLIP HORIZONTAL', val: rotateMode.VRM_FLIP_HORIZONTAL }
      ]
      this.rotationOptions = rotation
      // Set Default Webcam
      if (ssl) {
        dwtObj.IfSSL = true
        dwtObj.HTTPPort = 443
      }
      this.webcamSources = this.webcamObj.GetSourceList().map(e => { return { text: e, val: e } })
    },
    updateWebcamOptions () {
      this.webcamObj.StopVideo()
      this.webcamObj.SelectSource(this.selectedWebcam)
      
      function ObjToArr(obj) {
        let count = obj.GetCount()
        let arr = []
        for (let i = 0; i < count; i++) {
          arr.push({ text: obj.Get(i), val: obj.Get(i) })
        }
        return arr
      }
 
      new Promise((res) => {
        this.framerateOptions = ObjToArr(this.webcamObj.GetFrameRate())
        this.resolutionOptions = ObjToArr(this.webcamObj.GetResolution())
        this.mediaTypeOptions = ObjToArr(this.webcamObj.GetMediaType())
        res()
      })
      .then(() => {
        this.resolution = this.resolutionOptions[0]
        this.mediaType = this.mediaTypeOptions[0]
        this.framerate = this.framerateOptions[0]
      })
    },
    updateWebcamSetting () {
      if (this.liveViewing) { this.webcamObj.StopVideo() }
      this.webcamObj.SetFrameRate(this.framerate)
      this.webcamObj.SetMediaType(this.mediaType)
      this.webcamObj.SetResolution(this.resolution)
      this.webcamObj.SetVideoRotateMode(this.rotate)
      if (this.liveViewing) { this.webcamObj.PlayVideo() }
    },
    capture () {
      if (this.liveViewing) { this.toggleLiveView(false) }
      this.webcamObj.CaptureImage(()=>{}, (errCode, errStr) => { console.error(`${errCode} - ${errStr}`) })
    },
    toggleLiveView () {
      if (this.liveViewing) { this.setVideoPlayback(false) }
      else { this.setVideoPlayback(true) }
    },
    setVideoPlayback (show) {
      if (show) {
        this.webcamObj.StopVideo()
        setTimeout(
          () => {
            this.webcamObj.PlayVideo(this.dwtObj, 80, () => {})
            this.liveViewing = true
          },
          30
        )
      } else {
        this.webcamObj.StopVideo()
        this.liveViewing = false
      }
    }
  },
}
</script> 

Last step, do not forget to import your newly implemented Webcam Panel to DWT.

OCR

OCR is more complex than Webcam capture since it requires more dependencies to download. However, it is not a difficult problem with the same methodology. We pass the resource path, download the core DLL indicator, dwt object, and dwt library as the setup method’s arguments. Inside the method, we recursively download the core DLL and language pack sequentially. Finally, we configure the OCR object and dwt library to the data field:

<template>
  <div id="ocr-panel">
    <v-btn depressed color="primary" @click="doOCR">Recognize</v-btn>
    <div class="outlined-box">
      <p v-for="(text, idx) in result" :key="idx">{{ text }}</p>
    </div>
  </div>
</template>
 
<script>
export default {
  name: 'ocr-panel',
  data () {
    return {
      ocrObj: null,
      dwtLib: null,
      result: ''
    }
  },
  methods: {
    setupOcr (resPath, downloadDLL, dwtObj, dwtLib) {
      new Promise((res, rej) => {
        const ocrObj = dwtObj.Addon.OCR
        let corePath = resPath + '/addon/OCR.zip'
        let langPath = resPath + '/addon/OCRBasicLanguages/English.zip'
 
        if (downloadDLL) {
          ocrObj.Download(
            corePath, 
            this.setupOcr(resPath, false, dwtObj, dwtLib), 
            (errCode, errStr) => {
              rej({errCode: errCode, errStr: errStr})
            }
          )
        } else {
          ocrObj.DownloadLangData(
            langPath,
            () => { res([ocrObj, dwtLib]); },
            (errCode, errStr) => { 
              rej({errCode: errCode, errStr: errStr})
            }
          )
        }
      })
      .then((ocr) => {
        this.ocrObj = ocr[0]
        this.dwtLib = ocr[1]
      })
    },
    doOCR () {
      this.ocrObj.SetLanguage('eng')
      this.ocrObj.SetOutputFormat(0)
      this.ocrObj.RecognizeSelectedImages(
        (result) => {
          let _textResult = this.dwtLib.base64.decode(result.Get()).split(/\r?\n/g)
          let res = []
           for (let i = 0; i < _textResult.length; i++) {
            if (!_textResult[i].trim()) { continue }
            res.push(_textResult[i].trim() + '\n')
          }
          this.result = res
        },
        (errCode, errStr) => {
          console.error(`${errCode} - ${errStr}`)
        }
      )
    }
  }
}
</script>
 
<style scoped>
.outlined-box {
  border: 1px solid black;
}
</style> 

Now, we have finished the implementation of our application. It is time to test our outcome. Open your terminal and cd into your project folder, then type npm run serve to have a try. If the compiler does not output anything wrong, you can see the following page.

Component-based WebTWAIN document scanning app: Scan Panel

Component-based WebTWAIN application: Scan Panel

Source Code

https://github.com/dynamsoft-dwt/web-twain-vue-document-scanning