【入门到精通】鸿蒙next开发:分段式拍照及照片后处理实战

往期鸿蒙5.0全套实战文章必看:(文中附带全栈鸿蒙5.0学习资料)


场景描述:

应用希望三方相机能实现类似系统相机的功能,即拍照后立即保存。但是由于权限控制,要么要申请ACL权限,要么要使用安全控件保存,这种方式用户体验非常不好,而分段式拍照接口提供了这种能力。

  • 无需申请ACL权限和使用安全控件保存,拍照后照片自动保存到相册。
  • 分段式拍照后的照片经过后处理(如加水印),然后保存到沙箱目录。

方案描述

1、通过XComponentController获取XComponent组件的surfaceId。

2、调用三方相机接口,执行相机初始化流程,将surfaceId传给预览输出流,实现相机预览功能。同时注册photoAssetAvailable回调,实现分段式拍照功能。

3、点击拍照,收到photoAssetAvailable回调后:

     a. 调用媒体库落盘接口saveCameraPhoto保存一阶段低质量图,二阶段真图就绪后媒体库会主动帮应用替换落盘图片(都无需任何权限)。

     b. 调用媒体库接口注册低质量图或高质量图buffer回调,实现预览和加水印保存沙箱功能。备注:为了直观展示分段式拍照一阶段和二阶段的效果图,增加了拍照后的预览功能(3.2中的预览),将两个阶段获取到的PixelMap通过Iamge组件显示出来。

场景实现

  • 无需申请ACL权限和使用安全控件保存,拍照后照片自动保存到相册

效果图:

58_compressed.gif

核心代码

1、通过XComponentController获取XComponent组件的surfaceId。

XComponent({ 
  type: XComponentType.SURFACE, 
  controller: this.mXComponentController, 
}) 
  .onLoad(async () => { 
    Logger.info(TAG, 'onLoad is called'); 
    this.surfaceId = this.mXComponentController.getXComponentSurfaceId(); 
    GlobalContext.get().setObject('cameraDeviceIndex', this.defaultCameraDeviceIndex); 
    GlobalContext.get().setObject('xComponentSurfaceId', this.surfaceId); 
    Logger.info(TAG, `onLoad surfaceId: ${this.surfaceId}`); 
    await CameraService.initCamera(this.surfaceId, this.defaultCameraDeviceIndex); 
  })

2、调用三方相机接口,执行相机初始化流程,将surfaceId传给预览输出流,实现相机预览功能。

async onPageShow(): Promise<void> { 
  Logger.info(TAG, 'onPageShow'); 
  this.isOpenEditPage = false; 
  if (this.surfaceId !== '' && !this.isOpenEditPage) { 
    await CameraService.initCamera(this.surfaceId, GlobalContext.get().getT<number>('cameraDeviceIndex')); 
  } 
}
async initCamera(surfaceId: string, cameraDeviceIndex: number): Promise<void> { 
  Logger.debug(TAG, `initCamera cameraDeviceIndex: ${cameraDeviceIndex}`); 
  this.photoMode = AppStorage.get('photoMode'); 
  if (!this.photoMode) { 
    return; 
  } 
  try { 
    await this.releaseCamera(); 
    // Get Camera Manager Instance 
    this.cameraManager = this.getCameraManagerFn(); 
    if (this.cameraManager === undefined) { 
      Logger.error(TAG, 'cameraManager is undefined'); 
      return; 
    } 
    // Gets the camera device object that supports the specified 
    this.cameras = this.getSupportedCamerasFn(this.cameraManager); 
    if (this.cameras.length < 1 || this.cameras.length < cameraDeviceIndex + 1) { 
      return; 
    } 
    this.curCameraDevice = this.cameras[cameraDeviceIndex]; 
    let isSupported = this.isSupportedSceneMode(this.cameraManager, this.curCameraDevice); 
    if (!isSupported) { 
      Logger.error(TAG, 'The current scene mode is not supported.'); 
      return; 
    } 
    let cameraOutputCapability = 
      this.cameraManager.getSupportedOutputCapability(this.curCameraDevice, this.curSceneMode); 
    let previewProfile = this.getPreviewProfile(cameraOutputCapability); 
    if (previewProfile === undefined) { 
      Logger.error(TAG, 'The resolution of the current preview stream is not supported.'); 
      return; 
    } 
    this.previewProfileObj = previewProfile; 
    // Creates the previewOutput output object 
    this.previewOutput = this.createPreviewOutputFn(this.cameraManager, this.previewProfileObj, surfaceId); 
    if (this.previewOutput === undefined) { 
      Logger.error(TAG, 'Failed to create the preview stream.'); 
      return; 
    } 
    // Listening for preview events 
    this.previewOutputCallBack(this.previewOutput); 
    let photoProfile = this.getPhotoProfile(cameraOutputCapability); 
    if (photoProfile === undefined) { 
      Logger.error(TAG, 'The resolution of the current photo stream is not supported.'); 
      return; 
    } 
    this.photoProfileObj = photoProfile; 
    // Creates a photoOutPut output object 
    this.photoOutput = this.createPhotoOutputFn(this.cameraManager, this.photoProfileObj); 
    if (this.photoOutput === undefined) { 
      Logger.error(TAG, 'Failed to create the photo stream.'); 
      return; 
    } 
    // Creates a cameraInput output object 
    this.cameraInput = this.createCameraInputFn(this.cameraManager, this.curCameraDevice); 
    if (this.cameraInput === undefined) { 
      Logger.error(TAG, 'Failed to create the camera input.'); 
      return; 
    } 
    // Turn on the camera 
    let isOpenSuccess = await this.cameraInputOpenFn(this.cameraInput); 
    if (!isOpenSuccess) { 
      Logger.error(TAG, 'Failed to open the camera.'); 
      return; 
    } 
    // Camera status callback 
    this.onCameraStatusChange(this.cameraManager); 
    // Listens to CameraInput error events 
    this.onCameraInputChange(this.cameraInput, this.curCameraDevice); 
    // Session Process 
    await this.sessionFlowFn(this.cameraManager, this.cameraInput, this.previewOutput, this.photoOutput); 
  } catch (error) { 
    let err = error as BusinessError; 
    Logger.error(TAG, `initCamera fail: ${JSON.stringify(err)}`); 
  } 
}

3、注册photoAssetAvailable回调,实现分段式拍照功能。

// 注册photoAssetAvailable回调 
photoOutput.on('photoAssetAvailable', (err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset) => { 
  Logger.info(TAG, 'photoAssetAvailable begin'); 
  if (err) { 
    Logger.error(TAG, `photoAssetAvailable err:${err.code}`); 
    return; 
  } 
  this.handlePhotoAssetCb(photoAsset); 
}); 
 
setSavePictureCallback(callback: (photoAsset: photoAccessHelper.PhotoAsset | image.PixelMap) => void): void { 
  this.handlePhotoAssetCb = callback; 
}
// ModeComponent.ets中实现handlePhotoAssetCb回调方法,在收到回调后跳转到EditPage.ets页面进行逻辑处理。 
changePageState(): void { 
  if (this.isOpenEditPage) { 
    this.onJumpClick(); 
  } 
} 
 
aboutToAppear(): void { 
  Logger.info(TAG, 'aboutToAppear'); 
  CameraService.setSavePictureCallback(this.handleSavePicture); 
} 
 
handleSavePicture = (photoAsset: photoAccessHelper.PhotoAsset | image.PixelMap): void => { 
  Logger.info(TAG, 'handleSavePicture'); 
  this.setImageInfo(photoAsset); 
  AppStorage.set<boolean>('isOpenEditPage', true); 
  Logger.info(TAG, 'setImageInfo end'); 
}

4、点击拍照,收到photoAssetAvailable回调后,调用媒体库落盘接口saveCameraPhoto保存一阶段低质量图。

// EditPage.ets中进行回调后的具体逻辑处理 
requestImage(requestImageParams: RequestImageParams): void { 
  if (requestImageParams.photoAsset) { 
    // 1. 调用媒体库落盘接口保存一阶段低质量图,二阶段真图就绪后媒体库会主动帮应用替换落盘图片 
    mediaLibSavePhoto(requestImageParams.photoAsset); 
  } 
} 
 
aboutToAppear() { 
  Logger.info(TAG, 'aboutToAppear begin'); 
  if (this.photoMode === Constants.SUBSECTION_MODE) { 
    let curPhotoAsset = GlobalContext.get().getT<photoAccessHelper.PhotoAsset>('photoAsset'); 
    this.photoUri = curPhotoAsset.uri; 
    let requestImageParams: RequestImageParams = { 
      context: getContext(), 
      photoAsset: curPhotoAsset, 
      callback: this.photoBufferCallback 
    }; 
    this.requestImage(requestImageParams); 
    Logger.info(TAG, `aboutToAppear photoUri: ${this.photoUri}`); 
  } else if (this.photoMode === Constants.SINGLE_STAGE_MODE) { 
    this.curPixelMap = GlobalContext.get().getT<image.PixelMap>('photoAsset'); 
  } 
}

落盘接口saveCameraPhoto(媒体库提供的系统函数)保存一阶段低质量图,二阶段真图就绪后媒体库会主动帮应用替换落盘图片。

async function mediaLibSavePhoto(photoAsset: photoAccessHelper.PhotoAsset): Promise<void> { 
  try { 
    let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest = 
      new photoAccessHelper.MediaAssetChangeRequest(photoAsset) 
    assetChangeRequest.saveCameraPhoto() 
    await photoAccessHelper.getPhotoAccessHelper(context).applyChanges(assetChangeRequest) 
    console.info('apply saveCameraPhoto successfully') 
  } catch (err) { 
    console.error(`apply saveCameraPhoto failed with error: ${err.code}, ${err.message}`) 
  } 
}
  • 分段式拍照后的照片经过后处理(如加水印),然后保存到沙箱目录

效果图:

59.gif

核心代码

1、调用媒体库接口注册低质量图或高质量(通过设置DeliveryMode枚举值实现)图buffer回调。DeliveryMode枚举中有:

FAST_MODE:一次回调,速度快,直接返回一阶段低质量图。

HIGH_QUALITY_MODE:一次回调,速度慢,返回高质量资源图。

BALANCE_MODE:两次回调,先快速返回一阶段低质量图,第二次会收到高质量图的回调。

if (requestImageParams.photoAsset) { 
  // 2. 调用媒体库接口注册低质量图或高质量图buffer回调,自定义处理 
  mediaLibRequestBuffer(requestImageParams); 
}
async function mediaLibRequestBuffer(requestImageParams: RequestImageParams) { 
  try { 
    class MediaDataHandler implements photoAccessHelper.MediaAssetDataHandler<ArrayBuffer> { 
      onDataPrepared(data: ArrayBuffer, map: Map<string, string>): void { 
        Logger.info(TAG, 'onDataPrepared map' + JSON.stringify(map)); 
        requestImageParams.callback(data); 
        Logger.info(TAG, 'onDataPrepared end'); 
      } 
    }; 
    let requestOptions: photoAccessHelper.RequestOptions = { 
      deliveryMode: photoAccessHelper.DeliveryMode.BALANCE_MODE, 
    }; 
    const handler = new MediaDataHandler(); 
    photoAccessHelper.MediaAssetManager.requestImageData(requestImageParams.context, requestImageParams.photoAsset, 
      requestOptions, handler); 
  } catch (error) { 
    Logger.error(TAG, `Failed in requestImage, error code: ${error.code}`); 
  } 
}

自定义回调处理

photoBufferCallback: (arrayBuffer: ArrayBuffer) => void = (arrayBuffer: ArrayBuffer) => { 
  Logger.info(TAG, 'photoBufferCallback is called'); 
  let imageSource = image.createImageSource(arrayBuffer); 
  saveWatermarkPhoto(imageSource, context).then(pixelMap => { 
    this.curPixelMap = pixelMap 
  }) 
};

2、实现加水印和保存沙箱功能,注意这里只能保存到沙箱,要保存到图库是需要权限的。

export async function saveWatermarkPhoto(imageSource: image.ImageSource, context: Context) { 
  const imagePixelMap = await imageSource2PixelMap(imageSource); 
  const addedWatermarkPixelMap: image.PixelMap = addWatermark(imagePixelMap); 
  await saveToFile(addedWatermarkPixelMap!, context); 
  // 拍照后预览页面不带水印显示 
  //return imagePixelMap.pixelMap; 
  // 拍照后预览页面带水印显示 
  return addedWatermarkPixelMap; 
}

加水印功能

export function addWatermark( 
  imagePixelMap: ImagePixelMap, 
  text: string = 'watermark', 
  drawWatermark?: (OffscreenContext: OffscreenCanvasRenderingContext2D) => void): image.PixelMap { 
  const height = px2vp(imagePixelMap.height); 
  const width = px2vp(imagePixelMap.width); 
  const offScreenCanvas = new OffscreenCanvas(width, height); 
  const offScreenContext = offScreenCanvas.getContext('2d'); 
  offScreenContext.drawImage(imagePixelMap.pixelMap, 0, 0, width, height); 
  if (drawWatermark) { 
    drawWatermark(offScreenContext); 
  } else { 
    const imageScale = width / px2vp(display.getDefaultDisplaySync().width); 
    offScreenContext.textAlign = 'center'; 
    offScreenContext.fillStyle = '#A2FF0000'; 
    offScreenContext.font = 50 * imageScale + 'vp'; 
    const padding = 5 * imageScale; 
    offScreenContext.setTransform(1, -0.3, 0, 1, -170, -200); 
    offScreenContext.fillText(text, width - padding, height - padding); 
  } 
  return offScreenContext.getPixelMap(0, 0, width, height); 
}

保存沙箱功能

export async function saveToFile(pixelMap: image.PixelMap, context: Context): Promise<void> { 
  const path: string = context.cacheDir + "/pixel_map.jpg"; 
  let file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE); 
  const imagePackerApi = image.createImagePacker(); 
  let packOpts: image.PackingOption = { format: "image/jpeg", quality: 100 }; 
  imagePackerApi.packToFile(pixelMap, file.fd, packOpts).then(() => { 
    // 直接打包进文件 
  }).catch((error: BusinessError) => { 
    console.error('Failed to pack the image. And the error is: ' + error); 
  }) 
}

3、实现拍照后预览功能,pixelMap从上面的自定义回调处理中获取,然后通过Image组件显示。

Column() { 
  Image(this.curPixelMap) 
    .objectFit(ImageFit.Cover) 
    .width(Constants.FULL_PERCENT) 
    .height(Constants.EIGHTY_PERCENT) 
} 
.width(Constants.FULL_PERCENT) 
.margin({ top: 68 }) 
.layoutWeight(this.textLayoutWeight)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值