How to Build a Barcode and QR Code Scanner with Svelte

Svelte is a web framework like React or Vue. Instead of using techniques like virtual DOM diffing, Svelte writes code that surgically updates the DOM when the state of your app changes.

In this article, we are going to build a barcode and QR code scanner using Svelte. Dynamsoft Camera Enhancer is used to access the camera in browsers and Dynamsoft Barcode Reader is used to read barcodes from the camera video frames.

Getting started with Dynamsoft Barcode Reader

Build a Barcode and QR Code Scanner with Svelte in Steps

Let’s do this in steps.

New Project

Create a new Svelte project named barcode-qr-code-scanner using vite:

npm create vite@latest barcode-qr-code-scanner -- --template svelte

Then, we can run the following to test it:

cd barcode-qr-code-scanner
npm install
npm run dev

Install Dependencies

Install Dynamsoft Barcode Reader and Dynamsoft Camera Enhancer.

npm install dynamsoft-javascript-barcode@9.3.0 dynamsoft-camera-enhancer@3.0.1

Modify App.svelte

By default, the App.svelte contains a counter component. Here, we just remove it and add a start scanning button.

<script>
</script>

<main>
  <div>
    <h1>Svelte Barcode Scanner Demo</h1>
    <button>Start Scanning</button>
  </div>
</main>

<style>
</style>

Create a Scanner Component

Next, let’s create a scanner component under lib/Scanner.svelte that can open the camera and scan barcodes from the camera video frames.

  1. Define the UI element of the scanner and bind the container to cameraContainer.

    <script>
      let cameraContainer;
    </script>
    <div class="container" bind:this={cameraContainer} style="display:none;">
      <svg class="dce-bg-loading" viewBox="0 0 1792 1792"><path d="M1760 896q0 176-68.5 336t-184 275.5-275.5 184-336 68.5-336-68.5-275.5-184-184-275.5-68.5-336q0-213 97-398.5t265-305.5 374-151v228q-221 45-366.5 221t-145.5 406q0 130 51 248.5t136.5 204 204 136.5 248.5 51 248.5-51 204-136.5 136.5-204 51-248.5q0-230-145.5-406t-366.5-221v-228q206 31 374 151t265 305.5 97 398.5z"/></svg>
      <svg class="dce-bg-camera" viewBox="0 0 2048 1792"><path d="M1024 672q119 0 203.5 84.5t84.5 203.5-84.5 203.5-203.5 84.5-203.5-84.5-84.5-203.5 84.5-203.5 203.5-84.5zm704-416q106 0 181 75t75 181v896q0 106-75 181t-181 75h-1408q-106 0-181-75t-75-181v-896q0-106 75-181t181-75h224l51-136q19-49 69.5-84.5t103.5-35.5h512q53 0 103.5 35.5t69.5 84.5l51 136h224zm-704 1152q185 0 316.5-131.5t131.5-316.5-131.5-316.5-316.5-131.5-316.5 131.5-131.5 316.5 131.5 316.5 316.5 131.5z"/></svg>
      <div class="dce-video-container"></div>
      <div class="dce-scanarea">
          <div class="dce-scanlight"></div>
      </div>
      <div class="sel-container">
          <select class="dce-sel-camera"></select>
          <select class="dce-sel-resolution"></select>
      </div>
    </div>
    

    The style is as follows. The camera container will take up the whole screen.

    <style>
      @keyframes dce-rotate{from{transform:rotate(0turn);}to{transform:rotate(1turn);}}
      @keyframes dce-scanlight{from{top:0;}to{top:97%;}}
      .container{width:100%;height:100%;min-width:100px;min-height:100px;background:#eee;position:absolute;left:0;top:0;}
      .dce-bg-loading{animation:1s linear infinite dce-rotate;width:40%;height:40%;position:absolute;margin:auto;left:0;top:0;right:0;bottom:0;fill:#aaa;}
      .dce-bg-camera{display:none;width:40%;height:40%;position:absolute;margin:auto;left:0;top:0;right:0;bottom:0;fill:#aaa;}
      .dce-video-container{position:absolute;left:0;top:0;width:100%;height:100%;}
      .dce-scanarea{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;}
      .dce-scanarea .dce-scanlight{display:none;position:absolute;width:100%;height:3%;border-radius:50%;box-shadow:0px 0px 2vw 1px #00e5ff;background:#fff;animation:3s infinite dce-scanlight;user-select:none;}
      .sel-container{position: absolute;left: 0;top: 0;}
      .sel-container .dce-sel-camera{display:block;}
      .sel-container .dce-sel-resolution{display:block;margin-top:5px;}
      .sel-container {display:block;margin-top:5px;}
    </style>
    
  2. Initialize Barcode Reader and Camera Enhancer when the component is mounted.

    import { onMount } from 'svelte';
    import { CameraEnhancer } from 'dynamsoft-camera-enhancer';
    import { BarcodeReader } from "dynamsoft-javascript-barcode";
       
    BarcodeReader.engineResourcePath = "https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.3.0/dist/";
       
    let enhancer;
    let reader;
       
    onMount(() => {
      init();
    });
       
    async function init(){
      if (!enhancer) {
        BarcodeReader.license = "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="; // trial license
        enhancer = await CameraEnhancer.createInstance();
        reader = await BarcodeReader.createInstance();
        await enhancer.setUIElement(cameraContainer); // the UI element we just defined.
      }
    }
    
  3. Dispatch an initialization event when the initialization is done.

    <script>
      import { createEventDispatcher } from 'svelte';
      const dispatch = createEventDispatcher();
         
      async function init(){
       //initialization code
       dispatch('initialization', true);
     }
    </script>
    
  4. Define functions related to decoding.

    let interval;
    let decoding = false;
    function startDecoding() {
      if (interval) {
        clearInterval(interval);
      }
      decoding = false;
      interval = setInterval(captureAndDecode,100); //set an interval to read barcodes from camera video frames.
    }
    
    function stopDecoding(){
      if (interval) {
        clearInterval(interval);
      }
      decoding = false;
    }
       
    async function captureAndDecode() {
      if (!enhancer || !reader) {
        return
      }
      if (enhancer.isOpen() === false) {
        return;
      }
      if (decoding == true) {
        return;
      }
      let frame = enhancer.getFrame();
      if (frame) {
        decoding = true; // set decoding to true so that the next frame will be skipped if the decoding has not completed.
        let results = await reader.decode(frame);
        decoding = false;
        dispatch('barcodeResults',results); //dispatch an barcodeResults event
      }
    }
    
  5. Add an isActive props and trigger the isActiveUpdated function when it is updated using the $: JS label syntax.

    In the isActiveUpdated function, if isActive is set to false, stop and camera and the decoding, otherwise, start the camera and the decoding.

    export let isActive = false;
    $: isActiveUpdated(isActive);
       
    function isActiveUpdated(newValue) {
      if (enhancer) {
        if (newValue == true) {
          await enhancer.open(true);
          startDecoding();
        }else{
          await enhancer.close(true);
          stopDecoding();
        }
      }
    }
    
  6. When the component is going to be destroyed, stop decoding and release resources.

    onDestroy(()=>{
      stopDecoding();
      enhancer?.dispose(true);
      reader?.destroyContext();
      enhancer = null;
      reader = null;
    });
    

Use the Scanner Component in the App

Switch to App.svelte. Let’s use the scanner component in the app.

  1. Import the scanner component and bind an isActive props to it.

    <script>
      import Scanner from './lib/Scanner.svelte'
      let isActive = false;
      function startScanning(){
        isActive = true;
      }
    </script>
    
    <main>
      <div>
        <h1>Svelte Barcode Scanner Demo</h1>
        <button on:click={startScanning}>Start Scanning</button>
        <div>
          <Scanner isActive={isActive}></Scanner>
        </div>
      </div>
    </main>
    <style>
    </style>
    
  2. Bind the initialization event. Show an Initializing... text if the scanner has not been initialized. If the initialization has been done, display the start scanning button.

    HTML with if expression:

    {#if initialized}
      <button on:click={startScanning}>Start Scanning</button>
    {:else}
      <p>Initializing...</p>
    {/if}
    <div>
      <Scanner isActive={isActive} on:initialization={handleInitialization}></Scanner>
    </div>
    

    JavaScript:

    let initialized = false;
    function handleInitialization(event){
      initialized = event.detail;
    }
    
  3. Bind the barcodeResults event. Stop scanning and display the barcode results if barcodes are found.

    HTML:

    <div>
      <Scanner isActive={isActive} on:barcodeResults={handleBarcodeResults} on:initialization={handleInitialization}></Scanner>
      {#if barcodeResults.length>0}
        <ul>
          {#each barcodeResults as result}
            <li>{result.barcodeFormatString} : {result.barcodeText}</li>
          {/each}
        </ul>
      {/if}
    </div>
    

    JavaScript:

    let barcodeResults = [];
    function handleBarcodeResults(event){
      let results = event.detail;
      if (results.length>0) {
        isActive = false;
        barcodeResults = results;
      }
    }
    

All right, we’ve now finished the barcode and QR code scanner with Svelte. You can check out the online demo to have a try.

Source Code

https://github.com/tony-xlh/svelte-barcode-qr-code-scanner