HarmonyOS鸿蒙Next中分段式拍照及照片后处理

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

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

方案描述 1、通过 XComponentController 获取 XComponent 组件的 surfaceId。 2、调用三方相机接口,执行相机初始化流程,将 surfaceId 传给预览输出流,实现相机预览功能。同时注册 photoAssetAvailable 回调,实现分段式拍照功能。 3、点击拍照,收到 photoAssetAvailable 回调后: a. 调用媒体库落盘接口 saveCameraPhoto 保存一阶段低质量图,二阶段真图就绪后媒体库会主动帮应用替换落盘图片(都无需任何权限)。 b. 调用媒体库接口注册低质量图或高质量图 buffer 回调,实现预览和加水印保存沙箱功能。备注:为了直观展示分段式拍照一阶段和二阶段的效果图,增加了拍照后的预览功能,将两个阶段获取到的 PixelMap 通过 Image 组件显示出来。

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

效果图:

核心代码 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}`)
  }
}

分段式拍照后的照片经过后处理(如加水印),然后保存到沙箱目录

自定义回调处理

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
  })
};

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

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);
  })
}

实现拍照后预览功能,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)

更多关于HarmonyOS鸿蒙Next中分段式拍照及照片后处理的实战教程也可以访问 https://www.itying.com/category-93-b0.html

1 回复

更多关于HarmonyOS鸿蒙Next中分段式拍照及照片后处理的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS鸿蒙Next中,分段式拍照功能允许用户通过多次拍摄,将不同场景或角度的照片自动合成一张高质量的全景或长图。系统通过智能算法识别拍摄内容,确保无缝拼接。照片后处理方面,鸿蒙Next提供了丰富的编辑工具,包括AI美颜、滤镜、裁剪、旋转等,用户可根据需求进行个性化调整。此外,系统还支持AI场景识别,自动优化照片色彩、亮度和对比度,提升整体视觉效果。

回到顶部