How to Build Reusable Angular Components for Web Document Scanning
Tags: webdev, angular, documentscanning, typescript
Published: 2022-09-16 05:34:12

[Dynamic Web TWAIN](https://www.dynamsoft.com/web-twain/overview/) 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](https://nodejs.org/en/download/)
- Angular CLI v13.3.7

    ```bash
    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](https://www.npmjs.com/package/dwt)
    
    ```bash
    npm i dwt
    ```

## Wrapping Dynamic Web TWAIN SDK with Angular Components
We scaffold a new Angular library project with the Angular CLI:

```bash
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`:

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

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

```bash
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. 

```bash
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. 

```html
<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.

```typescript
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:

```typescript
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. 

```html
<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. 

```typescript
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.

    ```typescript
    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.

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

    ```typescript
    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.

```typescript
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](https://www.dynamsoft.com/web-twain/docs/info/api/Addon_Camera.html#scandocument) 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.

```typescript
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.

```typescript
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](https://www.dynamsoft.com/customer/license/trialLicense?product=dwt). 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](https://www.npmjs.com/):

```bash
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](https://www.npmjs.com/package/ngx-web-document-scanner)

```bash
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:

    ```bash
    ng create web-document-scanner
    ```

2. Install the `ngx-web-document-scanner` package:

    ```bash
    npm i ngx-web-document-scanner
    ```

3. Create a new component:

    ```bash
    ng generate component document-scanner
    ```

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

    ```typescript
    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:

    ```html
    <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:

    ```json
    "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:

    ```typescript
    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:

    ```bash
    ng serve
    ```

    ![Angular web document scanner](https://www.dynamsoft.com/codepool/img/2022/09/angular-document-scanner-sdk.png)

## Try the Online Demo
[https://yushulx.me/angular-scanner-camera-capture/](https://yushulx.me/angular-scanner-camera-capture/)


## Source Code
[https://github.com/yushulx/ngx-web-document-scanner](https://github.com/yushulx/ngx-web-document-scanner)