/** * MIT License * * Copyright (C) 2023 Huawei Device Co., Ltd. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import fs from '@ohos.file.fs'; import image from '@ohos.multimedia.image'; import util from '@ohos.util'; import { TurboModule } from '@rnoh/react-native-openharmony/ts'; import type { TurboModuleContext } from '@rnoh/react-native-openharmony/ts'; import Logger from './Logger'; import picker from '@ohos.multimedia.cameraPicker'; import camera from '@ohos.multimedia.camera' import { BusinessError } from '@kit.BasicServicesKit'; import cameraPicker from '@ohos.multimedia.cameraPicker'; import { buffer } from '@kit.ArkTS'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { TM } from './generated/ts'; import { dataSharePredicates } from '@kit.ArkData'; import { abilityAccessCtrl,common, Permissions } from '@kit.AbilityKit'; import { bundleManager } from '@kit.AbilityKit'; export type MediaType = 'photo' | 'video' | 'mixed'; export type CameraType = 'back' | 'front'; type ImagePickerOptions = TM.ImagePicker.ImagePickerOptions type ImagePickerResponse = TM.ImagePicker.ImagePickerResponse const PHOTO_EXT_LIST = ['xbm', 'tif', 'pjp', 'svgz', 'jpg', 'jpeg', 'ico', 'tiff', 'gif', 'svg', 'jfif', 'webp', 'png', 'bmp', 'pjpeg', 'avif'] const TAG = 'ImagePicker:'; const fetchColumns = [ photoAccessHelper.PhotoKeys.URI, photoAccessHelper.PhotoKeys.PHOTO_TYPE, photoAccessHelper.PhotoKeys.DISPLAY_NAME, photoAccessHelper.PhotoKeys.SIZE, photoAccessHelper.PhotoKeys.DATE_ADDED, photoAccessHelper.PhotoKeys.DATE_MODIFIED, photoAccessHelper.PhotoKeys.DURATION, photoAccessHelper.PhotoKeys.WIDTH, photoAccessHelper.PhotoKeys.HEIGHT, photoAccessHelper.PhotoKeys.DATE_TAKEN, photoAccessHelper.PhotoKeys.ORIENTATION, photoAccessHelper.PhotoKeys.TITLE, ] const mdType = ['photo', 'video', 'mixed'] function isPhoto(ext: string) { return PHOTO_EXT_LIST.includes(ext); } export class RNImagePickerTurboModule extends TurboModule implements TM.ImagePicker.Spec { constructor(protected ctx: TurboModuleContext) { super(ctx); } public showImagePicker(options: ImagePickerOptions, callback: (response: ImagePickerResponse) => void): void { let sheets = [] if (options.customButtons && options.customButtons.length > 0) { sheets = options.customButtons.map((item) => { const res = { customButtons: item.name } as ESObject const sheetsItem = { title: item.title, action: () => { callback(res) } } return sheetsItem }) } this.ctx.getUIContext()?.showActionSheet({ title: options.title, message: '', autoCancel: true, confirm: { value: options.cancelButtonTitle, action: () => { const res = { didCancel: true } as ESObject callback(res) } }, cancel: () => { const res = { didCancel: true } as ESObject callback(res) }, alignment: 1, offset: { dx: 0, dy: -20 }, sheets: [ { title: options.takePhotoButtonTitle, action: () => { this.launchCamera(options, callback) } }, { title: options.chooseFromLibraryButtonTitle, action: async () => { this.launchImageLibrary(options, callback) } }, ...sheets ] }) } public launchCamera(options: ImagePickerOptions, callback: (response: ImagePickerResponse) => void): void { let str = this.setMemoryPoints() this.checkPermissionGrant('ohos.permission.CAMERA').then(async (status) => { if (status === -1) { if (str === '0') { this.requestSystemAuth(['ohos.permission.CAMERA']).then(async (isAllowed) => { this.updateMemoryPoints() if (!isAllowed) { const res = { error: 'Permissions weren\'t granted' } as ESObject callback(res) return } await this.launchCameraLogic(options, callback) }) } else { // 去app设置相机权限页面 this.openThePermissionDeniedDialog(options, callback) } } else { await this.launchCameraLogic(options, callback) } }) } private async launchCameraLogic(options: ImagePickerOptions, callback: (response: ImagePickerResponse) => void) { try { let results = {} as ImagePickerResponse // 相机逻辑 let pickerProfile: picker.PickerProfile = { cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK }; if (options.cameraType === "front") { pickerProfile.cameraPosition = camera.CameraPosition.CAMERA_POSITION_FRONT; }; let pickerResult: picker.PickerResult = await picker.pick( this.ctx.uiAbilityContext, this.getMediaTypeByOption(options.mediaType as MediaType), pickerProfile ); if (pickerResult.resultCode == -1) { results.didCancel = true callback(results) return; }; this.createDir(options) let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.ctx.uiAbilityContext); let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates(); predicates.equalTo('uri', pickerResult.resultUri); let fetchOption: photoAccessHelper.FetchOptions = { fetchColumns: fetchColumns, predicates: predicates }; let fetchResult: photoAccessHelper.FetchResult = await phAccessHelper.getAssets(fetchOption); const asset: photoAccessHelper.PhotoAsset = await fetchResult.getFirstObject(); const { type } = this.getImgTypeAndName(asset.uri); results = await this.getImagePickerResponse(asset) if (asset.get(photoAccessHelper.PhotoKeys.ORIENTATION) !== 0) { results.isVertical = false } let file = fs.openSync(asset.uri, fs.OpenMode.CREATE); let userMediaType = options.mediaType if (!userMediaType || !mdType.includes(userMediaType)) { userMediaType = 'photo' } if (userMediaType !== 'video' && isPhoto(type)) { const { width, height, uri, imageType, fileSize } = await this.getImageInfo(options, asset, type) results.width = width; results.height = height; results.type = imageType results.uri = uri if (!uri.startsWith('file://')) { results.uri = 'file://' + uri; } if (fileSize !== 0) { results.fileSize = fileSize } } else { results.uri = this.copyFileToCache(file.fd, type, options); }; if (!results.uri.startsWith('file:')) { results.uri = this.copyFileToCache(file.fd, type, options); } if (options.noData === true) { delete results.data } results.fileName = results.uri.split("/").pop() if (!results.type?.includes("image")) { results = { path: results.path, uri: results.uri } as ESObject } if (results.path?.startsWith("file://")) { results.path = results.path.slice(6) } fs.closeSync(file.fd) callback(results) Logger.info(`${TAG} the pick pickerResult is:` + JSON.stringify(asset)); } catch (error) { Logger.error(`${TAG} the pick call failed. error code: ${error.code}`); const res= { error: error.message } as ESObject callback(res) } } public launchImageLibrary(options: ImagePickerOptions, callback: (response: ImagePickerResponse) => void): void { let str = this.setMemoryPoints() this.checkPermissionGrant('ohos.permission.CAMERA').then(async (status) => { if (status === -1) { if (str === '0') { this.requestSystemAuth(['ohos.permission.CAMERA']).then(async (isAllowed) => { this.updateMemoryPoints() if (!isAllowed) { const res = { error: 'Permissions weren\'t granted' } as ESObject callback(res) return } await this.launchImageLibraryLogic(options, callback) }) } else { this.openThePermissionDeniedDialog(options, callback) } } else { await this.launchImageLibraryLogic(options, callback) } }) } private async launchImageLibraryLogic(options: ImagePickerOptions, callback: (response: ImagePickerResponse) => void) { let photoSelectResult: photoAccessHelper.PhotoSelectResult try { let userMediaType = options.mediaType if (!userMediaType || !mdType.includes(userMediaType)) { userMediaType = 'photo' } let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); const Types = photoAccessHelper.PhotoViewMIMETypes; PhotoSelectOptions.MIMEType = userMediaType == 'photo' ? Types.IMAGE_TYPE : (userMediaType == 'video' ? Types.VIDEO_TYPE : Types.IMAGE_VIDEO_TYPE); PhotoSelectOptions.maxSelectNumber = 1; if (options.allowsEditing === false) { PhotoSelectOptions.isEditSupported = options.allowsEditing } let photoPicker = new photoAccessHelper.PhotoViewPicker(); photoSelectResult = await photoPicker.select(PhotoSelectOptions) Logger.info(`${TAG} select info ${JSON.stringify(photoSelectResult)}`) if (photoSelectResult.photoUris.length === 0) { const results = { didCancel: true } as ESObject callback(results) return false } } catch (err) { Logger.error(`${TAG} PhotoViewPicker.select failed with err: ` + JSON.stringify(err)); callback({ error: err.message } as ESObject) return false } this.createDir(options) let photoAsset: photoAccessHelper.PhotoAsset try { let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.ctx.uiAbilityContext); let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates(); // 配置查询条件,使用PhotoViewPicker选择图片返回的uri进行查询 predicates.equalTo('uri', photoSelectResult.photoUris[0]); let fetchOption: photoAccessHelper.FetchOptions = { fetchColumns: fetchColumns, predicates: predicates }; let fetchResult: photoAccessHelper.FetchResult = await phAccessHelper.getAssets(fetchOption); // 得到uri对应的PhotoAsset对象,读取文件的部分信息 photoAsset = await fetchResult.getFirstObject(); } catch (err) { Logger.error(`${TAG} getAssets failed, error: ${err.code}, ${err.message}`); callback({ error: err.message } as ESObject) return false } if (!photoAsset) return const { type } = this.getImgTypeAndName(photoAsset.uri); let results: ImagePickerResponse results = await this.getImagePickerResponse(photoAsset) let file = fs.openSync(photoAsset.uri, fs.OpenMode.CREATE); let userMediaType = options.mediaType if (!userMediaType || !mdType.includes(userMediaType)) { userMediaType = 'photo' } if ((userMediaType !== 'video') && isPhoto(type)) { if (photoAsset.get(photoAccessHelper.PhotoKeys.ORIENTATION) !== 0) { results.isVertical = false } const { width, height, uri, imageType, fileSize } = await this.getImageInfo(options, photoAsset, type, false) results.width = width; results.height = height; results.type = imageType results.uri = uri if (!uri.startsWith('file://')) { results.uri = 'file://' + uri; } if (fileSize !== 0) { results.fileSize = fileSize } } else { results.uri = this.copyFileToCache(file.fd, type, options, false); }; if (!results.uri.startsWith('file:')) { results.uri = this.copyFileToCache(file.fd, type, options, false); } if (options.noData === true) { delete results.data } results.fileName = results.uri.split("/").pop() if (!results.type?.includes("image")) { results = { path: results.path, uri: results.uri } as ESObject } if (results.path?.startsWith("file://")) { results.path = results.path.slice(6) } fs.closeSync(file.fd) callback(results); } private async getImageInfo(options: ImagePickerOptions, photoAsset: photoAccessHelper.PhotoAsset, type: string, isLaunchCamera = true) { let files = fs.openSync(photoAsset.uri, fs.OpenMode.CREATE) let { maxWidth, maxHeight } = options; maxWidth = Math.abs(maxWidth) maxHeight = Math.abs(maxHeight) const imageSourceApi: image.ImageSource = image.createImageSource(files.fd); const pixelMap = await imageSourceApi.createPixelMap({editable: true}) const imageInfo: image.ImageInfo = imageSourceApi.getImageInfoSync(0) let scaleNum = 1 let imageWidth = imageInfo.size.width let imageHeight = imageInfo.size.height let imageType= imageInfo.mimeType if (maxWidth && imageWidth > maxWidth) { scaleNum = maxWidth / imageWidth } if (maxHeight && imageHeight > maxHeight) { scaleNum = maxHeight / imageHeight } Logger.info(`${TAG} ========== scaleNum ${JSON.stringify(scaleNum)}`) let uri if (options.storageOptions?.path) { uri = this.getStorageOptionsFilePath(type, options.storageOptions?.path) } else { uri = this.getCacheFilePath(type) } let width: number = imageInfo.size.width let height: number = imageInfo.size.height let fileSize = 0 let quality = Math.abs(options.quality) * 100 if (quality === 0 || quality > 100) { quality = 100 } if (scaleNum !== 1 || quality !== 100) { await pixelMap.scale(scaleNum, scaleNum) const imagePackerApi: image.ImagePacker = image.createImagePacker(); const file = fs.openSync(uri, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE) let packOpts: image.PackingOption = { format: imageInfo.mimeType, quality } const buffer: ArrayBuffer = await imagePackerApi.packToData(pixelMap, packOpts) await fs.write(file.fd, buffer) // 压缩写入后重新获取图片信息 let scaleFiles = fs.openSync(uri, fs.OpenMode.CREATE) let stat = fs.statSync(scaleFiles.fd); fileSize = stat.size; const scaleImageSourceApi: image.ImageSource = image.createImageSource(scaleFiles.fd); const scaleImageInfo: image.ImageInfo = scaleImageSourceApi.getImageInfoSync(0) width = scaleImageInfo.size.width height = scaleImageInfo.size.height await imagePackerApi.release() await scaleImageSourceApi.release() fs.closeSync(scaleFiles.fd) } else { uri = this.copyFileToCache(files.fd, type, options, isLaunchCamera); } await imageSourceApi.release() await pixelMap.release() fs.closeSync(files.fd) return { uri, width, height, imageType, fileSize } } private copyFileToCache(fd: number, type: string, options: ImagePickerOptions, isLaunchCamera = true) { try { let filePath if (options.storageOptions?.path && isLaunchCamera) { filePath = this.getStorageOptionsFilePath(type, options.storageOptions?.path) } else { filePath = this.getCacheFilePath(type) } Logger.info(`${TAG} ======== ${filePath}`) fs.copyFileSync(fd, filePath, 0) return 'file://' + filePath } catch (e) { Logger.info(`${TAG} 复制到应用缓存区失败! ${JSON.stringify(e)}`); } } private createDir(options: ImagePickerOptions) { if(options.storageOptions && options.storageOptions?.path) { let dirPath = this.ctx.uiAbilityContext.cacheDir + `/${options.storageOptions?.path}`; const bool = fs.accessSync(dirPath) Logger.info(`${TAG} bool ${bool}`) if(!bool) { fs.mkdirSync(dirPath); } } } private getCacheFilePath(type) { return this.ctx.uiAbilityContext.cacheDir + '/rn_image_picker_lib_temp_' + util.generateRandomUUID(true) + '.' + type; } private getStorageOptionsFilePath(type: string, path: string) { return this.ctx.uiAbilityContext.cacheDir + '/' + path + '/rn_image_picker_lib_temp_' + util.generateRandomUUID(true) + '.' + type; } private async getImagePickerResponse(photoAsset: photoAccessHelper.PhotoAsset) { let res = { data: await this.getFileBase64(photoAsset.uri), fileName: photoAsset.displayName, isVertical: true, width: photoAsset.get(photoAccessHelper.PhotoKeys.WIDTH), height: photoAsset.get(photoAccessHelper.PhotoKeys.HEIGHT), fileSize: photoAsset.get(photoAccessHelper.PhotoKeys.SIZE), path: photoAsset.uri, origURL: photoAsset.uri, originalRotation: photoAsset.get(photoAccessHelper.PhotoKeys.ORIENTATION), timestamp: this.convertTimestampToCustomISO8601(Number(photoAsset.get(photoAccessHelper.PhotoKeys.DATE_MODIFIED))) } as ImagePickerResponse return res } private async getFileBase64(uri: string) { let file = fs.openSync(uri, fs.OpenMode.READ_ONLY); let arrayBuffer = new ArrayBuffer(100 * 1024 * 1024); let readLen = fs.readSync(file.fd, arrayBuffer); return buffer.from(arrayBuffer, 0, readLen).toString('base64'); } private getImgTypeAndName(imgUri: string) { let res = { type: '', fileName: '' }; const mimeUri = imgUri.substring(0, 4) if (mimeUri === 'file') { let i = imgUri.lastIndexOf('/') res.fileName = imgUri.substring(i + 1) i = imgUri.lastIndexOf('.') res.type = 'Unknown' if (i != -1) { res.type = imgUri.substring(i + 1) } } return res; } private openThePermissionDeniedDialog(options: ImagePickerOptions, callback: (response: ImagePickerResponse) => void) { Logger.info(`${TAG} 打开弹窗,选择去app设置相机权限页面`) this.ctx.getUIContext()?.showAlertDialog( { title: options.permissionDenied?.title, message: options.permissionDenied?.text, autoCancel: false, alignment: 3, offset: { dx: 0, dy: -20 }, gridCount: 3, primaryButton: { value: options.permissionDenied?.okTitle, action: () => { Logger.info(`${TAG} 按钮 okTitle`) callback({ didCancel: true } as ESObject) } }, secondaryButton: { enabled: true, defaultFocus: true, style: 1, value: options.permissionDenied?.reTryTitle, action: async () => { Logger.info(`${TAG} 按钮 reTryTitle `) // 去往App的设置权限页面 let context = this.ctx.uiAbilityContext as common.UIAbilityContext; let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT; try { const data: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleFlags) Logger.info(`${TAG} BundleInfo ${JSON.stringify(data)}`) Logger.info(`${TAG} context.startAbility 设置页面`) context.startAbility({ bundleName: 'com.huawei.hmos.settings', abilityName: 'com.huawei.hmos.settings.MainAbility', uri: 'application_info_entry', parameters: { pushParams: data.name } }); } catch (err) { let message = (err as BusinessError).message; Logger.error('testTag', 'getBundleInfoForSelf failed: %{public}s', message); } } }, cancel: () => { const res = { error: 'Permissions weren\'t granted' } as ESObject callback(res) return } } ) } private updateMemoryPoints() { let pathDir = this.ctx.uiAbilityContext.filesDir let filePath = pathDir + "/IMAGE_PICKER_TEST.txt"; let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); let str: string = "1"; fs.writeSync(file.fd, str); fs.closeSync(file); } private setMemoryPoints() { let pathDir = this.ctx.uiAbilityContext.filesDir let filePath = pathDir + "/IMAGE_PICKER_TEST.txt"; let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); let buf = new ArrayBuffer(4096); return fs.readSync(file.fd, buf).toString(); } private getMediaTypeByOption(mediaType: MediaType): cameraPicker.PickerMediaType[] { let mediaTypeArr: cameraPicker.PickerMediaType[] = []; if (mediaType === 'photo') { mediaTypeArr.push(picker.PickerMediaType.PHOTO) } if (mediaType === 'video') { mediaTypeArr.push(picker.PickerMediaType.VIDEO) } if (mediaType === 'mixed') { mediaTypeArr = [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO] } return mediaTypeArr; } private convertTimestampToCustomISO8601(timestamp: number): string { Logger.info(`${TAG} timestamp ${timestamp}`) const date = new Date(timestamp * 1000); let isoString = date.toISOString(); isoString = isoString.slice(0, -5) + 'Z'; Logger.info(`${TAG} isoString ${isoString}`) return isoString; } private async checkPermissionGrant(permission: Permissions): Promise { let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED; // 获取应用程序的accessTokenID let tokenId: number = 0; try { let bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo; tokenId = appInfo.accessTokenId; Logger.info(`${TAG} tokenId ${tokenId}`) } catch (error) { const err: BusinessError = error as BusinessError; Logger.error(`${TAG} Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`); } // 校验应用是否被授予权限 try { grantStatus = await atManager.checkAccessToken(tokenId, permission); Logger.info(`${TAG} grantStatus ${grantStatus}`) } catch (error) { const err: BusinessError = error as BusinessError; Logger.error(`${TAG} Failed to check access token. Code is ${err.code}, message is ${err.message}`); } return grantStatus; } private async requestSystemAuth(permissionArr: Permissions[]): Promise { // 权限管理 let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); let context = this.ctx.uiAbilityContext as common.UIAbilityContext; // let permissionArr: Permissions[] = ['ohos.permission.CAMERA'] try { // 请求用户授权 const data = await atManager.requestPermissionsFromUser(context, permissionArr); const grantStatus: number[] = data.authResults; // 检查所有权限是否都被授予 for (let i = 0; i < grantStatus.length; i++) { if (grantStatus[i] !== 0) { // 如果有任何一个权限被拒绝,返回 false return false; } } // 所有权限都被授予 return true; } catch (err: any) { // 错误处理 Logger.error(`${TAG} Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`); return false; } } }