How to Build Reusable Angular Components for Web Document Scanning

Dynamic Web TWAIN provides JavaScript API for camera and document scanner access, which allows developers to embed document scanning functionality into web applications. This article aims to build reusable Angular components based on Dynamic Web TWAIN SDK for expediting the development of web document scanner application in Angular.

Pre-requisites

  • Node.js
  • Angular CLI v13.3.7

      npm install -g @angular/cli
        
      ng --version
    
      Angular CLI: 13.3.7
      Node: 16.13.1
      Package Manager: npm 8.1.2
      OS: win32 x64
        
      Angular: 13.3.10
      ... animations, common, compiler, compiler-cli, core, forms
      ... platform-browser, platform-browser-dynamic, router
        
      Package                         Version
      ---------------------------------------------------------
      @angular-devkit/architect       0.1303.7
      @angular-devkit/build-angular   13.3.7
      @angular-devkit/core            13.3.7
      @angular-devkit/schematics      13.3.7
      @angular/cli                    13.3.7
      @schematics/angular             13.3.7
      ng-packagr                      13.3.1
      rxjs                            7.5.5
      typescript                      4.6.4
    
  • Dynamic Web TWAIN v17.3.1

      npm i dwt
    

Wrapping Dynamic Web TWAIN SDK with Angular Components

We scaffold a new Angular library project with the Angular CLI:

ng new my-workspace --no-create-application
cd my-workspace
ng generate library ngx-web-document-scanner

The project depends on Dynamic Web TWAIN SDK. Add the following line to projects/ngx-web-document-scanner/package.json:

"peerDependencies": {
    "dwt": "^17.3.1"
},

After that, install Dynamic Web TWAIN SDK as a peer dependency:

npm install

Create three Angular components

Next, generate three Angular components for the library. Every component consists of a TypeScript file, CSS file, and HTML file.

cd src/lib
ng generate component ngx-scanner-capture --skip-import
ng generate component ngx-camera-capture --skip-import
ng generate component ngx-document-scanner --skip-import

ngx-scanner-capture

This component is responsible for acquiring images from a TWAIN-compatible, SANE-compatible or ICA-compatible scanners. To communicate with the scanner, you need to install a local service provided by Dynamsoft on the client machine. Dynamic Web TWAIN JavaScript API will automatically detect the service and pop up the download link if the service is not installed.

The projects/ngx-web-document-scanner/src/lib/ngx-scanner-capture/ngx-scanner-capture.component.html file is the template of the component. We create a select element for listing all the available scanners and three buttons for acquiring documents, loading images from local files, and saving images to local disk.

<div id="scanner-capture" hidden>
    <select *ngIf="useLocalService" id="sources"></select><br />
    <button *ngIf="useLocalService" (click)="acquireImage()">Scan Documents</button>
    <button (click)="openImage()">Load Documents</button>
    <button (click)="downloadDocument()">Download Documents</button>
</div>

In the projects/ngx-web-document-scanner/src/lib/ngx-scanner-capture/ngx-scanner-capture.component.ts file, we firstly import the Dynamic Web TWAIN SDK and initialize the scanner. The useLocalService property is used to determine whether to use the local service. If the value is true, the API requests data from the local service. Otherwise, it invokes camera APIs implemented in the wasm module.

import { Component, EventEmitter, OnInit, Output, Input } from '@angular/core';
import { WebTwain } from 'dwt/dist/types/WebTwain';
import Dynamsoft from 'dwt';

@Component({
  selector: 'ngx-scanner-capture',
  templateUrl: './ngx-scanner-capture.component.html',
  styleUrls: ['./ngx-scanner-capture.component.css'],
})
export class NgxScannerCaptureComponent implements OnInit {
  dwtObject: WebTwain | undefined;
  selectSources: HTMLSelectElement | undefined;
  @Input() containerId = '';
  @Input() useLocalService = false;
  @Input() width = '600px';
  @Input() height = '600px';
  constructor() { 
  }

  ngOnDestroy() {
    Dynamsoft.DWT.Unload();
  }

  ngOnInit(): void {
    Dynamsoft.DWT.Containers = [{ ContainerId: this.containerId, Width: this.width, Height: this.height }];
    Dynamsoft.DWT.UseLocalService = this.useLocalService;
    Dynamsoft.DWT.Load();
    Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', () => { this.onReady(); });
  }

  onReady(): void {
    this.dwtObject = Dynamsoft.DWT.GetWebTwain(this.containerId);
    
    if (!this.useLocalService) {
      this.dwtObject.Viewer.cursor = "pointer";
    } else {
      let sources = this.dwtObject.GetSourceNames();
      this.selectSources = <HTMLSelectElement>document.getElementById("sources");
      this.selectSources.options.length = 0;
      for (let i = 0; i < sources.length; i++) {
        this.selectSources.options.add(new Option(<string>sources[i], i.toString()));
      }
    }

    let elem = document.getElementById('scanner-capture');
    if (elem) elem.hidden = false;
  }
}

Then implement the three button click events:

acquireImage(): void {
  if (!this.dwtObject) return;
  if (!this.useLocalService) {
    alert("Scanning is not supported under the WASM mode!");
  }
  else if (this.dwtObject.SourceCount > 0 && this.selectSources && this.dwtObject.SelectSourceByIndex(this.selectSources.selectedIndex)) {
    const onAcquireImageSuccess = () => { if (this.dwtObject) this.dwtObject.CloseSource(); };
    const onAcquireImageFailure = onAcquireImageSuccess;
    this.dwtObject.OpenSource();
    this.dwtObject.AcquireImage({}, onAcquireImageSuccess, onAcquireImageFailure);
  } else {
    alert("No Source Available!");
  }
}
    
openImage(): void {
  if (!this.dwtObject) return;
  this.dwtObject.IfShowFileDialog = true;
  this.dwtObject.Addon.PDF.SetConvertMode(Dynamsoft.DWT.EnumDWT_ConvertMode.CM_RENDERALL);
  this.dwtObject.LoadImageEx("", Dynamsoft.DWT.EnumDWT_ImageType.IT_ALL,
    () => {
    }, () => {
    });
}

downloadDocument() {
  if (this.dwtObject) {
    this.dwtObject.SaveAsJPEG("document.jpg", this.dwtObject.CurrentImageIndexInBuffer);
  }
}

ngx-camera-capture

This component is responsible for acquiring images from a camera. All camera-relevant APIs are implemented in the wasm module. So there is no need to install a local service.

Similar to the ngx-scanner-capture component, we create a source list and three buttons in the projects/ngx-web-document-scanner/src/lib/ngx-camera-capture/ngx-camera-capture.component.html file.

<div id="camera-capture" hidden>
    <div>
        <label for="videoSource"></label>
        <select id="videoSource"></select><br />
        <button (click)="openCamera()">Open a Camera</button> 
        <button (click)="captureDocument()">Capture Documents</button>
        <button (click)="downloadDocument()">Download Documents</button>
    </div>
</div>

The difference is that camera requires an HTML element to display the video stream. Therefore, we add a @Input() previewId property to the component. The previewId property is used to specify the ID of the HTML element that binds to the video stream.

import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { WebTwain } from 'dwt/dist/types/WebTwain';
import Dynamsoft from 'dwt';

@Component({
  selector: 'ngx-camera-capture',
  templateUrl: './ngx-camera-capture.component.html',
  styleUrls: ['./ngx-camera-capture.component.css']
})
export class NgxCameraCaptureComponent implements OnInit {
  dwtObject: WebTwain | undefined;
  videoSelect: HTMLSelectElement | undefined;
  sourceDict: any = {};
  @Input() containerId = '';
  @Input() useLocalService = true;
  @Input() width = '600px';
  @Input() height = '600px';
  @Input() previewId = '';

  constructor() {
  }

  ngOnDestroy() {
    Dynamsoft.DWT.Unload();
  }

  ngOnInit(): void {
    this.videoSelect = document.querySelector('select#videoSource') as HTMLSelectElement;
    Dynamsoft.DWT.Containers = [{ ContainerId: this.containerId, Width: this.width, Height: this.height }];
    Dynamsoft.DWT.UseLocalService = this.useLocalService;
    Dynamsoft.DWT.Load();
    Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', () => { this.onReady(); });
  }
}

Here are the button click events:

  • openCamera: Open a camera and display the video stream in the HTML element specified by the previewId property.

      openCamera() {
        if (this.videoSelect) {
          let index = this.videoSelect.selectedIndex;
          if (index < 0) return;
    
          var option = this.videoSelect.options[index];
          if (this.dwtObject) {
            this.dwtObject.Addon.Camera.selectSource(this.sourceDict[option.text]).then(camera => {
              if (this.videoSelect) this.createCameraScanner(this.sourceDict[option.text]);
            });
          }
        }
    
      }
    
      async createCameraScanner(deviceId: string): Promise<void> {
        if (this.dwtObject) {
          await this.dwtObject.Addon.Camera.closeVideo();
          await this.dwtObject.Addon.Camera.play(document.getElementById(this.previewId) as HTMLDivElement);
        }
      }
    
  • captureDocument: Capture images from the camera.

      async captureDocument() {
        if (this.dwtObject) {
          await this.dwtObject.Addon.Camera.capture();
        }
      }
    
  • downloadDocument: Download the captured images.

      async downloadDocument() {
        if (this.dwtObject) {
          this.dwtObject.SaveAsJPEG("document.jpg", this.dwtObject.CurrentImageIndexInBuffer);
        }
      }
    

ngx-document-scanner

This component contains camera capture and some advanced image processing features. In contrast to the ngx-camera-capture component, it calls a more advanced method scanDocument() instead of play() when initializing the camera preview.

async createCameraScanner(deviceId: string): Promise<void> {
  if (this.dwtObject) {
    await this.dwtObject.Addon.Camera.closeVideo();
    this.dwtObject.Addon.Camera.scanDocument({
      scannerViewer: {
        deviceId: deviceId,
        fullScreen: true,
        autoDetect: {
          enableAutoDetect: true
        },
        continuousScan: true
      }

    }).then(
      function () { console.log("OK"); },
      function (error: any) { console.log(error.message); });
  }
}

The scanDocument() method launches a document scanner viewer which features document capture, document edge detection, image cropping, perspective correction and image enhancement. You can refer to the API documentation for more details.

Export the components

As the three components are done, we need to declare them in the projects/ngx-web-document-scanner/src/lib/ngx-web-document-scanner.module.ts file.

import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { NgxScannerCaptureComponent } from './ngx-scanner-capture/ngx-scanner-capture.component';
import { NgxCameraCaptureComponent } from './ngx-camera-capture/ngx-camera-capture.component';
import { NgxDocumentScannerComponent } from './ngx-document-scanner/ngx-document-scanner.component';
import { DocumentScannerServiceConfig } from './ngx-web-document-scanner.service';
import { CommonModule } from '@angular/common'; 
@NgModule({
  declarations: [
    NgxScannerCaptureComponent,
    NgxCameraCaptureComponent,
    NgxDocumentScannerComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    NgxScannerCaptureComponent,
    NgxCameraCaptureComponent,
    NgxDocumentScannerComponent
  ]
})

In addition, set the license key and resource path globally in the projects/ngx-web-document-scanner/src/lib/ngx-web-document-scanner.service.ts file.

import { Injectable, Optional } from '@angular/core';
import Dynamsoft from 'dwt';

export class DocumentScannerServiceConfig {
  licenseKey = '';
  resourcePath = '';
}

@Injectable({
  providedIn: 'root'
})

export class NgxDocumentScannerService {

  constructor(@Optional() config?: DocumentScannerServiceConfig) { 
    if (config) { 
      Dynamsoft.DWT.ProductKey = config.licenseKey;
      Dynamsoft.DWT.ResourcesPath = config.resourcePath;
    }
  }
}

The license key of Dynamic Web TWAIN can be obtained from here. The resource path is the path to the folder containing the Dynamic Web TWAIN resources. You also need to configure the resource path in angular.json file.

Publish the package

We can now build and publish the Angular library project to npm:

ng build ngx-web-document-scanner
cd dist/ngx-web-document-scanner
npm publish

NPM Package

https://www.npmjs.com/package/ngx-web-document-scanner

npm i ngx-web-document-scanner

Creating Web Document Scanner Application in Angular

Let’s implement a web document scanner application in Angular within 5 minutes.

  1. Create a new Angular project:

     ng create web-document-scanner
    
  2. Install the ngx-web-document-scanner package:

     npm i ngx-web-document-scanner
    
  3. Create a new component:

     ng generate component document-scanner
    
  4. Inject the NgxDocumentScannerService service in the document-scanner.component.ts file:

     import { Component, OnInit } from '@angular/core';
     import { NgxDocumentScannerService } from 'ngx-web-document-scanner';
    
     @Component({
       selector: 'app-document-scanner',
       templateUrl: './document-scanner.component.html',
       styleUrls: ['./document-scanner.component.css']
     })
     export class DocumentScannerComponent implements OnInit {
    
       constructor(private documentScannerService: NgxDocumentScannerService) { }
    
       ngOnInit(): void {
       }
    
     }
    
  5. Add an HTML Div element as the image container and include the ngx-document-capture component in the document-scanner.component.html file:

     <div id="container">
       <div id="dwtcontrolContainer"></div>
     </div>
    
     <ngx-scanner-capture [useLocalService]="true" [containerId]="'dwtcontrolContainer'"
     [width]="'600px'" [height]="'600px'"></ngx-scanner-capture>
    
  6. Configure the resource path of Dynamic Web TWAIN in the angular.json file:

     "build": {
         "builder": "@angular-devkit/build-angular:browser",
         ...
         "assets": [
             "src/favicon.ico",
             "src/assets",
             {
               "glob": "**/*",
               "input": "./node_modules/dwt/dist",
               "output": "assets/dynamic-web-twain"
             }
           ],
         ...
     }
    
  7. Specify the license key and resource path in the app.module.ts file:

     import { NgxDocumentScannerModule } from 'ngx-web-document-scanner';
    
     @NgModule({
       ...
       imports: [
               BrowserModule,
               AppRoutingModule,
               NgxDocumentScannerModule.forRoot({ 
             licenseKey: "LICENSE-KEY", 
             resourcePath: "assets/dynamic-web-twain"}),
             ],
             ...
     })
    
  8. Launch the web document scanner application:

     ng serve
    

    Angular web document scanner

Try the Online Demo

https://yushulx.me/angular-scanner-camera-capture/

Source Code

https://github.com/yushulx/ngx-web-document-scanner