How to Build a NativeScript Plugin for Barcode Reading

In the previous article, we’ve built a NativeScript plugin for camera preview. In this article, we are going to create a NativeScript plugin for Dynamsoft Barcode Reader which can be used for scanning barcodes or QR codes.

Build a Barcode Reader NativeScript Plugin

Let’s do this in steps.

New Plugin Project

Create a plugin project using the plugin workspace seed. Here, we can just use the project we’ve created in the previous article.

Then, we can add a new barcode reader package:

npm run add

Follow the prompts to complete the setup. Here, we use nativescript-dynamsoft-barcode-reader as the package’s name.

Write Definitions

Let’s write the definitions first.

  1. Open index.d.ts to write function definitions.

    The plugin will have the following functions to use Dynamsoft Barcode Reader to read barcodes:

    import { BarcodeReaderCommon } from './common';
    
    export declare class BarcodeReader extends BarcodeReaderCommon {
        initLicense(license:string,licenseListener?:LicenseListener):void;
        initRuntimeSettingsWithString(template:string);
        decodeFrame(frame:any):TextResult[];
        decodeBitmap(bitmap:any):TextResult[];
        decodeBase64(base64:string):TextResult[];
        decodeFile(file:string):TextResult[];
        setCameraEnhancer(dce:any):void;
        startScanning():void;
        stopScanning():void;
        setTextResultListener(listener: TextResultListener);
    }
    
    export * from "./common";
    
  2. Open common.ts to define the interfaces shared by both Android and iOS platforms.

    export interface TextResultListener{
      (results:TextResult[]):void;
    }
    
    export interface LicenseListener{
      (isSuccess:boolean,error:any):void;
    }
    
    export interface TextResult {
      barcodeText:string;
      barcodeFormat:string;
      x1:number;
      x2:number;
      x3:number;
      x4:number;
      y1:number;
      y2:number;
      y3:number;
      y4:number;
    }
    

Android Implementation

Add the Dependency

Create a include.gradle file under platforms\android to add the dependency of Dynamsoft Barcode Reader.

allprojects {
    repositories {
        maven { url "https://download2.dynamsoft.com/maven/aar" }
    }
}

dependencies {
    implementation 'com.dynamsoft:dynamsoftbarcodereader:9.6.0@aar'
}

Generate Typings

  1. Download the aar file of Dynamsoft Barcode Reader from here.
  2. Use the Android d.ts Generator to generate TypeScript definitions for the aar file. Put the generated android.d.ts file under typings.

Implement the Barcode Reader in TypeScript

Open index.android.ts to call the native APIs using TypeScript.

The complete file is like the following:

import { LicenseListener, TextResultListener, BarcodeReaderCommon, TextResult } from './common';

export class BarcodeReader extends BarcodeReaderCommon {
  dbr:com.dynamsoft.dbr.BarcodeReader;
  constructor(){
    super();
    this.dbr = new com.dynamsoft.dbr.BarcodeReader();
  }

  initLicense(license:string,listener?:LicenseListener) {
    com.dynamsoft.dbr.BarcodeReader.initLicense(license,new com.dynamsoft.dbr.DBRLicenseVerificationListener({
        DBRLicenseVerificationCallback: function(isSuccessful:boolean,exception:java.lang.Exception){
          if (listener) {
            listener(isSuccessful, exception);
          }
        }
    }));
  }

  initRuntimeSettingsWithString(template:string) {
    this.dbr.initRuntimeSettingsWithString(template,com.dynamsoft.dbr.EnumConflictMode.CM_OVERWRITE);
  }

  decodeFrame(frame:any):TextResult[] {
    let results = this.dbr.decodeBuffer(frame.getImageData(),frame.getWidth(),frame.getHeight(), frame.getStrides()[0], frame.getPixelFormat());
    return this.wrapResult(results);
  }

  decodeBitmap(bitmap:any):TextResult[] {
    let results = this.dbr.decodeBufferedImage(bitmap);
    return this.wrapResult(results);
  }

  decodeFile(file:string):TextResult[] {
    let results = this.dbr.decodeFile(file)
    return this.wrapResult(results);
  }

  decodeBase64(base64:string):TextResult[] {
    let bitmap = this.base642Bitmap(base64);
    return this.decodeBitmap(bitmap);
  }

  wrapResult(results:androidNative.Array<com.dynamsoft.dbr.TextResult>):TextResult[] {
    let textResults:TextResult[] = [];
    for (let index = 0; index < results.length; index++) {
      const result = results[index];
      let textResult:TextResult = {
        barcodeText:result.barcodeText,
        barcodeFormat:result.barcodeFormatString,
        x1:result.localizationResult.resultPoints[0].x,
        x2:result.localizationResult.resultPoints[1].x,
        x3:result.localizationResult.resultPoints[2].x,
        x4:result.localizationResult.resultPoints[3].x,
        y1:result.localizationResult.resultPoints[0].y,
        y2:result.localizationResult.resultPoints[1].y,
        y3:result.localizationResult.resultPoints[2].y,
        y4:result.localizationResult.resultPoints[3].y
      }
      textResults.push(textResult);
    }
    return textResults;
  }

  base642Bitmap(base64:string):android.graphics.Bitmap {
    let decode = android.util.Base64.decode(base64,android.util.Base64.DEFAULT);
    return android.graphics.BitmapFactory.decodeByteArray(decode,0,decode.length);
  }

  setCameraEnhancer(dce:any) {
    this.dbr.setCameraEnhancer(dce);
  }

  startScanning(){
    this.dbr.startScanning();
  }

  stopScanning(){
    this.dbr.stopScanning();
  }

  setTextResultListener(listener: TextResultListener){
    let pThis = this;
    this.dbr.setTextResultListener(new com.dynamsoft.dbr.TextResultListener({
      textResultCallback: function(id: number, imageData: com.dynamsoft.dbr.ImageData, textResults: androidNative.Array<com.dynamsoft.dbr.TextResult>){
        listener(pThis.wrapResult(textResults));
      }
    }));
  }
}

iOS Implementation

Add the Dependency

Create a Podfile file under platforms\ios to add the Dynamsoft Barcode Reader dependency.

platform :ios, '9.0'
pod 'DynamsoftBarcodeReader','9.6.0'

Generate Typings

Run the following to generate the TypeScript definitions for iOS. Then, copy the objc!DynamsoftBarcodeReader.d.ts to packages\nativescript-dynamsoft-barcode-reader\typings.

cd apps/demo
ns typings ios

Implement the Barcode Reader in TypeScript

Open index.ios.ts to call the native APIs using TypeScript.

The complete file is like the following:

import { LicenseListener, TextResultListener, BarcodeReaderCommon, TextResult } from './common';

@NativeClass()
class LicenseListenerImpl
    extends NSObject // native delegates mostly always extend NSObject
    implements DBRLicenseVerificationListener {
    private callback: (isSuccess: boolean, error: any) => void;
    static ObjCProtocols = [DBRLicenseVerificationListener] // define our native protocalls

    static new(): LicenseListenerImpl {
        return <LicenseListenerImpl>super.new() // calls new() on the NSObject
    }
    
    DBRLicenseVerificationCallbackError(isSuccess: boolean, error: NSError): void {
      if (this.callback) {
        this.callback(isSuccess, error);
      }
    }

    public setCallback(callback: (isSuccess:boolean, error:any) => void): void {
      this.callback = callback;
    }
}

/**
 * "Listener" for text result events
 *
 * @link https://v7.docs.nativescript.org/core-concepts/ios-runtime/how-to/objc-subclassing#typescript-delegate-example
 */
@NativeClass()
class TextResultListenerImpl
    extends NSObject // native delegates mostly always extend NSObject
    implements DBRTextResultListener {

    private callback: TextResultListener;
    private wrapResult: (results:NSArray<iTextResult>)=>TextResult[];
    static ObjCProtocols = [DBRTextResultListener] // define our native protocalls

    static new(): TextResultListenerImpl {
        return <TextResultListenerImpl>super.new() // calls new() on the NSObject
    }
    
    textResultCallbackImageDataResults(frameId: number, imageData: iImageData, results: NSArray<iTextResult> | iTextResult[]): void {
      if (this.callback) {
        // @ts-ignore
        this.callback(this.wrapResult(results));
      }
    }

    public setCallback(callback: TextResultListener,wrapResult:(results:NSArray<iTextResult>)=>TextResult[]) {
      this.callback = callback;
      this.wrapResult = wrapResult;
    }
}

export class BarcodeReader extends BarcodeReaderCommon {
  dbr:DynamsoftBarcodeReader;
  licenseListener:LicenseListenerImpl;
  textResultListener:TextResultListenerImpl;
  constructor(){
    super();
    this.dbr = DynamsoftBarcodeReader.alloc().init();
  }

  initLicense(license:string,listener?:LicenseListener) {
    if (!this.licenseListener) {
      this.licenseListener = LicenseListenerImpl.new();
    }
    if (listener) {
      this.licenseListener.setCallback(listener);
    }
    DynamsoftBarcodeReader.initLicenseVerificationDelegate(license,this.licenseListener);
  }

  initRuntimeSettingsWithString(template:string) {
    this.dbr.initRuntimeSettingsWithStringConflictModeError(template,EnumConflictMode.Overwrite);
  }

  decodeFrame(frame:any):TextResult[] {
    let results = this.dbr.decodeBufferWithWidthHeightStrideFormatError(frame.imageData,frame.width,frame.height, frame.stride, frame.pixelFormat);
    return this.wrapResult(results);
  }

  decodeBitmap(bitmap:any):TextResult[] {
    let results = this.dbr.decodeImageError(bitmap);
    return this.wrapResult(results);
  }

  decodeFile(file:string):TextResult[] {
    let results = this.dbr.decodeFileWithNameError(file)
    return this.wrapResult(results);
  }

  decodeBase64(base64:string):TextResult[] {
    let image = this.base642UIImage(base64);
    return this.decodeBitmap(image);
  }

  wrapResult(results:NSArray<iTextResult>):TextResult[] {
    let textResults:TextResult[] = [];
    if (results) {
      for (let index = 0; index < results.count; index++) {
        const result = results[index];
        let textResult:TextResult = {
          barcodeText:result.barcodeText,
          barcodeFormat:result.barcodeFormatString,
          x1:result.localizationResult.resultPoints[0].x,
          x2:result.localizationResult.resultPoints[1].x,
          x3:result.localizationResult.resultPoints[2].x,
          x4:result.localizationResult.resultPoints[3].x,
          y1:result.localizationResult.resultPoints[0].y,
          y2:result.localizationResult.resultPoints[1].y,
          y3:result.localizationResult.resultPoints[2].y,
          y4:result.localizationResult.resultPoints[3].y
        }
        textResults.push(textResult);
      }
    }
    return textResults;
  }

  base642UIImage(base64:string):UIImage{
    let data = NSData.alloc().initWithBase64EncodedStringOptions(base64,NSDataBase64DecodingOptions.IgnoreUnknownCharacters);
    let image = UIImage.alloc().initWithData(data);
    return image;
  }

  setCameraEnhancer(dce:any) {
    this.dbr.setCameraEnhancer(dce);
  }

  startScanning(){
    this.dbr.startScanning();
  }

  stopScanning(){
    this.dbr.stopScanning();
  }

  setTextResultListener(listener: TextResultListener){
    if (!this.textResultListener) {
      this.textResultListener = TextResultListenerImpl.new();
    }
    this.textResultListener.setCallback(listener,this.wrapResult);
    this.dbr.setDBRTextResultListener(this.textResultListener)
  }
}

Update the Demo to Use the Plugin

Let’s update the plain TypeScript demo to use the plugins to create a barcode scanning demo.

  1. Open apps/demo/src/plugin-demos/nativescript-dynamsoft-barcode-reader.xml to add the camera preview view which is provided by the Dynamsoft Camera Enhancer plugin. Here we use the GridLayout to make the view contain the whole space and add several buttons to test the functions.

    <Page xmlns="http://schemas.nativescript.org/tns.xsd" 
     xmlns:dce="nativescript-dynamsoft-camera-enhancer"
     navigatingTo="navigatingTo" class="page">
        <GridLayout rows="*, auto, auto">
            <dce:CameraEnhancer 
            loaded="" 
            rowSpan="3" 
            active=""
            cameraID=""
            torch=""></dce:CameraEnhancer>
            <Label rowSpan="3" verticalAlignment="top" textAlignment="center" text="" textWrap="true"/>
            <StackLayout row="1">
                <Button class="btn btn-primary" text="Init License" tap=""></Button>
                <Button class="btn btn-primary" text="Switch Torch" tap=""></Button>
                <Button class="btn btn-primary" text="Switch Camera" tap=""></Button>
                <Button class="btn btn-primary" text="Decode Frame" tap=""></Button>
                <Button class="btn btn-primary" text="" tap=""></Button>
            </StackLayout>
        </GridLayout>
    </Page>
    
  2. Open apps/demo/src/plugin-demos/nativescript-dynamsoft-barcode-reader.ts to add relevant functions.

    1. Define props used by the views.

      isActive: boolean = true;
      desiredTorchStatus:boolean = false;
      desiredCamera:string = "";
      liveButtonText:string = "Turn on Live Detection";
      barcodeText:string = "";
      
    2. Initialize Dynamsoft Barcode Reader in the constructor. We also have to set a license to use it. You can apply for a license here.

      dbr:BarcodeReader;
      constructor(){
        super();
        this.dbr = new BarcodeReader();
      }
            
      initLicense(){
        const listener:LicenseListener = function (isSuccess:boolean,error:any) {
          console.log("License initialization result: "+isSuccess);
        }
        this.dbr.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==", listener); //one-day public trial
      }
      
    3. When the camera view is loaded, get the instance of the camera enhancer and bind it to Dynamsoft Barcode Reader. We can get the barcodes’ text results in the text result listener after we call startScanning.

      dce:CameraEnhancer;
      barcodes:TextResult[] = [];
      dceLoaded(args: EventData) {
        this.dce = <CameraEnhancer>args.object;
        this.dbr.setCameraEnhancer(this.dce.getCameraEnhancer());
        let pThis = this;
        this.dbr.setTextResultListener(function (textResults:TextResult[]) {
          pThis.barcodes = textResults;
        })
      }
      
    4. Add a function for toggling live detection of barcodes. The results will be displayed in a label.

      interval:any;
      liveOn:boolean = false;
      toggleLiveDetection(){
        if (this.liveOn === false) {
          this.liveOn = true;
          this.set("liveButtonText","Turn off Live Detection");
          this.dbr.startScanning();
          const rerender = async () => {
            console.log("rerender");
            let barcodes = "Found "+this.barcodes.length+" barcode(s).\n";
            this.barcodes.forEach(textResult => {
              barcodes = barcodes + textResult.barcodeFormat + ": " + textResult.barcodeText + "\n";
            });
            this.set("barcodeText",barcodes);
          }
          console.log("set interval");
          this.interval = setInterval(rerender,200);
        }else{
          this.liveOn = false;
          this.set("liveButtonText","Turn on Live Detection");
          this.dbr.stopScanning();
          if (this.interval) {
            console.log("clear interval");
            clearInterval(this.interval);
          }
        }
      }
      
    5. Handle the lifecycle events for Android. Close the camera and stop scanning when the app is paused. Start the camera and start scanning when the app is resumed.

      constructor(){
        super();
        this.dbr = new BarcodeReader();
        this.registerLifeCycleEvents();
      }
            
      registerLifeCycleEvents(){
        if (global.isAndroid) {
          let pThis = this;
          Application.android.on(AndroidApplication.activityPausedEvent, function (args: AndroidActivityBundleEventData) {
            console.log("paused");
            if (pThis.dbr && pThis.liveOn) {
              console.log("stop scanning");
              pThis.dbr.stopScanning();
            }
            if (pThis.dce && pThis.isActive) {
              console.log("close camera");
              pThis.dce.close();
            }
          });
            
          Application.android.on(AndroidApplication.activityResumedEvent, function (args: AndroidActivityBundleEventData) {
            console.log("resumed");
            if (pThis.dce && pThis.isActive === true) {
              console.log("restart camera");
              pThis.dce.open();
            }
            if (pThis.dbr && pThis.liveOn) {
              console.log("start scanning");
              pThis.dbr.startScanning();
            }
          });
        }
      }
      
    6. Add a function for capturing a frame and reading barcodes from it.

      async onDecodeFrame(args: EventData){
        let frame = this.dce.captureFrame();
        console.log(frame);
        let textResults:TextResult[] = this.dbr.decodeFrame(frame);
        console.log(textResults);
        let barcodes = "Found "+textResults.length+" barcode(s).\n";
        textResults.forEach(textResult => {
          barcodes = barcodes + textResult.barcodeFormat + ": " + textResult.barcodeText + "\n";
        });
        alert(barcodes);
      }
      
    7. Add functions to control the camera.

      onSwitchCamera(args: EventData) {
        if (this.dce) {
          if (this.liveOn) {
            this.toggleLiveDetection();
          }
          if (!this.cameras) {
            this.cameras = this.dce.getAllCameras();
          }
          const selectedCamera = this.dce.getSelectedCamera();
          let nextIndex = this.cameras.indexOf(selectedCamera) + 1;
          if (nextIndex >= this.cameras.length) {
            nextIndex = 0;
          }
          const nextCamera = this.cameras[nextIndex];
          if (nextCamera != selectedCamera) {
            this.set("desiredCamera",nextCamera);
          }
        }
      }
      
      onSwitchTorch(args: EventData) {
        this.set("desiredTorchStatus",!this.desiredTorchStatus);
      }
      

All right, we’ve finished writing the plugin and the demo. We can run npm run start to run the app on Android or iOS devices for a test.

Android Screenshot:

Android barcode reader

Notes about Decoding without Blocking the UI

In the above example, we use the text result listener to get the barcode results. This will start capturing frames from Dynamsoft Camera Enhancer and then use Dynamsoft Barcode Reader to read barcodes. The decoding runs on a background thread so it won’t block the UI.

If we write the frame capturing and barcode reading in TypeScript, the code will be executed on the main thread on Android, which will block the UI for a while. This will not happen on iOS as the NativeScript iOS runtime can handle such a situation.

Source Code

https://github.com/tony-xlh/nativescript-dynamsoft-capture-vision