分段式拍照及照片后处理
·
场景描述:
应用希望三方相机能实现类似系统相机的功能,即拍照后立即保存。但是由于权限控制,要么要申请ACL权限,要么要使用安全控件保存,这种方式用户体验非常不好,而分段式拍照接口提供了这种能力。
- 无需申请ACL权限和使用安全控件保存,拍照后照片自动保存到相册。
- 分段式拍照后的照片经过后处理(如加水印),然后保存到沙箱目录。
方案描述
- 通过XComponentController获取XComponent组件的surfaceId。
- 调用三方相机接口,执行相机初始化流程,将surfaceId传给预览输出流,实现相机预览功能。同时注册photoAssetAvailable回调,实现分段式拍照功能。
- 点击拍照,收到photoAssetAvailable回调后:
- 调用媒体库落盘接口saveCameraPhoto保存一阶段低质量图,二阶段真图就绪后媒体库会主动帮应用替换落盘图片(都无需任何权限)。
- 调用媒体库接口注册低质量图或高质量图buffer回调,实现预览和加水印保存沙箱功能。备注:为了直观展示分段式拍照一阶段和二阶段的效果图,增加了拍照后的预览功能(3.2中的预览),将两个阶段获取到的PixelMap通过Iamge组件显示出来。
场景实现
- 无需申请ACL权限和使用安全控件保存,拍照后照片自动保存到相册
效果图:
核心代码
- 通过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); })
- 调用三方相机接口,执行相机初始化流程,将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)}`); } }
- 注册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'); }
- 点击拍照,收到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}`) } }
- 分段式拍照后的照片经过后处理(如加水印),然后保存到沙箱目录
效果图:
核心代码
- 调用媒体库接口注册低质量图或高质量(通过设置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 }) };
- 实现加水印和保存沙箱功能,注意这里只能保存到沙箱,要保存到图库是需要权限的。
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)
更多推荐
所有评论(0)