文档扫描与图像处理

如何在uni-app中集成Dynamsoft Barcode Reader进行扫码

uni-app是一个使用Vue.js开发前端应用的框架,编写的应用可以在Android、iOS、Web以及各种小程序上运行。

Dynamsoft Barcode Reader是使用C++编写的高性能二维码/条码扫描SDK,可以在Android、iOS和Web等各种平台运行。

下面我们会讨论如何在uni-app中集成Dynamsoft Barcode Reader来进行扫码。

针对不同平台,在uni-app里集成Dynamsoft Barcode Reader有多种方式。

  • Web:可以直接使用Dynamsoft Barcode Reader的JavaScript版
  • 原生应用:编写原生语言插件或者UTS插件使用Dynamsoft Barcode Reader的Android版和iOS版,或者在WebView中使用JavaScript版。本文会使用WebView
  • 小程序:使用相机插件打开相机,传送视频帧给服务端解码

在线demo

集成到Web

安装依赖

新建一个uni-app后,我们需要初始化npm项目以导入第三方库:

npm init -y

之后安装Dynamsoft Barcode Reader:

npm install dynamsoft-javascript-barcode

建立一个二维码扫描组件

  1. 新建一个components目录,在里面建立一个新的组件,命名为QRCodeScannerWeb.vue

  2. 在template中添加扫描界面,包含相机视频容器、相机选择器、分辨率选择器等元素。

    <div ref="elRefs" class="component-barcode-scanner">
      <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"></path>
      </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">
        </path>
      </svg>
      <div class="dce-video-container"></div>
      <div class="dce-scanarea">
        <div class="dce-scanlight"></div>
      </div>
      <div class="div-select-container">
        <select class="dce-sel-camera"></select>
        <select class="dce-sel-resolution"></select>
      </div>
    </div>
    
  3. onMounted生命周期中,初始化Dynamsoft Barcode Reader并开启相机进行扫码。如果完成一帧的处理,就触发scanned的事件,把扫码结果传递给父组件。这里定义了一个license属性用于激活Dynamsoft Barcode Reader。可以访问这里申请一个license。

    props: ['license'],
    setup(props,context){
      const pScanner = ref(null);
      const elRefs = ref(null);
      onMounted(async () => {
        try {
          if (BarcodeScanner.isWasmLoaded() === false) {
            BarcodeScanner.license = props.license ?? "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==";
            BarcodeScanner.engineResourcePath = "https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.6.21/dist/";
          }
          const scanner = await (pScanner.value = BarcodeScanner.createInstance());
          await scanner.setUIElement(elRefs.value);
          scanner.onFrameRead = (results) => {
            for (let result of results) {
              console.log(result.barcodeText);
            }
            context.emit("scanned", results);
          };
          await scanner.open();
        } catch (ex) {
          let errMsg = ex.message||ex;
          if (errMsg.includes("network connection error")) {
            errMsg = "Failed to connect to Dynamsoft License Server: network connection error. Check your Internet connection or contact Dynamsoft Support (support@dynamsoft.com) to acquire an offline license.";
          }
          alert(errMsg);
        }
      });
      onBeforeUnmount(async () => {
        if (pScanner.value) {
          (await pScanner.value).destroyContext();
          console.log('BarcodeScanner Component Unmount');
        }
      });
      return {
        elRefs
      }
    }
    
  4. 组件卸载时,销毁Dynamsoft Barcode Reader。

    onBeforeUnmount(async () => {
      if (pScanner.value) {
        (await pScanner.value).destroyContext();
        console.log('BarcodeScanner Component Unmount');
      }
    });
    
  5. index.vue中,使用这个扫码组件,扫到码后关闭扫描界面并显示结果。

    <template>
      <view class="content">
        <button @click="startScan">Start Scanning</button>
        <view class="fullscreen" v-if="scanning">
          <QRCodeScannerWeb @scanned="scanned" license="DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="></QRCodeScannerWeb>
        </view>
        <view v-for="(result,index) in barcodeResults">
          <view>{{ (index+1) + ". " + result.barcodeFormatString + ": " + result.barcodeText }}</view>
        </view>
      </view>
    </template>
    
    <script>
      import QRCodeScannerWeb from "../../components/QRCodeScannerWeb.vue";
      import { ref } from "vue";
      export default {
        components: {
          QRCodeScannerWeb
        },
        setup(){
          const scanning = ref(false);
          const barcodeResults = ref([]);
          const startScan = () => {
            scanning.value = true;
          }
          const scanned = (results) => {
            if (results.length>0) {
              barcodeResults.value = results;
              scanning.value = false;
            }
          }
          return {
            startScan,
            scanned,
            barcodeResults,
            scanning
          }
        },
        data() {
          return {
          }
        },
        onLoad() {
    
        },
        methods: {
    
        }
      }
    </script>
    
    <style>
      .content {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
      }
    
      .fullscreen {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
      }
    </style>
    

集成到原生应用

我们需要编写一个网页应用,在WebView中运行,通过postMessage接口和WebView通讯,传递扫描结果。

编写一个网页扫码应用

  1. 新建一个html文件。

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta
          name="viewport"
          content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
        />
        <title>QR Code Scanner in uniapp</title>
        <style>
        </style>
      </head>
      <body>
        </script>
      </body>
    </html>
    
  2. 在head中包含Dynamsoft Barcode Reader和Dynamsoft Camera Enhancer的库。

    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@3.3.4/dist/dce.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.6.20/dist/dbr.js"></script>
    
  3. 添加一个触发扫描的按钮、扫描界面的容器和一个状态条。

    <div class="app">
      <div class="controls">
        <button class="scanButton" onclick="startScan();">Scan From Camera</button>
      </div>
      <div class="status"></div>
    </div>
    <div class="scanner">
    </div>
    
  4. 在页面加载后,如果按下了扫描的按钮或者startScan的URL参数为true,用Dynamsoft Camera Enhancer打开相机,之后用Dynamsoft Barcode Reader处理视频帧解码。

    let enhancer;
    let reader;
    let interval;
    let processing = false;
    let hasCamera = true;
    init();
    async function init(){
      hideControls();
      updateStatus("Initializing...");
      try {
        await requestCameraPermission();
      }catch (e) {
        console.log(e);
        hasCamera = false;
        document.getElementsByClassName("scanButton")[0].style.display = "none";
        alert("No camera detected.");
      }
         
      if (getUrlParam("startScan") !== "true") {
        revealControls();
      }
         
      if (getUrlParam("license")) {
        Dynamsoft.DBR.BarcodeScanner.license = getUrlParam("license");
      }else{
        Dynamsoft.DBR.BarcodeScanner.license = 'DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=='; // one-day trial
      }
         
      reader = await Dynamsoft.DBR.BarcodeScanner.createInstance();
         
      if (hasCamera) {
        enhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance();
        enhancer.on("played", (playCallbackInfo) => {
          console.log("camera started");
          startProcessingLoop();
        });
        enhancer.on("cameraClose", playCallBackInfo => {
          stopScan();
        });
        await enhancer.setUIElement(Dynamsoft.DCE.CameraEnhancer.defaultUIElementURL);
        let container = document.getElementsByClassName("scanner")[0];
        container.appendChild(enhancer.getUIElement());
        if (getUrlParam("startScan") === "true") {
          startScan();
        }
        updateStyleForiOS();
      }
      updateStatus("");
    }
    
    function updateStyleForiOS(){
      document.getElementsByClassName("dce-sel-camera")[0].parentElement.style = "position: absolute;left: 0;top: 0;top: env(safe-area-inset-top);";
      document.getElementsByClassName("dce-btn-close")[0].style = "position: absolute;right: 0;top: 0;top: env(safe-area-inset-top);";
    }
    
    function hideControls(){
      document.getElementsByClassName("controls")[0].style.display = "none";
    }
    
    function revealControls(){
      document.getElementsByClassName("controls")[0].style.display = "";
    }
    
    function startProcessingLoop(isBarcode){
      stopProcessingLoop();
      interval = setInterval(captureAndDecode,100); // read barcodes
    }
    
    function stopProcessingLoop(){
      if (interval) {
        clearInterval(interval);
        interval = undefined;
      }
      processing = false;
    }
    
    async function captureAndDecode() {
      if (!enhancer || !reader) {
        return
      }
      if (enhancer.isOpen() === false) {
        return;
      }
      if (processing == true) {
        return;
      }
      processing = true; // set processing to true so that the next frame will be skipped if the processing has not completed.
      let frame = enhancer.getFrame();
      if (frame) {  
        let results = await reader.decode(frame);
        if (results.length > 0) {
          updateStatus("Found "+results.length+((results.length>1)?" results":" result"));
          stopScan();
        }
        processing = false;
      }
    };
    
    function startScan(){
      if (!enhancer || !reader) {
        alert("Please wait for the initialization of Dynamsoft Barcode Reader");
        return;
      }
      document.getElementsByClassName("scanner")[0].classList.add("active");
      enhancer.open(true); //start the camera
    }
         
    function stopScan(){
      stopProcessingLoop();
      enhancer.close(true);
      document.getElementsByClassName("scanner")[0].classList.remove("active");
      revealControls();
    }
    
    function updateStatus(info){
      document.getElementsByClassName("status")[0].innerText = info;
    }
    
    async function requestCameraPermission() {
      const constraints = {video: true, audio: false};
      const stream = await navigator.mediaDevices.getUserMedia(constraints);
      const tracks = stream.getTracks();
      for (let i=0;i<tracks.length;i++) {
        const track = tracks[i];
        track.stop();  // stop the opened camera
      }
    }
    
    function getUrlParam(name) {
      var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
      var r = window.location.search.substr(1).match(reg);
      if (r != null) return unescape(r[2]); return null;
    }
    

下面我们还需要把扫码结果返回给WebView。

  1. 引入uni-app的webview js库。

    <script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
    
    <script>
      document.addEventListener('UniAppJSBridgeReady', function() {
        uni.webView.getEnv(function(res) {
          console.log('当前环境:' + JSON.stringify(res));
        });
        // uni.webView.navigateTo(...)
      });
    </script>
    
  2. 扫到码后,就发送结果给应用。

    async function captureAndDecode() {
      //...
        if (frame) {  
          let results = await reader.decode(frame);
          if (results.length > 0) {
            updateStatus("Found "+results.length+((results.length>1)?" results":" result"));
            stopScan();
            sendBarcodeResults(results);
          }
          processing = false;
        }
      //...
    }
       
    function sendBarcodeResults(results){
      const message = {
        data: {
          action: 'scanned',
          results: JSON.stringify(results)
        }
      }
      if (window.uni) {
        window.uni.postMessage(message);
      }
    }
    

新建一个基于WebView的扫码组件

  1. 建立一个新的组件,命名为QRCodeScannerWebView.vue

  2. 组件挂载时,安卓端需要申请相机权限。

    setup(props,context) {
      const hasPermission = ref(false);
      const requestCameraPermission = () => {
        let platform=uni.getSystemInfoSync().platform
        if(platform === 'ios'){
          hasPermission.value = true;
        }else if(platform=='android'){
          plus.android.requestPermissions(['android.permission.CAMERA'], function(e){
            if(e.deniedAlways.length>0){
              console.log(e.deniedAlways.toString());  
            }  
            if(e.deniedPresent.length>0){
              console.log(e.deniedPresent.toString());  
            }  
            if(e.granted.length>0){
              hasPermission.value = true;
            }  
          }, function(e){  
             console.log('Request Permissions error:'+JSON.stringify(e));  
          });  
        }
      }
      onMounted(()=>{
        //#ifdef APP
        requestCameraPermission();
        //#endif
      })
    }
    
  3. 在template中添加WebView,URL指向上一步的网页应用。

    <template>
      <view>
        <web-view v-if="hasPermission" @message="handlePostMessage" cache="true" src="https://tony-xlh.github.io/Vanilla-JS-Barcode-Reader-Demos/uniapp?startScan=true"></web-view>
      </view>
    </template>
    
  4. 处理网页发来的消息,如果有扫码结果就触发scanned事件。

    const handlePostMessage = (e) => {
      if (e.detail.data && e.detail.data.length>0) {
        if (e.detail.data[0].results) {
          context.emit("scanned", JSON.parse(e.detail.data[0].results));
        }
      }
    }
    

在页面中使用扫码组件

使用条件编译,如果是Web端就用Web版的扫码组件,如果是应用端就用基于WebView的扫码组件。

<view class="fullscreen" v-if="scanning">
  <!--  #ifdef H5 -->
  <QRCodeScannerWeb @scanned="scanned" license="DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="></QRCodeScannerWeb>
  <!--  #endif -->
  <!--  #ifdef APP -->
  <QRCodeScanner @scanned="scanned" license="DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="></QRCodeScanner>
  <!--  #endif -->
</view>

集成到小程序

小程序也支持WebView,可以用集成到原生移动应用一样的方法。但在微信小程序中,postMessage需要使用复制链接、分享等操作触发,扫描到结果后需要在执行上述操作之一才能返回主界面。所以,我们采用服务端解码的方案。

新建一个小程序用的扫码组件

  1. 建立一个新的组件,命名为QRCodeScannerMP.vue

  2. 使用相机插件打开相机。

    <template>
      <view>
          <camera device-position="back" flash="off" @error="error" style="width: 100%; height: 300px;"></camera>
      </view>
    </template>
    
    <script>
      export default {
        name:"QRCodeScannerMP",
        setup(props,context){
        },
        data() {
          return {
               
          };
        }
      }
    </script>
    
    <style>
    
    </style>
    
  3. 新建一个解码函数,获取视频帧,将其转换为base64后发送给服务端解码,并将扫码结果返回给父组件。这里我们使用之前一篇文章写的Python后端提供解码API。这一服务部署在Vercel上,国内可能需要翻墙。小程序正式上架的话,需要部署在国内已经备案的主机上。

    const ctx = uni.createCameraContext();
    const scanning = ref(false);
    const captureAndScan = () => {
      if (scanning.value === true) {
        console.log("skip");
        return;
      }
      scanning.value === true;
      ctx.takePhoto({
        quality: 'high',
        success: (res) => {
          let url = res.tempImagePath;
          uni.getFileSystemManager().readFile({
              filePath: url,
              encoding: 'base64', 
              success: res => {
                let base64 = res.data 
                decode(base64);
              },fail: (e) => {
                console.log("Failed");
                scanning.value = false;
              },
            }
          )
        },
        fail: ()=> {
          scanning.value = false;
        }
      });
    }
       
    const decode = async (base64) =>  {
      uni.request({
          url: 'https://barcode-reading-server.vercel.app/',
          method: 'POST',
          data: {
              base64: base64
          },
          header: {
            'content-type': 'application/json'
          },
          success: (res) => {
            console.log(res.data);
            let results = [];
            let parsedResults = res.data.results;
            for (var i = 0; i < parsedResults.length; i++) {
              let parsedResult = parsedResults[i];
              let result = {};
              result.barcodeText = parsedResult.barcodeText;
              result.barcodeFormatString = parsedResult.barcodeFormat;
              result.localizationResult = {};
              result.localizationResult.x1 = parsedResult.x1;
              result.localizationResult.x2 = parsedResult.x2;
              result.localizationResult.x3 = parsedResult.x3;
              result.localizationResult.x4 = parsedResult.x4;
              result.localizationResult.y1 = parsedResult.y1;
              result.localizationResult.y2 = parsedResult.y2;
              result.localizationResult.y3 = parsedResult.y3;
              result.localizationResult.y4 = parsedResult.y4;
              results.push(result);
            }
            context.emit("scanned", results);
          },
          complete: (res) => {
            scanning.value = false;
          }
      });
    }
    

    这里用scanning这一变量控制扫码,如果上一次解码没有完成,则等待它完成。

  4. 组件挂载时,开启一个interval进行实时扫描。

    onMounted(() => {
      startScanning();
    });
    
  5. 组件卸载时,则停止扫描。

    const interval = ref<any>(null);
    onBeforeUnmount(() => {
      stopScanning();
    });
    
  6. index.vue的条件编译中,添加小程序的判定。

    <!--  #ifdef MP -->
    <QRCodeScannerMP @scanned="scanned"></QRCodeScannerMP>
    <!--  #endif -->
    

测试下来,服务端扫码的性能还可以,对准后一秒内就能扫出码。

源代码

欢迎下载源代码并尝试使用:

https://gitee.com/xulihang/uniapp-qr-code-scanner