import { image } from '@kit.ImageKit'; import { FileHelper } from './FileHelper'; import fs from '@ohos.file.fs'; import { BusinessError } from '@kit.BasicServicesKit'; import { textRecognition } from '@kit.CoreVisionKit'; import { util } from '@kit.ArkTS'; export class ImageHelper { private constructor() { } /** * 图片压缩 * @param sourcePixelMap:原始待压缩图片的PixelMap对象 * @param maxCompressedImageSize:指定图片的压缩目标大小,单位kb * @returns ArrayBuffer:返回最终压缩后的图片信息 */ static async compressedImage(sourcePixelMap: image.PixelMap, maxCompressedImageSize: number): Promise { const imagePackerApi = image.createImagePacker(); const IMAGE_QUALITY = 0; const packOpts: image.PackingOption = { format: "image/png", quality: IMAGE_QUALITY }; // 通过PixelMap进行编码。compressedImageData为打包获取到的图片文件流。 let compressedImageData: ArrayBuffer = await imagePackerApi.packing(sourcePixelMap, packOpts); // 压缩目标图像字节长度 const maxCompressedImageByte = maxCompressedImageSize * 1024; // 图片压缩。先判断设置图片质量参数quality为0时,packing能压缩到的图片最小字节大小是否满足指定的图片压缩大小。如果满足,则使用packing方式二分查找最接近指定图片压缩目标大小的quality来压缩图片。如果不满足,则使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据。 if (maxCompressedImageByte > compressedImageData.byteLength) { // 使用packing二分压缩获取图片文件流 compressedImageData = await ImageHelper.packingImage(compressedImageData, sourcePixelMap, IMAGE_QUALITY, maxCompressedImageByte); } else { // 使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据 let imageScale = 1; const REDUCE_SCALE = 0.4; // 判断压缩后的图片大小是否大于指定图片的压缩目标大小,如果大于,继续降低缩放倍数压缩。 while (compressedImageData.byteLength > maxCompressedImageByte) { if (imageScale > 0) { // 性能知识点: 由于scale会直接修改图片PixelMap数据,所以不适用二分查找scale缩放倍数。这里采用循环递减0.4倍缩放图片,来查找确定最适合的缩放倍数。如果对图片压缩质量要求不高,建议调高每次递减的缩放倍数reduceScale,减少循环,提升scale压缩性能。 imageScale = imageScale - REDUCE_SCALE; await sourcePixelMap.scale(imageScale, imageScale); compressedImageData = await ImageHelper.packing(sourcePixelMap, IMAGE_QUALITY); } else { // imageScale缩放小于等于0时,没有意义,结束压缩。这里不考虑图片缩放倍数小于reduceScale的情况。 break; } } } return compressedImageData } /** * packing压缩 * @param sourcePixelMap:原始待压缩图片的PixelMap * @param imageQuality:图片质量参数 * @returns data:返回压缩后的图片数据 */ static async packing(sourcePixelMap: image.PixelMap, imageQuality: number): Promise { const imagePackerApi = image.createImagePacker(); const packOpts: image.PackingOption = { format: "image/jpeg", quality: imageQuality }; const data: ArrayBuffer = await imagePackerApi.packing(sourcePixelMap, packOpts); return data; } /** * packing二分方式循环压缩 * @param compressedImageData:图片压缩的ArrayBuffer * @param sourcePixelMap:原始待压缩图片的PixelMap * @param imageQuality:图片质量参数 * @param maxCompressedImageByte:压缩目标图像字节长度 * @returns compressedImageData:返回二分packing压缩后的图片数据 */ static async packingImage(compressedImageData: ArrayBuffer, sourcePixelMap: image.PixelMap, imageQuality: number, maxCompressedImageByte: number): Promise { // 图片质量参数范围为0-100,这里以10为最小二分单位创建用于packing二分图片质量参数的数组。 const packingArray: number[] = []; const DICHOTOMY_ACCURACY = 10; // 性能知识点: 如果对图片压缩质量要求不高,建议调高最小二分单位dichotomyAccuracy,减少循环,提升packing压缩性能。 for (let i = 0; i <= 100; i += DICHOTOMY_ACCURACY) { packingArray.push(i); } let left = 0; let right = packingArray.length - 1; // 二分压缩图片 while (left <= right) { const mid = Math.floor((left + right) / 2); imageQuality = packingArray[mid]; // 根据传入的图片质量参数进行packing压缩,返回压缩后的图片文件流数据。 compressedImageData = await ImageHelper.packing(sourcePixelMap, imageQuality); // 判断查找一个尽可能接近但不超过压缩目标的压缩大小 if (compressedImageData.byteLength <= maxCompressedImageByte) { left = mid + 1; if (mid === packingArray.length - 1) { break; } // 获取下一次二分的图片质量参数(mid+1)压缩的图片文件流数据 compressedImageData = await ImageHelper.packing(sourcePixelMap, packingArray[mid + 1]); // 判断用下一次图片质量参数(mid+1)压缩的图片大小是否大于指定图片的压缩目标大小。如果大于,说明当前图片质量参数(mid)压缩出来的图片大小最接近指定图片的压缩目标大小。传入当前图片质量参数mid,得到最终目标图片压缩数据。 if (compressedImageData.byteLength > maxCompressedImageByte) { compressedImageData = await ImageHelper.packing(sourcePixelMap, packingArray[mid]); break; } } else { // 目标值不在当前范围的右半部分,将搜索范围的右边界向左移动,以缩小搜索范围并继续在下一次迭代中查找左半部分。 right = mid - 1; } } return compressedImageData; } /** * 提取图片中文字 * @param path * @returns */ static recognizeText(path: string | image.PixelMap): Promise { return new Promise((resolve, reject) => { if (typeof path === 'string') { let file = FileHelper.openSync(path, fs.OpenMode.READ_ONLY) const imageSource = image.createImageSource(file.fd); imageSource.createPixelMap().then((pixelMap) => { ImageHelper._recognizeText(pixelMap) .then((data: textRecognition.TextRecognitionResult) => { resolve(data) }).catch((err: BusinessError) => { reject(err) }) }).catch((err: BusinessError) => { reject(err) }); } else { ImageHelper._recognizeText(path) .then((data: textRecognition.TextRecognitionResult) => { resolve(data) }).catch((err: BusinessError) => { reject(err) }) } }); } private static _recognizeText(pixelMap: image.PixelMap): Promise { return new Promise((resolve, reject) => { let visionInfo: textRecognition.VisionInfo = { pixelMap: pixelMap }; let textConfiguration: textRecognition.TextRecognitionConfiguration = { isDirectionDetectionSupported: false }; textRecognition.recognizeText(visionInfo, textConfiguration) .then((data: textRecognition.TextRecognitionResult) => { resolve(data) }) .catch((error: BusinessError) => { reject(error) }); }); } static base64ToPixelMap(base64Str: string): Promise { // 移除 Base64 字符串中的前缀(如果存在) const reg = new RegExp('data:image/\\w+;base64,'); const pureBase64Str = base64Str.replace(reg, ''); // 使用 Base64Helper 解码 Base64 字符串为 ArrayBuffer const base64Helper = new util.Base64Helper(); const data = base64Helper.decodeSync(pureBase64Str); // 创建 ImageSource 并生成 PixelMap const imageSource = image.createImageSource(data.buffer); const opts: image.DecodingOptions = { editable: true }; return imageSource.createPixelMap(opts); } }