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.

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 Info.plist:

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

For Android, add the following to AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />

Install dependencies

Install the capacitor plugins to use Dynamsoft Barcode Reader and open the camera.

npm install capacitor-plugin-dynamsoft-barcode-reader capacitor-plugin-camera

Modify the home page and create a scanner page

In the home page, add a button to navigate to the scanner page, a checkbox for configuring 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-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({
  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(['torchOn']);
    const emit = defineEmits(['onScanned','onPlayed']);
    
  3. Add a container in the template.

    <template>
      <div ref="container">
        <div class="dce-video-container"></div>
      </div>
    </template>
    <script lang="ts" setup>
    import { ref } from 'vue';
    const container = ref<HTMLDivElement|undefined>();
    </script>
    
  4. Initialize the plugin and start scanning in the onMounted event:

    <script lang="ts" setup>
    import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
    import { DBR } from "capacitor-plugin-dynamsoft-barcode-reader";
    import { CameraPreview } from "capacitor-plugin-camera";
    import { Capacitor,PluginListenerHandle } from "@capacitor/core";
       
    let onPlayedListener:PluginListenerHandle|undefined;
    onMounted(async () => {
      if (container.value && Capacitor.isNativePlatform() === false) {
        await CameraPreview.setElement(container.value);
      }
      await CameraPreview.initialize();
      await CameraPreview.requestCameraPermission();
      await DBR.initialize();
    
      if (onPlayedListener) {
        onPlayedListener.remove();
      }
      onPlayedListener = await CameraPreview.addListener("onPlayed", async () => {
        startDecoding();
        const orientation = (await CameraPreview.getOrientation()).orientation;
        const resolution = (await CameraPreview.getResolution()).resolution;
        emit("onPlayed",{orientation:orientation,resolution:resolution});
      });
      await CameraPreview.startCamera();
    });
    </script>
    

    Start an interval to read barcodes from the camera if the camera is opened.

    let interval:any;
    let decoding = false;
    
    const startDecoding = () => {
      stopDecoding();
      interval = setInterval(captureAndDecode,100);
    }
    
    const stopDecoding = () => {
      clearInterval(interval);
    }
    
    const captureAndDecode = async () => {
      if (decoding === true) {
        return;
      }
      let results = [];
      let dataURL;
      decoding = true;
      try {
        if (Capacitor.isNativePlatform()) {
          await CameraPreview.saveFrame();
          results = (await DBR.decodeBitmap({})).results;
        }else{
          let frame = await CameraPreview.takeSnapshot({quality:50});
          dataURL = "data:image/jpeg;base64,"+frame.base64;
          results = await readDataURL(dataURL);
        }
        console.log(results);
        emit("onScanned",results);
      } catch (error) {
        console.log(error);
      }
      decoding = false;
    }
    
    const readDataURL = async (dataURL:string) => {
      let response = await DBR.decode({source:dataURL});
      let results = response.results;
      return results;
    }
    
  5. Remove the listeners and stop scanning in the onBeforeUnmount event:

    onBeforeUnmount(async () => {
      if (onPlayedListener) {
        onPlayedListener.remove();
      }
      stopDecoding();
      await CameraPreview.stopCamera();
      console.log("QRCodeScanner unmount");
    });
    
  6. Watch the torchOn props for toggling the torch.

    watch(() => props.torchOn, (newVal, oldVal) => {
      if (newVal === true) {
        CameraPreview.toggleTorch({on:true});
      }else{
        CameraPreview.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 
          @onScanned="onScanned"
          @onPlayed="onPlayed"
        ></QRCodeScanner>
      </ion-page>
    </template>
    
  2. In the script, define onScanned and onPlayed functions.

    export default defineComponent({
      name: 'HomePage',
      components: {
        IonPage,
        QRCodeScanner,
      },
      setup() {
        const sharedStates = states;
           
        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,
          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 = (results:TextResult[]) => {
      if (results.length>0 && scanned === false && sharedStates.continuousScan === false) {
        scanned = true;
        document.documentElement.style.setProperty('--ion-background-color', ionBackground);
        sharedStates.barcodeResults = results;
        router.back();
      }else{
        barcodeResults.value = results;
      }
    }
    
    const onPlayed = (result:{orientation:"LANDSCAPE"|"PORTRAIT",resolution:string}) => {
      const width = result.resolution.split("x")[0];
      const height = result.resolution.split("x")[1];
      let frameWidth = parseInt(width);
      let frameHeight = parseInt(height);
      if (result.orientation === "PORTRAIT") {
        viewBox.value = "0 0 " + frameHeight + " " + frameWidth;
      }else{
        viewBox.value = "0 0 " + frameWidth  + " " + frameHeight;
      }
    }
       
    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;
    }
    

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,
       viewBox,
       barcodeResults,
       getPointsData,
       onScanned,
       onPlayed
      };
    }
    

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