Write a Polyfill for the Barcode Detection Web API

In the previous article, we’ve talked about how to use the Barcode Detection API to create a web QR code scanner. However, not many browsers support the API. So in this article, we are going to write a polyfill for the Barcode Detection API with the JavaScript version of Dynamsoft Barcode Reader so that the API can work on all major browsers.

Writing the Barcode Detection API Polyfill

Let’s do this in steps.

Create a New Project and Make Configurations

  1. Run npm init to create a new project.
  2. Install microbundle as the bundler: npm i -D microbundle.
  3. Install Dynamsoft Barcode Reader: npm i dynamsoft-javascript-barcode.
  4. In the package.json, add the following:

     {
       "type": "module",
       "source": "src/barcode-detector.ts",
       "main": "./dist/barcode-detector.js",
       "exports": {
         "require": "./dist/barcode-detector.cjs",
         "default": "./dist/barcode-detector.modern.js"
       },
       "unpkg": "./dist/barcode-detector.umd.js",
       "amdName": "BarcodeDetectorPolyfill",
       "scripts": {
         "build": "microbundle build --globals dynamsoft-javascript-barcode=Dynamsoft.DBR",
         "dev": "microbundle watch"
       }
     }
    
  5. In src, create barcode-detector.ts as the entry file, BarcodeDetectorDBR.ts which contains the implementation and Definitions.ts for defining types and interfaces.

Define Types and Interfaces

In BarcodeDetectorDBR.ts, define the methods according to the spec:

import { BarcodeDetectorOptions, BarcodeFormat, DetectedBarcode } from "./Definitions";
export default class BarcodeDetector {
    constructor(barcodeDetectorOptions?: BarcodeDetectorOptions);
    static getSupportedFormats(): Promise<BarcodeFormat[]>;
    detect(image: ImageBitmapSource): Promise<DetectedBarcode[]>;
}

In Definitions.ts, define related interfaces and types.

  • Barcode formats

      export type BarcodeFormat
        = "aztec"
        | "code_128"
        | "code_11"
        | "code_39"
        | "code_93"
        | "codabar"
        | "data_matrix"
        | "ean_13"
        | "ean_8"
        | "itf"
        | "pdf417"
        | "qr_code"
        | "micro_qr_code"
        | "maxi_code"
        | "upc_a"
        | "upc_e"
        | "gs1_composite"
        | "gs1_databar"
        | "gs1_databar_expanded"
        | "gs1_databar_expanded_stacked"
        | "gs1_databar_limited"
        | "gs1_databar_omnidirectional"
        | "gs1_databar_stacked"
        | "gs1_databar_stacked_omnidirectional"
        | "gs1_databar_truncated"
        | "unknown"
    

    The spec only specifies 13 barcode formats while Dynamsoft Barcode Reader supports more formats, like Code 11, Maxi Code and GS1 Databar, so some extra barcode format names are defined.

  • BarcodeDetectorOptions

      export interface BarcodeDetectorOptions {
        formats? : Array<BarcodeFormat>
      };
    
  • Point2D

      export interface Point2D {
        x : Number, 
        y : Number
      };
    
  • DetectedBarcode

      export interface DetectedBarcode {
        boundingBox : DOMRectReadOnly,
        rawValue : String,
        format : BarcodeFormat,
        cornerPoints : ReadonlyArray<Point2D>
      };
    

In the entry file barcode-detector.ts, export the above files:

import BarcodeDetector  from './BarcodeDetectorDBR';
export * from './BarcodeDetectorDBR';
export * from './Definitions';
export default BarcodeDetector;

Implement the Barcode Detector

Implement getSupportedFormats

Dynamsoft Barcode Reader has barcode formats enumerations. We need to map them with the barcode format names defined in the spec.

const mapFormat = new Map<BarcodeFormat, EnumBarcodeFormat>([
  [ "aztec", EnumBarcodeFormat.BF_AZTEC ],
  [ "codabar", EnumBarcodeFormat.BF_CODABAR],
  [ "code_11", EnumBarcodeFormat.BF_CODE_11 ],
  [ "code_39", EnumBarcodeFormat.BF_CODE_39 ],
  [ "code_93", EnumBarcodeFormat.BF_CODE_93 ],
  [ "code_128", EnumBarcodeFormat.BF_CODE_128 ],
  [ "data_matrix", EnumBarcodeFormat.BF_DATAMATRIX],
  [ "ean_8", EnumBarcodeFormat.BF_EAN_8 ],
  [ "ean_13", EnumBarcodeFormat.BF_EAN_13 ],
  [ "itf", EnumBarcodeFormat.BF_ITF ],
  [ "pdf417", EnumBarcodeFormat.BF_PDF417 ],
  [ "qr_code", EnumBarcodeFormat.BF_QR_CODE ],
  [ "micro_qr_code", EnumBarcodeFormat.BF_MICRO_QR ],
  [ "gs1_composite", EnumBarcodeFormat.BF_GS1_COMPOSITE],
  [ "gs1_databar", EnumBarcodeFormat.BF_GS1_DATABAR],
  [ "gs1_databar_expanded", EnumBarcodeFormat.BF_GS1_DATABAR_EXPANDED],
  [ "gs1_databar_expanded_stacked", EnumBarcodeFormat.BF_GS1_DATABAR_EXPANDED_STACKED],
  [ "gs1_databar_limited", EnumBarcodeFormat.BF_GS1_DATABAR_LIMITED],
  [ "gs1_databar_omnidirectional", EnumBarcodeFormat.BF_GS1_DATABAR_OMNIDIRECTIONAL],
  [ "gs1_databar_stacked", EnumBarcodeFormat.BF_GS1_DATABAR_STACKED],
  [ "gs1_databar_stacked_omnidirectional", EnumBarcodeFormat.BF_GS1_DATABAR_STACKED_OMNIDIRECTIONAL],
  [ "gs1_databar_truncated", EnumBarcodeFormat.BF_GS1_DATABAR_TRUNCATED],
  [ "maxi_code", EnumBarcodeFormat.BF_MAXICODE ],
  [ "upc_a", EnumBarcodeFormat.BF_UPC_A ],
  [ "upc_e", EnumBarcodeFormat.BF_UPC_E ]
])

const mapFormatInv = new Map<EnumBarcodeFormat, BarcodeFormat>(
  Array.from(mapFormat).map(([key, val]) => [val, key])
)

Then, we can just return the array of BarcodeFormat in the getSupportedFormats method.

const allSupportedFormats : BarcodeFormat[] = Array.from(mapFormat.keys())

export default class BarcodeDetector {
  static async getSupportedFormats() : Promise<BarcodeFormat[]> {
    return allSupportedFormats
  }
}

Implement the Contructor

We need to check supported barcode formats and set barcode formats to use according to the spec in the constructor.

private formats: BarcodeFormat[];
constructor (barcodeDetectorOptions? : BarcodeDetectorOptions) {
    this.formats = barcodeDetectorOptions?.formats ?? allSupportedFormats

    // SPEC: If barcodeDetectorOptions.formats is present and empty, then throw a new TypeError.
    if (this.formats.length === 0) {
      throw new TypeError("")
    }

    // SPEC: If barcodeDetectorOptions.formats is present and contains unknown, then throw a new TypeError.
    if (this.formats.includes("unknown")) {
      throw new TypeError("")
    }
  }

Then, we need to create a BarcodeReader instance of Dynamsoft Barcode Reader.

Here, we add a static init method to do this. If barcode formats are specified, update the BarcodeReader’s runtime settings.

let reader: BarcodeReader;
static async init() : Promise<BarcodeReader> {
  reader = await BarcodeReader.createInstance();
  
  if (this.formats.length != allSupportedFormats.length) {
    console.log("update runtime settings for formats");
    let settings = await reader.getRuntimeSettings();
    let ids:number;
    for (let index = 0; index < this.formats.length; index++) {
      if (index === 0) {
        ids = mapFormat.get(this.formats[index]);
      }else{
        ids = ids || mapFormat.get(this.formats[index]);
      }
    }
    settings.barcodeFormatIds = ids;
    await reader.updateRuntimeSettings(settings);
  }
  
  return reader;
}

Because it requires a license for activation, we need to define methods for setting its license. The license has to be set before the creation of an instance.

static setLicense(license:string) {
  BarcodeReader.license = license;
}
  
static getLicense(license:string) : string {
  return BarcodeReader.license;
}

Implement detect

The detect method in the spec has an ImageBitmapSource argument, while Dynamsoft Barcode Reader can read the following types of data: Blob | Buffer | ArrayBuffer | Uint8Array | Uint8ClampedArray | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | string | DCEFrame | DSImage. Here we just pass it to the reader’s decode method as normally, we only need to decode image elements, canvas elements and video elements and Dynamsoft Barcode Reader supports all of them.

async detect(image : ImageBitmapSource) : Promise<DetectedBarcode[]> {
  if (!reader) {
    throw new Error("Dynamsoft Barcode Reader has not been initialized.");
  }
  let results:TextResult[] = await reader.decode(image as any);
  let detectedBarcodes:DetectedBarcode[] = [];
  results.forEach(result => {
    let detectedBarcode:DetectedBarcode = this.wrapResult(result);
    detectedBarcodes.push(detectedBarcode);
  });
  return detectedBarcodes;
}

As for the returned barcodes array, we need to wrap the TextResult of Dynamsoft Barcode Reader to DetectedBarcode.

wrapResult(result:TextResult):DetectedBarcode{
  const cornerPoints = [];

  let minX: number, minY: number, maxX: number, maxY: number;

  //set initial values
  minX = result.localizationResult.x1;
  minY = result.localizationResult.y1;
  maxX = result.localizationResult.x1;
  maxY = result.localizationResult.y1;
  
  for (let index = 1; index < 5; index++) {
    const x = result.localizationResult["x"+index];
    const y = result.localizationResult["y"+index];

    minX = Math.min(x,minX);
    minY = Math.min(y,minY);
    maxX = Math.max(x,maxX);
    maxY = Math.max(y,maxY);
    let point:Point2D = {x:x,y:y};
    cornerPoints.push(point);
  }

  let boundingBox = new DOMRectReadOnly(minX, minY, maxX - minX, maxY - minY);

  return { 
    boundingBox: boundingBox, 
    rawValue: result.barcodeText,
    format: mapFormatInv.get(result.barcodeFormat),
    cornerPoints
  };
}

Build and Use the Library

Build the Library

Now that we’ve finished the library, we can build it with the following command to use:

npm run build

This will output the following files in dist:

Definitions.d.ts
barcode-detector.cjs
barcode-detector.cjs.map
barcode-detector.d.ts
barcode-detector.esm.js
barcode-detector.esm.js.map
barcode-detector.modern.js
barcode-detector.modern.js.map
barcode-detector.umd.js
barcode-detector.umd.js.map

Use the Library

We can use the library using a <script> tag:

<script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.0.2/dist/dbr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/barcode-detection@latest/dist/barcode-detector.umd.js"></script>

Or use it in a project using webpack or other bundlers:

Install it:

npm install barcode-detection // the package has been published to npmjs

Import it:

import {default as BarcodeDetectorPolyfill} from "barcode-detection"

After that, we can use the polyfill if the browser does not support the Barcode Detection API.

if ("BarcodeDetector" in window) {
  alert('Barcode Detector supported!');
}else{
  alert('Barcode Detector is not supported by this browser, using the Dynamsoft Barcode Reader polyfill.');
  
  //initialize the Dynamsoft Barcode Reader with a license
  BarcodeDetectorPolyfill.setLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==");
  await BarcodeDetectorPolyfill.init();
  window.BarcodeDetector = BarcodeDetectorPolyfill;
  
}
barcodeDetector = new window.BarcodeDetector({ formats: ["qr_code"] });

Source Code

You can check out the polyfill’s source code and online demos to learn more:

https://github.com/xulihang/barcode-detector-polyfill