Ionic Vue QR Code Scanner with Capacitor

In the previous article, we’ve given a brief demonstration on how to use the capacitor-plugin-dynamsoft-barcode-reader to create a cross-platform QR code scanner using Ionic React. In this article, we are going to use Ionic Vue to create such a QR code scanner.

Screenshots of the final results:

Home Scanning Home (with results)

Online demo

Building an Ionic Vue QR Code Scanner

Let’s do this in steps.

New project

Create a new Ionic Vue app:

ionic start QRCodeScanner --type=vue --capacitor

We can start a server to have a live test in the browser:

ionic serve

To run it on Android:

ionic capacitor add android
ionic capacitor copy android // sync files
ionic capacitor run android 

To run it on iOS:

ionic capacitor add ios
ionic capacitor copy ios // sync files
ionic capacitor open ios // use Xcode to open the project

Add camera permission

For iOS, add the following to ios\App\App\Info.plist:

<key>NSCameraUsageDescription</key>
<string>For barcode scanning</string>

Install dependencies

npm install capacitor-plugin-dynamsoft-barcode-reader

Modify the home page and create a scanner page

In the home page, add a button to navigate to the scanner page, two checkboxes for configuring whether to scan QR codes only and whether to enable continuous scanning and display the barcode results if found.

Template of the home page:

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>QR Code Scanner</ion-title>
      </ion-toolbar>
    </ion-header>
    
    <ion-content>
      <ion-button expand="full" v-on:click="gotoScannerPage">Scan Barcodes</ion-button>
      <ion-list>
        <ion-item>
          <ion-label>Continuous Scan</ion-label>
          <ion-checkbox slot="end" v-model="sharedStates.continuousScan"></ion-checkbox>
        </ion-item>
        <ion-item>
          <ion-label>Scan QR Codes Only</ion-label>
          <ion-checkbox slot="end" v-model="sharedStates.QRCodeOnly"></ion-checkbox>
        </ion-item>
        <ion-list-header v-if="sharedStates.barcodeResults.length>0">
          <ion-label>Results:</ion-label>
        </ion-list-header>
        <ion-item v-bind:key="'result'+index" v-for="(result,index) in sharedStates.barcodeResults">
          <ion-label>{{result}}</ion-label>
        </ion-item>
      </ion-list>
    </ion-content>
  </ion-page>
</template>

Script of the home page:

import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonButton, IonList, IonListHeader, IonLabel, IonCheckbox, IonItem, useIonRouter } from '@ionic/vue';
import { defineComponent } from 'vue';
import { states } from '../states'
export default defineComponent({
  name: 'HomePage',
  components: {
    IonContent,
    IonHeader,
    IonPage,
    IonTitle,
    IonToolbar,
    IonButton,
    IonList,
    IonListHeader,
    IonCheckbox,
    IonLabel,
    IonItem
  },
  setup() {
    const router = useIonRouter();
    const sharedStates = states;
    const gotoScannerPage = () => {
      router.push('/scanner');
    }
    return { 
      sharedStates,
      gotoScannerPage
    };
  },
});

Create a states.ts file to share the states between the two pages.

export const states = reactive({
  QRCodeOnly: false,
  continuousScan: false,
  barcodeResults: [] as any[],
});

Create a scanner page under this path: src\views\ScannerPage.vue.

<template>
<ion-page></ion-page>
</template>

<script lang="ts">
import { IonPage, useIonRouter } from '@ionic/vue';
import { defineComponent, onMounted } from 'vue';
import { states } from '../states'
export default defineComponent({
  name: 'HomePage',
  components: {
    IonPage
  },
  setup() {
    const sharedStates = states;
    const router = useIonRouter();
    const goBack = () => {
      sharedStates.barcodeResults = ["1","2","3"]; // dummy results to test the navigation
      router.back();
    }
    onMounted(() => {
      setTimeout(goBack,2000);
    });
  },
});
</script>

<style scoped>
</style>

Configure the router for the scanner page in src/router/index.ts:

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    redirect: '/home'
  },
  {
    path: '/home',
    name: 'Home',
    component: HomePage
  },
+ {
+   path: '/scanner',
+   name: 'Scanner',
+   component: ScannerPage
+ }
]

Create a QR code scanner component

  1. Create a new component in the following path: src\components\QRCodeScanner.vue.

  2. Define props and emits:

    const props = defineProps(['license','dceLicense','torchOn','runtimeSettings']);
    const emit = defineEmits(['onScanned','onPlayed']);
    
  3. Initialize the plugin and start scanning in the onMounted event:

    <script lang="ts" setup>
    const initialized = ref(false);
    let frameReadListener:PluginListenerHandle|undefined;
    let onPlayedListener:PluginListenerHandle|undefined;
    onMounted(async () => {
      let options:Options = {};
      if (props.license) {
        options.license = props.license; // license of Dynamsoft Barcode Reader
      }
      if (props.dceLicense) {
        options.dceLicense = props.dceLicense; // license of Dynamsoft Camera Enhancer
      }
      let result = await DBR.initialize(options);
         
      if (result.success === true) {
        initialized.value = true;
           
        frameReadListener = await DBR.addListener('onFrameRead', (scanResult:ScanResult) => {
          emit("onScanned",scanResult);
        });
    
        onPlayedListener = await DBR.addListener("onPlayed", (result:{resolution:string}) => {
          emit("onPlayed",result.resolution);
        });
    
        if (props.runtimeSettings) {
          console.log("update runtime settings");
          await DBR.initRuntimeSettingsWithString({template:props.runtimeSettings});
        }
    
        await DBR.startScan();
          
      }
    });
    </script>
    
  4. Remove the listeners and stop scanning in the onBeforeUnmount event:

    onBeforeUnmount(() => {
      if (frameReadListener) {
        frameReadListener.remove();
      }
      if (onPlayedListener) {
        onPlayedListener.remove();
      }
      DBR.stopScan();
    });
    
  5. Watch the torchOn props for toggling the torch.

    watch(() => props.torchOn, (newVal, oldVal) => {
      if (newVal === true) {
        DBR.toggleTorch({on:true});
      }else{
        DBR.toggleTorch({on:false});
      }
    });
    

Add the QR code scanner component to the scanner page

  1. Add the QR code scanner component in the template:

    <template>
      <ion-page>
        <QRCodeScanner 
          license="DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="
          :runtimeSettings="runtimeSettings"
          @onScanned="onScanned"
          @onPlayed="onPlayed"
        ></QRCodeScanner>
      </ion-page>
    </template>
    
  2. In the script, define onScanned and onPlayed functions. In addition, if it is set to scan QR codes only, update runtime settings to specify the barcode format to QR code, otherwise, decode all barcode formats.

    export default defineComponent({
      name: 'HomePage',
      components: {
        IonPage,
        QRCodeScanner,
      },
      setup() {
        const sharedStates = states;
        const runtimeSettings = ref('');
    
        if (sharedStates.QRCodeOnly) {
          runtimeSettings.value = "{\"ImageParameter\":{\"BarcodeFormatIds\":[\"BF_QR_CODE\"],\"Description\":\"\",\"Name\":\"Settings\"},\"Version\":\"3.0\"}"; // update the runtime settings with a JSON template
        }else{
          runtimeSettings.value = "{\"ImageParameter\":{\"BarcodeFormatIds\":[\"BF_ALL\"],\"Description\":\"\",\"Name\":\"Settings\"},\"Version\":\"3.0\"}";
        }
           
        const router = useIonRouter();
        let scanned = false;
    
        const onScanned = (result:ScanResult) => {
          if (result.results.length>0 && scanned === false && sharedStates.continuousScan === false) {
            scanned = true;
            sharedStates.barcodeResults = result.results;
            router.back(); // go to home page with barcode results
          }
        }
    
        const onPlayed = (resolution:string) => {
          console.log(resolution);
        }
        
    
        return {
          sharedStates,
          runtimeSettings,
          onScanned,
          onPlayed
        };
      },
    });
    
  3. Update states.ts to use the TextResult type.

    export const states = reactive({
      QRCodeOnly: false,
      continuousScan: false,
    - barcodeResults: [] as any[],
    + barcodeResults: [] as TextResult[],
    });
    
  4. Update the home page to use the TextResult typed result.

     <ion-item v-bind:key="'result'+index" v-for="(result,index) in sharedStates.barcodeResults">
    -  <ion-label> {{result}}</ion-label>
    +  <ion-label> {{result.barcodeFormat+": "+result.barcodeText}}</ion-label>
     </ion-item>
    

Draw QR code overlays

We can use SVG to draw QR code overlays in continuous scanning mode.

  1. Add the following in the template:

    <svg
      :viewBox="viewBox"
      class="overlay"
      v-if="sharedStates.continuousScan"
    >
      <polygon v-bind:key="'polygon'+index" v-for="(barcodeResult,index) in barcodeResults"
        :points="getPointsData(barcodeResult)"
        class="barcode-polygon"
      />
      <text v-bind:key="'text'+index" v-for="(barcodeResult,index) in barcodeResults"
        :x="barcodeResult.x1"
        :y="barcodeResult.y1"
        fill="red"
        font-size="25"
      > {{barcodeResult.barcodeText}}</text>
    </svg>
    
  2. Add the following styles:

    .barcode-polygon {
      fill:rgba(85,240,40,0.5);
      stroke:green;
      stroke-width:1;
    }
    .overlay {
      top: 0;
      left: 0;
      position: absolute;
      width: 100%;
      height: 100%;
      z-index: 998;
    }
    
  3. In the script, define a viewBox property and a barcodeResults property using ref and update them in the onPlayed and onScanned events.

    const viewBox = ref("0 0 1280 720");
    const barcodeResults = ref([] as TextResult[]);
    
    const onScanned = (result:ScanResult) => {
      if (result.results.length>0 && scanned === false && sharedStates.continuousScan === false) {
        scanned = true;
        sharedStates.barcodeResults = result.results;
        router.back();
      }else{
        viewBox.value = "0 0 " + frameWidth  + " " + frameHeight;
        barcodeResults.value = result.results;
      }
    }
    
    const onPlayed = (resolution:string) => {
      const width = resolution.split("x")[0];
      const height = resolution.split("x")[1];
      frameWidth = parseInt(width);
      frameHeight = parseInt(height);
    }
       
    const getPointsData = (tr:TextResult) => {
      let pointsData = tr.x1 + "," + tr.y1 + " ";
      pointsData = pointsData + tr.x2+ "," + tr.y2 + " ";
      pointsData = pointsData + tr.x3+ "," + tr.y3 + " ";
      pointsData = pointsData + tr.x4+ "," + tr.y4;
      return pointsData;
    }
    
  4. For Android and iOS platforms, we also need to handle the rotation. The default orientation of the image sensor is landscape, we need to rotate the localization results according to the device’s orientation and the frame’s orientation.

    1. In QRCodeScanner.vue, rotate the points of the localization results.

      frameReadListener = await DBR.addListener('onFrameRead', (scanResult:ScanResult) => {
        for (let index = 0; index < scanResult.results.length; index++) {
          const result = scanResult.results[index];
          if (scanResult.deviceOrientation && scanResult.frameOrientation) {
            handleRotation(result,scanResult.deviceOrientation,scanResult.frameOrientation);
          }
        }
        emit("onScanned",scanResult);
      });
            
      const handleRotation = (result:any, orientation: string, rotation:number) => {
        let width,height;
        if (orientation == "portrait") {
          width = currentHeight;
          height = currentWidth;
        }else{
          width = currentWidth;
          height = currentHeight;
        }
        for (let i = 1; i < 5; i++) {
          let x = result["x"+i];
          let y = result["y"+i];
          let rotatedX;
          let rotatedY;
                       
          switch (rotation) {
            case 0:
              rotatedX = x;
              rotatedY = y;
              break;
            case 90:
              rotatedX = width - y;
              rotatedY = x;
              break;
            case 180:
              rotatedX = width - x;
              rotatedY = height - y;
              break;
            case 270:
              rotatedX = height - y;
              rotatedY = width - x;
              break;
            default:
              rotatedX = x;
              rotatedY = y;
          }
          result["x"+i] = rotatedX;
          result["y"+i] = rotatedY;
        }
      }
      
    2. In ScannerPage.vue, switch frame width and frame height for viewBox.

      if (result.deviceOrientation === "portrait") {
        viewBox.value = "0 0 " + frameHeight + " " + frameWidth;
      }else{
        viewBox.value = "0 0 " + frameWidth  + " " + frameHeight;
      }
      

Add floating action buttons to the scanner page

We can add floating action buttons to control the scanning.

Here, we add a button to stop scanning and a button to toggle the torch.

  1. Add the following to ScannerPage.vue’s template:

    <ion-fab vertical="bottom" horizontal="start" slot="fixed">
      <ion-fab-button>
        <ion-icon name="ellipsis-horizontal-outline"></ion-icon>
      </ion-fab-button>
      <ion-fab-list side="top">
        <ion-fab-button @click="toggleFlash">
          <ion-icon name="flashlight-outline"></ion-icon>
        </ion-fab-button>
        <ion-fab-button @click="close">
          <ion-icon name="close-outline"></ion-icon>
        </ion-fab-button>
      </ion-fab-list>
    </ion-fab>
    
  2. In the script, add icons, define relevant functions and properties:

    setup() {
      addIcons({
        'ellipsis-horizontal-outline': ellipsisHorizontalOutline,
        'flashlight-outline': flashlightOutline,
        'close-outline': closeOutline,
      });
      const torchOn = ref(false);
         
      const toggleFlash = () => {
        torchOn.value = !torchOn.value;
      }
    
      const close = () => {
        router.back();
      }
         
      return {
       torchOn,
       toggleFlash,
       close,
       sharedStates,
       runtimeSettings,
       viewBox,
       barcodeResults,
       getPointsData,
       onScanned,
       onPlayed
      };
    }
    
  3. We also need to set the app root’s z-index to avoid the scanner UI appended to body blocking the app’s UI. This is for the web platform.

    Here is the modified part of App.vue:

    <template>
      <ion-app style="z-index:100">
        <ion-router-outlet />
      </ion-app>
    </template>
    

All right, we’ve now finished the Ionic Vue QR Code Scanner.

Source Code

Check out the source code to have a try on your own:

https://github.com/xulihang/Ionic-Vue-QR-Code-Scanner