Typescript and jsQR – Howto read QR codes into Aurelia webapps

Typescript and jsQR – Howto read QR codes into Aurelia webapps

Reading QR codes into your JavaScript webapp and handling the stored data is pretty easy with the Library jsQR. But how to integrate this Javascript Library with an Aurelia webapp using TypeScript? Pretty easy too! We will show you how it is done.


About this demo

We will demonstrate how to integrate jsQR into an Aurelia webapp here. We won’t show you how to setup a complete webapp with TypeScript, node.js, Aurelia and webpack. You can generate a whole new webapp project with the Aurelia CLI on the fly. A description can be found here.

The shown demo webpage displays a camera feed of your device, scans for QR codes, evaluates and stores the data of a found QR code into an input field on the page.

Here is an overview of what the demo page is about.

  • contains an input field for an URL where the user can enter a URL
  • contains a button to enable a QR code scan utilizing the local devices (phone, tablet, laptop) camera
  • the camera / QR code scanner is disabled by default and can be enabled and disabled over the camera button
  • if there is no camera or the access is blocked an warning text is shown
  • if the QR code scanner is running it shows the video feed of the local devices camera
  • a found QR code will be marked with a red square if it contains no URL
  • a found QR code will be marked with a green square if it contains an URL
  • the found URL will be stored in the URL input field
  • (the demo can easily be modified to close the QR code scanner when a valid URL is found)

This all is done with HTML, JavaScrip and CSS in the browser and with Aurelia, node.js, TypeScript, SCSS and jsQR during development.

Install jsQR

To scan QR codes within a TypeScript webapp we can use a framework doing the QR code evaluation like jsQR. You can find a nice introduction how to use it with JavaScript on the GitHub site too. But if your using TypeScript a small example is missing.

To use jsQR install it with npm first in your project.

npm install jsqr --save

After that you can access jsQR in your code.

import jsQR, { QRCode } from 'jsqr'

const code: QRCode | null = jsQR(imageData.data, imageData.width, imageData.height)

We will provide a full example how to integrate jsQR into an TypeScript Aurelia so that you can read and use QR codes in an interactive way.

Add a page for scanning QR codes

If your webapp is up and running you can add a new page where you want to scan QR codes. First add a new HTML page.

demo.html

<template>
  <require from="_controls/qr-code-reader"></require>

  <div id="demo" class="single-column-route-layout narrow">
    <form ref="formRoot" class="route-content">
      <p class="section">
      <div class="section form-layout">
        <qr-code-reader class="full" if.bind="showQrReader" codedata.bind="qrData" validate.call="checkQrCode(data)"></qr-code-reader>

        <label class="wide above">
          URL <input type="text" value.two-way="foundUrl" required>
        </label>
        <div class="narrow no-label-button">
          <button type="button" click.delegate="toggleShowQrReader()"><i class="material-icons">add_a_photo</i></button>
        </div>
      </div>

      <div class="field full button-layout">
        <button click.delegate="onNext()">
          Next
        </button>
      </div>
    </form>
  </div>
</template>

We will integrate the QR code reader as custom element for Aurelia called qr-code-reader. We use a flag showQrReader of the view model class to show or hide the reader. We will use the button with the camera icon to toggle this flag.

codedata.bind="qrData" will bind the qrData member of the view model class to the codedata member of the QR code reader class. Aurelia will handle everything to keep both members in sync.

validate.call="checkQrCode(data)" this will bind a callback function to the QR code reader with Aurelia. We bind the view model checkQrCode(data: string) to the function member validate of the QR code reader. That way the QR code reader can call an external function for every scanned QR code to evaluate the result. We can use this to check if a QR code contains an expected result, stop the QR code reader and start the next processing step.

The view model of the page uses Aurelia to connect to the HTML page elements and looks like that.

demo.ts

import { autoinject } from 'aurelia-dependency-injection'
import { Router } from 'aurelia-router'

@autoinject()
export class Demo {
  public foundUrl: string

  public formRoot: HTMLFormElement

  public showQrReader: boolean = false
  public qrData: string

  constructor (private readonly router: Router) { }

  public async activate (params: any) {
    this.foundUrl = params.url
  }

  public async onNext () {
    if (!this.formRoot.checkValidity()) {
      this.formRoot.reportValidity()

      throw new Error()
    }

    await this.router.navigateToRoute('process', { url: this.foundUrl })
  }

  public toggleShowQrReader () {
    this.showQrReader = !this.showQrReader
  }

  public async checkQrCode (data: string): Promise<boolean> {
    if (data && data.length > 0) {
      // store qr code data
      this.qrData = data
      // evaluate QR code URL
      const pattern = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
      if (pattern.test(this.qrData)) {
        this.foundUrl = this.qrData
        // close QR reader
        // this.toggleShowQrReader()

        return true
      }
    }

    return false
  }
}

showQrReader: boolean will show or hide the complete QR reader part in the page. By default it is hidden. The camera button simply calls toggleShowQrReader () that will change the state in the view model.

qrData: string is used to bind to the QR readers result. It will get the text from a scanned QR code. It is not exposed in the HTML page in any way. This is done by foundUrl: string that is bound by the input field of the page. That way only URLs from QR codes will be shown to the user. If a QR code contains a simple text it is skipped.

checkQrCode (data: string) is taking a text and confirms that it contains an URL. This method is meant to be used by the QR code reader to evaluate the text from a QR code. It should check the content and tell the reader if it is a valid QR code or not. We use it to do a little bit more. The function stores the QR codes text in qrData: string and if the text contains an URL then it stores the URL in foundUrl: string so that it is shown in the URL input field of the demo page. As you can see in the comments you could close and hide the QR code reader here too after finding the first valid URL. That way the user only needs to press the NEXT button to start processing the read in QR code. (Or you can start processing in the checkQrCode function too.)

Setup QR code reader and retrieving data

To complete the solution we have to integrate jSQR in a separate custom element for Aurelia. First the HTML template of the element.

qr-code-reader.html

<template >
  <div>
    <div id="qrErrorMessage" class="section info-box warning"  if.bind="!hasVideoAccess">
      <i class="material-icons icon">warning</i>
      No access to video stream! (Please make shure that the camera is working)
    </div>
    <canvas id="qrReaderCanvas" if.bind="hasVideoAccess"></canvas>
    <div id="qrReaderOutput" if.bind="hasVideoAccess">
      <span class="info-text" if.bind="qrValid">Found valid URL from QR code.</span>
      <span class="info-text" else>No valid URL found.</span>
    </div>
  </div>
</template>

There are some info messages for the user that are shown if we can’t get access to the devices camera or if no valid URL is found in a scanned QR code. The shown messages are controlled with the view model flags hasVideoAccess and qrValid.

The most main part of this template is the canvas qrReaderCanvas. It will show the video stream from the devices camera. The view models functions will control the video stream and camera setup. If there is a QR code in the image of the camera it will be marked with a colorful border to show the user that we have found the QR code.

The setup and handling of the camera and video stream is done in the view model of the QR code reader.

qr-code-reader.ts

import { bindingMode } from 'aurelia-binding'
import { getLogger } from 'aurelia-logging'
import { bindable } from 'aurelia-templating'
import jsQR, { QRCode } from 'jsqr'
import { Point } from 'jsqr/dist/locator'


interface IValidateParams {
  data: string
}

export class QrCodeReader {
  @bindable({ defaultBindingMode: bindingMode.twoWay }) public codedata: string
  @bindable public validate: (params: IValidateParams) => Promise<boolean>

  public hasVideoAccess: boolean = true
  public hasQrCodeFound: boolean = false

  public qrValid: boolean = false

  private canvasElement: HTMLCanvasElement
  private canvasContext: CanvasRenderingContext2D | null
  private video: HTMLVideoElement
  public constructor () {
    // nothing
  }

  public async attached () {
    this.initQrReader()
  }

  public async detached () {
    if (this.video) {
      this.video.pause()
      this.video.src = ''
      // tslint:disable:no-null-keyword
      this.video.srcObject = null
    }
  }

  private initQrReader () {
    this.hasVideoAccess = true
    this.hasQrCodeFound = false

    this.canvasElement = document.getElementById('qrReaderCanvas') as HTMLCanvasElement
    this.canvasContext = this.canvasElement.getContext('2d')

    if (this.canvasContext && navigator.mediaDevices) {
      this.video = document.createElement('video')

      navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
        .then(async (stream: MediaStream) => {
          this.video.srcObject = stream
          // no full screen for iOS
          this.video.setAttribute('playsinline', 'true')
          await this.video.play()
          this.video.playbackRate = 0.5
          requestAnimationFrame(this.drawVideoFrame.bind(this))
        }).catch(async () => {
          this.hasVideoAccess = false
        })
    } else {
      this.hasVideoAccess = false
    }
  }

  private drawLine (begin: Point, end: Point, color: string): void {
    if (this.canvasContext) {
      this.canvasContext.beginPath()
      this.canvasContext.moveTo(begin.x, begin.y)
      this.canvasContext.lineTo(end.x, end.y)
      this.canvasContext.lineWidth = 3
      this.canvasContext.strokeStyle = color
      this.canvasContext.stroke()
    }
  }

  private drawVideoFrame (): void {
    if (!this.canvasContext) {
      // we can't draw video frame
      return
    }

    if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
      // draw video frame from camera and mark found QR code in frame
      this.canvasElement.height = this.video.videoHeight
      this.canvasElement.width = this.video.videoWidth
      this.canvasContext.drawImage(this.video, 0, 0, this.canvasElement.width, this.canvasElement.height)
      const imageData: ImageData = this.canvasContext.getImageData(0, 0,
        this.canvasElement.width, this.canvasElement.height)
      const code: QRCode | null = jsQR(imageData.data, imageData.width, imageData.height)
      if (code) {
        // draw rectangle around found code in video frame
        const color = this.qrValid ? '#409e26' : '#FF3B58'
        this.drawLine(code.location.topLeftCorner, code.location.topRightCorner, color)
        this.drawLine(code.location.topRightCorner, code.location.bottomRightCorner, color)
        this.drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner, color)
        this.drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner, color)
        // QR code found
        this.codedata = code.data
        this.hasQrCodeFound = true

        this.validate({ data: this.codedata }).then(async (result: boolean) => {
          this.qrValid = result
        }).catch(() => undefined)
      } else {
        // no QR code found
        this.hasQrCodeFound = false
        this.codedata = ''
      }
    }
    requestAnimationFrame(this.drawVideoFrame.bind(this))
  }
}

@bindable({ defaultBindingMode: bindingMode.twoWay }) public codedata: string will contain the text content of any found QR code. It is bindable by the out page. We use a two way bind here so that any change to codedata: string will be pushed to the binding page and vi-sa-vie.

@bindable public validate: (params: IValidateParams) => Promise<boolean> contains the callback to the outer page to validate every found QR code. The callback is called with the data from a QR code and the returned boolean flag tells the QR code reader if the data is valid or not. For this binding of callback function to work with Aurelia the we have to use a function parameter named params that is of any type or an object with a member with the name that maps to the parameter name from the binding in the outer page. validate.call="checkQrCode(data)" and interface IValidateParams { data: string } map to each other over the parameter name data. There is no mapping needed for the boolean result value of the callback function.

When the QR code reader is shown the attached () function is called that will init the reader. That includes finding the canvas of the template by its ID qrReaderCanvas and creating a 2D canvas context. The init routine will query the browser for camera access. If the browser can’t provide camera access the flag hasVideoAccess is set to false and a warning message is shown instead of a camera video feed. There might be good reasons we can’t access the camera for example because the device has no camera at all, the site is using no HTTPS access or the user blocked the camera in the browser. We handle this by disabling the QR code reader.

If the user hides or closes the QR code reader the detached () method will be called. In this case any open camera video stream is stopped and closed. Unfortunately this part depends on different browser behavior.

If the camera access is granted the reader creates a video element video: HTMLVideoElement to render the cameras video feed. drawVideoFrame is called frequently to draw video images. The reader grep the images from the camera and draws them in the canvas.

const code: QRCode | null = jsQR(imageData.data, imageData.width, imageData.height) this line of code processes a single camera image with the help of jsQR to look for a QR code in the image. If jsQR identifies a QR code in the given image than a QR code is returned. If not the result is null. That is all to do to integrate jsQR in a working solution.  drawVideoFrame draws a rectangle around a found QR code. The border color depends on the qrValid flag. Green if a valid QR is found or red if it is invalid. The data of the QR code is stored in codedata: string that is bound to the outer page. hasQrCodeFound is set to true if a QR code is found or else to false. This will change the shown message of the template.

The callback to the outer page is triggered with the call this.validate({ data: this.codedata }). We put the QR code data in an object with the parameter name of the bound function call. That way Aurelia calls the checkQrCode(data: string) function from our demo.ts class to determine the valid flag.

Thats about it. Lets try it out!

Using the example

When the user opens the page there is no QR code reader visible. There is only an input field for an URL, a camera button and a NEXT button. If you click on the NEXT button it asks for a URL in the input field.

QR code reader disabled by default

The QR code reader shows up when the user clicks on the camera button. If there is no camera available or access is blocked a warning is shown to the user.

no access to camera

The user is asked by the browser if he or she allows access to the camera by the webapp. If access is granted by the user a video frame is shown above the input field. The page is constantly looking for QR codes in the video. By putting a QR code in front of the camera the page evaluates any found QR code. If the found QR code contains no URL a red border appears around the QR code.

invalid QR code found
QR code with URL found

If the QR code contains an URL a green border is shown around the QR code. The URL is stored in the text input field.

The user can stop the QR code reader by hiding it with a second click on the camera button.

Troubleshooting

  • You need a host with a valid SSL / HTTPS address to access the devices camera with any browser. This is a safety feature by modern browsers to prevent misuse of the local camera. Without HTTPS navigator.mediaDevices is undefined and not accessible. You can test the QR code reader demo without HTTPS by running it localhost. A lot of browsers allow access to the camera when run localhost.

Background vector created by freepik – www.freepik.com

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.