乐趣区

关于openharmony:OpenHarmony应用实现二维码扫码识别

本文转载自《OpenHarmony 利用实现二维码扫码辨认》,作者 zhushangyuan_

概念介绍
二维码的利用场景十分宽泛,在购物利用中,消费者能够间接扫描商品二维码,浏览并购买产品,如图是购物利用的扫描二维码的页面。

本文就以橘子购物示例利用为例,来解说 OpenHarmony 利用二维码开发相干的技术点。

咱们先看下二维码相干的几个概念。

● 二维码生成
OpenHarmony 利用框架提供了 QRCode 组件,用于显示单个二维码的组件。该组件只能用于显示二维码,无奈显示条码与解析码内容。

● 二维码解析
OpenHarmony 提供了功能强大的三方库 @ohos/zxing,是一个解析 / 生成一维码 / 二维码的库。具体内容能够参考 @ohos/zxing。

二维码解析时,通常有两种形式,应用相机拍摄获取图片或关上相册选取图片,而后图片解析适合的图片格式,进行二维码解析。

橘子购物示例利用扫描二维码的示例图:

配置文件
理解了二维码相干的概念后,咱们看下橘子购物示例利用的 oh-package.json5 配置文件。

在橘子购物示例利用中,实现首页二维码扫描的页面的文件地位为:
entry/src/main/ets/pages/ScanPage.ets。文件内容如下:

import {QRCodeScanComponent} from "@ohos/scan-component"
@Entry
@Component
struct Scan {build() {Column() {QRCodeScanComponent()
    }
  }
}

内容非常简单,次要是导入的自定义组件 QRCodeScanComponent,这个组件的代码来自:二维码扫描示例利用,后文咱们这样剖析如何开发这个二维码扫描利用。从这一行,能够理解到 OpenHarmony 利用如何援用 ohpm 本地三方库。

"@ohos/scan-component":"file:../libs/ohos-qr-code-scan-1.0.1.har",

oh-package.json5 配置文件片段如下:

{
  "license": "ISC",
  "devDependencies": {},
  "name": "product",
  "description": "example description",
  "repository": {},
  "version": "1.0.0",
  "dependencies": {
    "@ohos/http": "file:../libs/ohos-http-1.0.0.tgz",
    "@ohos/video-component": "file:../libs/ohos-video-component-1.0.5.tgz",
    "@ohos/details-page-component": "file:../feature/detailPage",
    "@ohos/notification": "file:../libs/ohos-notification-1.0.0.tgz",
    "@ohos/scan-component": "file:../libs/ohos-qr-code-scan-1.0.1.har",
    "@ohos/updatedialog": "file:../libs/ohos-updatedialog-1.0.0.tgz",
    "@ohos/enter-animation": "file:../libs/ohos-enter-animation-1.0.1.tgz",
    "@ohos/share-component": "file:../libs/ohos-sharecomponent-1.0.1.tgz",
    "@ohos/emitter": "file:../feature/emitter",
    "@ohos/navigation-component": "file:../feature/navigationHome"
  }
}

开发步骤
咱们来看二维码扫描性能是如何开发的。
导入 ohpm 三方库
在开发前,咱们须要导入 ohpm 组件库:@ohos/zxing。能够应用命令行形式导入 ohpm install @ohos/zxing,也能够间接在文件 entry\oh-package.json5 中配置,如文件片段所示。

能够看出,二维码扫描的外围代码寄存在 Feature 目录,是一个独立的 module 模块,不便复用:“@ohos/feature-qr-code-scan”:“file:…/Feature”。

文件 entry\oh-package.json5 片段:

 "dependencies": {
    "@ohos/feature-qr-code-scan": "file:../Feature",
    "@ohos/zxing": "^2.0.0"
  }  

相机服务
CameraService.ets 文件相机服务构造函数中,会创立一个图片接收器。该图片接收器能够监听’imageArrival’事件,当相机拍照时会触发该事件。在监听事件的回调函数里,实现对拍照的图片进行解决。

CameraService.ets 文件相机服务构造函数:

constructor(imgReceiver?: image.ImageReceiver) {if (imgReceiver === undefined) {
      this.imageReceiver = image.createImageReceiver(QRCodeScanConst.IMG_DEFAULT_SIZE.WIDTH,
      QRCodeScanConst.IMG_DEFAULT_SIZE.HEIGHT, image.ImageFormat.JPEG, QRCodeScanConst.MAX_IMAGE_CAPACITY)
    } else {
      this.imageReceiver = image.createImageReceiver(imgReceiver.size.width, imgReceiver.size.height,
      imgReceiver.format, imgReceiver.capacity)
    }
  }

在 CameraService.ets 文件创建相机函数中,次要蕴含如下几个步骤:

● 获取反对的相机
依据 context 获取 CameraManager,而后获取反对的相机(摄像头)。如果没有反对的相机,则而后。
如有反对的相机,则默认应用相机列表中的第一个。理论利用中,对于二维码扫描,须要应用后置相机摄像头。

● 获取相机输入输出流
首先,依据指定的相机,创立相机输出流 this.cameraInput。
而后,获取相机的 cameraOutputCapability 参数,接着创立两个输入流:
1、预览输入流创立相机预览输入流 this.previewOutput,应用的 surfaceId 来自 XComponent 组件。预览输入流,对应相机拍照前的图片预览。
2、相片输入流创立相片输入流 this.photoOutput,应用的 receivingSurfaceId 来自上文创立的图片接收器。相片输入流,用于保留到相片。

● 配置相机会话
配置相机会话,也比较简单,增加输出流和输入流即可,见代码及其正文。CameraService.ets 文件创建相机函数:

  /**
   * 创立相机
   */
  async createCamera(surfaceId: string) {Logger.info("createCamera start")
    // 依据 context 获取 CameraManager
    let cameraManager = camera.getCameraManager(AppStorage.Get('context'))
    // 获取 Camera 对象数组
    let cameras = cameraManager.getSupportedCameras()
    // 没有相机就进行
    if (cameras.length === 0) {Logger.error("createCamera: cameras length is 0.")
      return
    }
    // 拿到相机列表中的第一个默认相机 id, 依据 id 获取相机输出流
    this.cameraInput = cameraManager.createCameraInput(cameras[0])
    this.cameraInput.open()
    // 获取 cameraOutputCapability 参数
    let cameraOutputCapability = cameraManager.getSupportedOutputCapability(cameras[0])
    // 获取相机输入流
    this.previewOutput = cameraManager.createPreviewOutput(cameraOutputCapability.previewProfiles[0], surfaceId)
    // 获取一个能够创立相片输入流的 id
    let receivingSurfaceId = await this.imageReceiver.getReceivingSurfaceId()
    // 创立相片输入流
    this.photoOutput = cameraManager.createPhotoOutput(cameraOutputCapability.photoProfiles[0], receivingSurfaceId)
    // 获取捕捉会话的实例
    this.captureSession = cameraManager.createCaptureSession()
    // 开始会话配置
    this.captureSession.beginConfig()
    // 应用相机输出流 --- 增加一个摄像头输出流
    this.captureSession.addInput(this.cameraInput)
    // 应用相机输入流 --- 增加一个摄像头输入
     this.captureSession.addOutput(this.previewOutput)
    // 应用相片输入流 --- 增加相机照片的输入
    this.captureSession.addOutput(this.photoOutput)
    // 完结并提交配置
    await this.captureSession.commitConfig()
    // 开始捕捉会话
    await this.captureSession.start()
    Logger.info("createCamera end")
  }

CameraService.ets 文件拍照函数中,指定相片参数设置,而后调用 capture()函数实现拍照。
拍照后会触发图片接收器的’imageArrival’事件。拍照函数在应用相机扫描二维码的时候调用。
该图片接收器能够监听’imageArrival’事件,当相机拍照时会触发该事件。在监听事件的回调函数里,实现对拍照的图片进行解决。
CameraService.ets 文件拍照函数:

 takePicture() {
    let photoSetting = {
      rotation: camera.ImageRotation.ROTATION_0,
      quality: camera.QualityLevel.QUALITY_LEVEL_MEDIUM,
      mirror: false
    }
    this.photoOutput.capture(photoSetting)
  }

二维码解析实现代码
二维码解析类文件为:QRCodeParser.ets,反对拍照辨认二维码,还反对从相册抉择二维码图片进行辨认。

咱们首先看下如何解析从相机获取的二维码图片,对应函数为:parseQRCodeImageFromCamera,该类指定一个工夫随机的图片文件名,图片归档格局,而后持续调用函数 parseQRCodeImageWithNameFromCamera。

  /**
   * 解析从相机获取的二维码图片
   *
   * @param cameraService
   * @param canvasContext
   */
  parseQRCodeImageFromCamera(cameraService: CameraService,
                             imageComponentType?: image.ComponentType): void {Logger.info("parseQRCodeImageFromCamera start")
    let fileName = this.getRandomFileName(QRCodeScanConst.IMG_FILE_PREFIX, QRCodeScanConst.IMG_SUFFIX_JPG)
    this.parseQRCodeImageWithNameFromCamera(cameraService, fileName, imageComponentType);
    Logger.info("parseQRCodeImageFromCamera end")
  }

在函数 parseQRCodeImageWithNameFromCamera 中,注册图片接收器监听’imageArrival’事件,在监听函数里,对二维码图片进行解析辨认。
当相机对二维码拍照后,二维码图片会被保留到指定的目录下,返回文件 URI。保留图片的函数 createPublicDirFileAsset 的实现,能够自行查阅源码。
依据返回的图片 URI,调用函数 parseImageQRCode 对二维码进行解析。函数 parseImageQRCode 后文会介绍。
如果解析失败,弹窗提醒解析失败。如果解析胜利,会被解析后果保留到 AppStorage。
保留到 AppStorage 的二维码解析后果会被 @watch 装璜器的变量监督,当监督到有二维码辨认后果后,会在界面展现,后文会介绍。
QRCodeParser.ets 文件 parseQRCodeImageWithNameFromCamera 函数代码:

  /**
   * 解析从相机获取的二维码图片,指定文件名称
   *
   * @param cameraService
   * @param canvasContext
   */
  parseQRCodeImageWithNameFromCamera(cameraService: CameraService,
                                     fileDisplayName: string,
                                     imageComponentType?: image.ComponentType): void {Logger.info("parseQRCodeImageWithNameFromCamera...")
    cameraService.imageReceiver.on('imageArrival', async () => {Logger.info("parseQRCodeImageWithNameFromCamera imageArrival start")
      // 从接收器获取下一个图像,并返回后果
      let targetImage: image.Image = await cameraService.imageReceiver.readNextImage()
      // 默认按 JPEG 格局解决
      let imgComponentType = imageComponentType === undefined ? image.ComponentType.JPEG : imageComponentType
      let imageComponent = await targetImage.getComponent(imgComponentType)
      // 将 image 的 ArrayBuffer 写入指定文件中,返回文件 uri
      let imageUri = await this.createPublicDirFileAsset(fileDisplayName, mediaLibrary.MediaType.IMAGE,
                     mediaLibrary.DirectoryType.DIR_IMAGE, imageComponent.byteBuffer);
      // 开释已读取的 image 资源,以便解决下一个资源
      await targetImage.release()

      // 解析二维码
      let qrCodeParseRlt = await this.parseImageQRCode(imageUri);
      if (!qrCodeParseRlt.isSucess) {Logger.error("parseQRCodeImageWithNameFromCamera qrCodeParseRlt is null")
        prompt.showToast({message: $r('app.string.qrCodeNotRecognized')
        })
        return;
      }
      // 拼接解析后果
      AppStorage.SetOrCreate(QRCodeScanConst.QR_CODE_PARSE_RESULT, qrCodeParseRlt.decodeResult);
      Logger.info("parseQRCodeImageWithNameFromCamera imageArrival end")
    })
  }

二维码解析类文件为:QRCodeParser.ets,反对拍照辨认二维码,还反对从相册抉择二维码图片进行辨认。
咱们接着,再看下如何解析从相册里筛选的二维码图片。
参数 imageSrc 为选定图片的 URI 地址。
getImageSource()代码能够自行查问,实现依据图片 URI 返回图片的宽、高,以及图片的 pixelMap 数据。而后,把像素数据写入 ArrayBuffer,供 zxing 二维码辨认程序应用。
函数 RGBLuminanceSource、BinaryBitmap、BinaryBitmap 等都是 zxing 的类。通过调用 MultiFormatReader 的 decode 函数对二维码图像进行解析。
如果解析胜利,会返回胜利的标记和解析的后果。
如果解析失败,会在 catch 语句块里进行解决,会返回失败的标记和解析失败的起因。
QRCodeParser.ets 文件 parseImageQRCode 函数代码:

 /**
   * 解析图片二维码信息
   * @param canvasContext
   * @param imageSrc
   */
  async parseImageQRCode(imageSrc: string): Promise<DecodeResultAttribute> {Logger.info(`parseImageQRCode start`);
    // 获取图片的宽高
    let imageSource = await this.getImageSource(imageSrc);
    let imageWidth = imageSource.width;
    let imageHeight = imageSource.height;
    // 获取 PixelMap 图片数据
    let pixMapData = imageSource.pixelMap;
    let pixelBytesNumber = pixMapData.getPixelBytesNumber();
    let arrayBuffer: ArrayBuffer = new ArrayBuffer(pixelBytesNumber);
    // 读取图像像素数据,后果写入 ArrayBuffer 里
    await pixMapData.readPixelsToBuffer(arrayBuffer);
    let int32Array = new Int32Array(arrayBuffer);
    let luminanceSource = new RGBLuminanceSource(int32Array, imageWidth, imageHeight);
    let binaryBitmap = new BinaryBitmap(new HybridBinarizer(luminanceSource));
    let mltiFormatReader = new MultiFormatReader();
    let hints = new Map();
    hints.set(DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.QR_CODE]);
    mltiFormatReader.setHints(hints);
    try {
      // 解析二维码
      let decodeResult = mltiFormatReader.decode(binaryBitmap);
      let decodeText = decodeResult.getText();
      Logger.info(`parseImageQRCode end ${decodeText}`);
      return {isSucess: true, decodeResult: decodeText};
    } catch (err) {let error = `The error is ${err}`;
      Logger.info(`parseImageQRCode end`);
      return {isSucess: false, decodeResult: error};
    }
  }

相机扫描辨认二维码
在文件 QRCodeScanComponent.ets 中实现了二维码扫描自定义组件。咱们看下该文件中如何实现相机扫描二维码的。
在二维码扫描组件的 aboutToAppear()函数调用的 watchCameraPermission()函数,用于应用相机扫描二维码进行辨认。
在 watchCameraPermission()函数中,应用 setInterval 函数每 100ms 判断下是否具备相机权限,当有相机权限的时候,能力应用相机扫描二维码。
当具备相机权限时,应用 setInterval 函数每 4000ms 轮询判断下是否辨认到二维码图片,如果辨认到则勾销执行轮询。
如果没有辨认到二维码,则持续调用函数 takePicture()拍照。调用该函数后,会触发图片接收器的监听事件’imageArrival’,对这个事件的监听剖析,见上文。
文件 QRCodeScanComponent.ets 中,相机拍照辨认二维码的代码片段:

aboutToAppear() {
// 监听相机权限
this.watchCameraPermission()
// 设置扫描动画
this.setQRCodeScanAnimation()
// 解析二维码图片信息
this.qrCodeParser.parseQRCodeImageFromCamera(this.cameraService);
}
......
// 监听相机权限变动
watchCameraPermission() {let interval = setInterval(() => {this.hasCameraPermission = AppStorage.Get(QRCodeScanConst.HAS_CAMERA_PERMISSION)
  if (this.hasCameraPermission) {let qrCodeScanInterval = setInterval(() => {if (this.qrCodeParseResult.length > 0 || this.isQRCodeScanStopped) {clearInterval(qrCodeScanInterval)
      }
      // 拍照
      this.cameraService.takePicture()}, 4000)
    clearInterval(interval)
  }
}, 100)
}

辨认相册二维码图片
在文件 QRCodeScanComponent.ets 中实现了二维码扫描自定义组件。咱们看下该文件中如何辨认相册二维码图片。
首先,设置 this.isQRCodeScanStopped 为 true,这个会敞开相机拍照辨认二维码。
而后,通过 startAbilityForResult 启动相册利用,供用户抉择二维码图片。
如果抉择图片失败,则弹窗报错。
如果抉择图片胜利,则调用二维码解码函数 parseImageQRCode 实现对图片二维码的辨认。
如果辨认二维码胜利,则弹窗展现二维码后果。
如果辨认辨认,则 toast 展现:未辨认到二维码。
文件 QRCodeScanComponent.ets 中,相册抉择二维码图片进行辨认代码片段:

Image($r('app.media.scan_photo'))
  .width(30)
  .height(30)
  .id('scanPhoto')
  .onClick(async () => {
    // 关上相册获取图片
    this.isQRCodeScanStopped = true
    let context = AppStorage.Get('context') as common.UIAbilityContext
    await context.startAbilityForResult({parameters: { uri: 'singleselect'},
      bundleName: 'com.ohos.photos',
      abilityName: 'com.ohos.photos.MainAbility',
    }).then(data => {
      // 获取 want 数据
      let want = data['want'];
      if (want) {
        // param 代表 want 参数中的 paramters
        let param = want['parameters'];
        if (param) {
          // 被选中的图片门路 media/image/8
          let selectedUri = param['select-item-list'];
          setTimeout(async () => {if (!selectedUri) {
              prompt.showToast({message: $r('app.string.queryImageFailed'),
                duration: 1000
              })
              return;
            }
            // 获取解析数据
            let qrCodeParseRlt = await this.qrCodeParser.parseImageQRCode(selectedUri[0]);
            if (qrCodeParseRlt.isSucess) {
              prompt.showDialog({title: $r('app.string.qrcodeResult'),
                message: qrCodeParseRlt.decodeResult
              })
            } else {
              prompt.showToast({message: $r('app.string.qrCodeNotRecognized')
              })
            }
          }, 50)
        }
      }
    })
  })

二维码扫描组件界面
在文件 QRCodeScanComponent.ets 中实现了二维码扫描自定义组件。咱们看下二维码扫描组件的页面布局。
整个页面应用 Stack 进行重叠布局。
如果有相机权限,会 XComponent 组件,用于展现相机的预览输入流。XComponent 组件的 onLoad 函数里会创立相机,onDestroy 函数里会开释相机。
Image($r(‘app.media.scan_border’))图片就是二维码扫描框,疏导用户把二维码放到框内进行扫描辨认。
Divider 是个分割线,该分割线使能了动画成果,在辨认二维码的过程中,分割线从二维码辨认框里从上到下挪动。扫描动画实现代码如下:

 // 扫描扫描动画
  setQRCodeScanAnimation() {setInterval(() => {
      animateTo({
        duration: 1000, // 动画工夫
        tempo: 0.5, // 动画速率
        curve: Curve.EaseInOut,
        delay: 200, // 动画延迟时间
        iterations: -1, // 动画是否反复播放
        playMode: PlayMode.Normal,
      }, () => {this.animationOrdinate = 390 // 扫描动画完结 Y 坐标})
    }, 2000)
  }

Text($r(‘app.string.putTheQRCodeToScan’))疏导用户把二维码放到框内进行扫描辨认。
Image($r(‘app.media.scan_back’))返回退出利用。
Image($r(‘app.media.scan_photo’))从相册里筛选二维码图片进行辨认。

build() {Column() {Stack() {if (this.hasCameraPermission) {
      XComponent({
        id: 'componentId',
        type: 'surface',
        controller: this.xComponentController
      })
        .onLoad(() => {
          // 适配可能须要获取设施信息
          this.xComponentController.setXComponentSurfaceSize({
            surfaceWidth: QRCodeScanConst.IMG_DEFAULT_SIZE.WIDTH,
            surfaceHeight: QRCodeScanConst.IMG_DEFAULT_SIZE.HEIGHT
          })
          this.surFaceId = this.xComponentController.getXComponentSurfaceId()
          this.cameraService.createCamera(this.surFaceId)
        })
        .onDestroy(() => {this.cameraService.releaseCamera()
        })
        .height('100%')
        .width('100%')
    }
    Column() {Column() {Image($r('app.media.scan_border'))
        ......
        Divider()
          .strokeWidth(1)
          .height(4)
          .width('100%')
          .color(Color.White)
          .width('100%')
          .position({x: 0, y: 0})
          .translate({x: 0, y: this.animationOrdinate})
      }
    ......
      Text($r('app.string.putTheQRCodeToScan'))
    ......
    }
    ......
    Row() {Image($r('app.media.scan_back'))
      ......
      Row({space: 16}) {Image($r('app.media.scan_photo'))
        ......
}

运行测试成果
能够下载橘子购物示例利用代码,应用 DevEco Studio 编译构建,应用 Simulator 模拟器或者实在设施进行运行体验。能够体验下应用相机对二维码图片进行辨认,还能够尝试下辨认相册中的二维码图片。

git init
git config core.sparsecheckout true
echo code/Solutions/Shopping/OrangeShopping/ > .git/info/sparse-checkout
git remote add origin https://gitee.com/openharmony/applications_app_samples.git
git pull origin master

注意事项
以后二维码示例利用辨认相册的二维码,弹出辨认后果后,程序会解体,曾经提单跟踪。示例程序待改良。
应用相机性能间接拍摄二维码的性能,始终没有胜利运行,须要进一步优化。

参考资料
橘子购物示例利用
二维码扫描示例利用
@ohos/zxing
QRCode 组件
相机开发概述
图片开发概述
XComponent

退出移动版