From 8ab35786fb136dfa747733216f6a8aa806796a8f Mon Sep 17 00:00:00 2001 From: xiaoxiao Date: Thu, 14 Aug 2025 11:56:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/ets/utils/ImageCompressUtils.ets | 239 ++++++++++++++++ .../ets/components/AddAndEditRecordComp.ets | 261 +++++++++++++++++- 2 files changed, 494 insertions(+), 6 deletions(-) create mode 100644 commons/basic/src/main/ets/utils/ImageCompressUtils.ets diff --git a/commons/basic/src/main/ets/utils/ImageCompressUtils.ets b/commons/basic/src/main/ets/utils/ImageCompressUtils.ets new file mode 100644 index 0000000..332fe09 --- /dev/null +++ b/commons/basic/src/main/ets/utils/ImageCompressUtils.ets @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { image } from '@kit.ImageKit'; +import { fileIo } from '@kit.CoreFileKit'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { promptAction } from '@kit.ArkUI'; + + +const TAG: string = 'ImageCompressInfo'; +// let context = getContext(this); +// let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context); +let compressedImageData: ArrayBuffer + +//压缩图片类 +export class CompressedImageInfo { + public imageUri: string = ''; // 压缩后图片保存位置的uri + public imageByteLength: number = 0; // 压缩后图片字节长度 +} + + +/** + * 图片压缩,保存 + * @param sourcePixelMap:原始待压缩图片的PixelMap对象 + * @param maxCompressedImageSize:指定图片的压缩目标大小,单位kb + * @returns compressedImageInfo:返回最终压缩后的图片信息 + */ +export async function compressedImage(sourcePixelMap: PixelMap, + maxCompressedImageSize: number): Promise { + // 创建图像编码ImagePacker对象 + const IMAGE_PACKER_API = image.createImagePacker(); + const IMAGE_QUALITY = 0; + const PACK_OPTS: image.PackingOption = { format: 'image/jpeg', quality: IMAGE_QUALITY }; + // 通过PixelMap进行编码。compressedImageData为打包获取到的图片文件流。 + compressedImageData = await IMAGE_PACKER_API.packToData(sourcePixelMap, PACK_OPTS); + // 压缩目标图像字节长度 + const MAX_COMPRESS_IMAGE_BYTE = maxCompressedImageSize * 1024; + // 图片压缩。先判断设置图片质量参数quality为0时,packing能压缩到的图片最小字节大小是否满足指定的图片压缩大小。 + // 如果满足,则使用packToData方式二分查找最接近指定图片压缩目标大小的quality来压缩图片。 + // 如果不满足,则使用scale对图片先进行缩放,采用while循环每次递减0.2倍缩放图片。 + // 再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据。 + if (MAX_COMPRESS_IMAGE_BYTE > compressedImageData.byteLength) { + // 使用packing二分压缩获取图片文件流 + compressedImageData = + await packingImage(compressedImageData, sourcePixelMap, IMAGE_QUALITY, MAX_COMPRESS_IMAGE_BYTE); + } else { + // 使用scale对图片先进行缩放,采用while循环每次递减0.2倍缩放图片。 + // 再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据。 + let imageScale = 1; + const REDUCE_SCALE = 0.2; + // 判断压缩后的图片大小是否大于指定图片的压缩目标大小,如果大于,继续降低缩放倍数压缩。 + while (compressedImageData.byteLength > MAX_COMPRESS_IMAGE_BYTE) { + if (imageScale > 0) { + // 性能知识点: 由于scale会直接修改图片PixelMap数据,所以不适用二分查找scale缩放倍数。 + // 这里采用循环递减0.2倍缩放图片,来查找确定最适合的缩放倍数。 + // 如果对图片压缩质量要求不高,建议调高每次递减的缩放倍数reduceScale,减少循环,提升scale压缩性能。 + imageScale = imageScale - REDUCE_SCALE; + await sourcePixelMap.scale(imageScale, imageScale); + compressedImageData = await packing(sourcePixelMap, IMAGE_QUALITY); + } else { + // imageScale缩放小于等于0时,没有意义,结束压缩。这里不考虑图片缩放倍数小于reduceScale的情况。 + break; + } + } + } + + // 保存图片,返回压缩后的图片信息。 + const COMPRESS_IMAGE_INFO: CompressedImageInfo = await saveImageToSandboxPath(compressedImageData); + return COMPRESS_IMAGE_INFO; +} + + +/** + * packing压缩 + * @param sourcePixelMap:原始待压缩图片的PixelMap + * @param imageQuality:图片质量参数 + * @returns data:返回压缩后的图片数据 + */ +export async function packing(sourcePixelMap: image.PixelMap, imageQuality: number): Promise { + const IMAGE_PACKER_API = image.createImagePacker(); + const PACK_OPTS: image.PackingOption = { format: 'image/jpeg', quality: imageQuality }; + const DATA: ArrayBuffer = await IMAGE_PACKER_API.packToData(sourcePixelMap, PACK_OPTS); + return DATA; +} + + +/** + * packing二分方式循环压缩 + * @param compressedImageData:图片压缩的ArrayBuffer + * @param sourcePixelMap:原始待压缩图片的PixelMap + * @param imageQuality:图片质量参数 + * @param maxCompressedImageByte:压缩目标图像字节长度 + * @returns compressedImageData:返回二分packing压缩后的图片数据 + */ +export async function packingImage(compressedImageData: ArrayBuffer, sourcePixelMap: image.PixelMap, + imageQuality: number, + maxCompressedImageByte: number): Promise { + // 图片质量参数范围为0-100,这里以10为最小二分单位创建用于packing二分图片质量参数的数组。 + const PACKING_ARRAY: number[] = []; + const DICHOTOMY_ACCURACY = 10; + // 性能知识点: 如果对图片压缩质量要求不高,建议调高最小二分单位dichotomyAccuracy,减少循环,提升packing压缩性能。 + for (let i = 0; i <= 100; i += DICHOTOMY_ACCURACY) { + PACKING_ARRAY.push(i); + } + let left = 0; + let right = PACKING_ARRAY.length - 1; + // 二分压缩图片 + while (left <= right) { + const MID = Math.floor((left + right) / 2); + imageQuality = PACKING_ARRAY[MID]; + // 根据传入的图片质量参数进行packing压缩,返回压缩后的图片文件流数据。 + compressedImageData = await packing(sourcePixelMap, imageQuality); + // 判断查找一个尽可能接近但不超过压缩目标的压缩大小 + if (compressedImageData.byteLength <= maxCompressedImageByte) { + left = MID + 1; + if (MID === PACKING_ARRAY.length - 1) { + break; + } + // 获取下一次二分的图片质量参数(mid+1)压缩的图片文件流数据 + compressedImageData = await packing(sourcePixelMap, PACKING_ARRAY[MID + 1]); + // 判断用下一次图片质量参数(mid+1)压缩的图片大小是否大于指定图片的压缩目标大小。 + // 如果大于,说明当前图片质量参数(mid)压缩出来的图片大小最接近指定图片的压缩目标大小。传入当前图片质量参数mid,得到最终目标图片压缩数据。 + if (compressedImageData.byteLength > maxCompressedImageByte) { + compressedImageData = await packing(sourcePixelMap, PACKING_ARRAY[MID]); + break; + } + } else { + // 目标值不在当前范围的右半部分,将搜索范围的右边界向左移动,以缩小搜索范围并继续在下一次迭代中查找左半部分。 + right = MID - 1; + } + } + return compressedImageData; +} + + +/** + * 图片保存至沙箱路径 + * @param compressedImageData:压缩后的图片数据 + * @returns compressedImageInfo:返回压缩后的图片信息 + */ +async function saveImageToSandboxPath(compressedImageData: ArrayBuffer): Promise { + const CONTEXT: Context = getContext(); + // 定义要保存的压缩图片uri。afterCompressiona.jpeg表示压缩后的图片。 + const COMPRESSED_IMAGE_URI: string = CONTEXT.filesDir + '/' + 'afterCompressiona.jpeg'; + try { + const res = fileIo.accessSync(COMPRESSED_IMAGE_URI); + if (res) { + // 如果图片afterCompressiona.jpeg已存在,则删除 + fileIo.unlinkSync(COMPRESSED_IMAGE_URI); + } + } catch (err) { + hilog.error(0X0000, TAG, `AccessSync failed with error message: ${err.message}, error code: ${err.code}`); + } + // 获取最终图片压缩数据compressedImageData,保存图片。 + // 压缩图片数据写入文件 + const FILE: fileIo.File = fileIo.openSync(COMPRESSED_IMAGE_URI, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); + try { + fileIo.writeSync(FILE.fd, compressedImageData); + } catch (err) { + let error = err as BusinessError; + throw new Error(`Image saving failed: ${error.message}`); + } finally { + if (FILE) { + fileIo.closeSync(FILE); + } + } + // 获取压缩图片信息 + let compressedImageInfo: CompressedImageInfo = new CompressedImageInfo(); + compressedImageInfo.imageUri = COMPRESSED_IMAGE_URI; + compressedImageInfo.imageByteLength = compressedImageData.byteLength; + return compressedImageInfo; +} + +//图片保存至图库 +export async function saveImageToGallery() { + // try { + // const CONTEXT: Context = getContext(); + // // 定义要保存的压缩图片uri。afterCompressiona.jpeg表示压缩后的图片。 + // const COMPRESS_IMAGE_URI: string = CONTEXT.filesDir + '/' + 'afterCompressiona.jpeg'; + // let srcFileUris: Array = [ + // COMPRESS_IMAGE_URI + // ]; + // let photoCreationConfigs: Array = [ + // { + // title: 'test', // 可选。 + // fileNameExtension: 'jpg', + // photoType: photoAccessHelper.PhotoType.IMAGE, + // subtype: photoAccessHelper.PhotoSubtype.DEFAULT, // 可选。 + // } + // ]; + // // 获取最终图片压缩数据compressedImageData,保存图片。 + // // 压缩图片数据写入文件 + // // 基于弹窗授权的方式获取媒体库的目标uri。 + // let desFileUris: Array = await phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs); + // // 将来源于应用沙箱的照片内容写入媒体库的目标uri。 + // let desFile: fileIo.File = await fileIo.open(desFileUris[0], fileIo.OpenMode.WRITE_ONLY); + // let srcFile: fileIo.File = await fileIo.open(COMPRESS_IMAGE_URI, fileIo.OpenMode.READ_ONLY); + // const FILE: fileIo.File = + // fileIo.openSync(COMPRESS_IMAGE_URI, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); + // try { + // fileIo.writeSync(FILE.fd, compressedImageData); + // await fileIo.copyFile(srcFile.fd, desFile.fd); + // } catch (err) { + // let error = err as BusinessError; + // throw new Error(`Image saving failed: ${error.message}`); + // } finally { + // promptAction.showToast({ + // message: '保存成功', + // textColor: '#0A59F7', + // }) + // if (FILE) { + // fileIo.closeSync(FILE); + // } + // if (srcFile) { + // fileIo.closeSync(srcFile); + // } + // if (desFile) { + // fileIo.closeSync(desFile); + // } + // } + // } catch (err) { + // hilog.error(0X0000, TAG, `failed to save image to gallery, errCode is: ${err.code}, ${err.message}`); + // } + +} \ No newline at end of file diff --git a/features/patient/src/main/ets/components/AddAndEditRecordComp.ets b/features/patient/src/main/ets/components/AddAndEditRecordComp.ets index 9bf96eb..065f9c8 100644 --- a/features/patient/src/main/ets/components/AddAndEditRecordComp.ets +++ b/features/patient/src/main/ets/components/AddAndEditRecordComp.ets @@ -3,6 +3,7 @@ import { authStore, BasicConstant, ChangePhotoGrids, ChangeUtil, + compressedImage, hdHttp, HdLoadingDialog, HdNav, @@ -11,9 +12,11 @@ import { PhotoActionSheet, TimestampUtil, ViewImageInfo } from "@itcast/basic"; import { promptAction } from "@kit.ArkUI"; import { BusinessError } from "@kit.BasicServicesKit"; -import { values } from "@nimsdk/vendor"; -import { uri } from "@kit.ArkTS"; import { rcp } from "@kit.RemoteCommunicationKit"; +import { image } from "@kit.ImageKit"; +import { fileIo } from "@kit.CoreFileKit"; +import { buffer } from "@kit.ArkTS"; +import fs from '@ohos.file.fs'; @HMRouter({pageUrl:'AddAndEditRecordComp'}) @Component @@ -67,9 +70,11 @@ export struct AddAndEditRecordComp { this.creat_time = model.create_date this.maxSelectNumber = 8 - this.photos.length - ChangeUtil.convertUrisOrUrlsToBase64(model.photo).then(base64Array => { + this.dialog.open() + this.convertUrisOrUrlsToBase64(model.photo).then(base64Array => { console.info('转换结果:', base64Array+'转换个数:'+base64Array.length) this.base64Array = base64Array + this.dialog.close() }).catch((err:BusinessError) => { console.error('批量转换失败:', err) }) @@ -96,8 +101,7 @@ export struct AddAndEditRecordComp { "expert_uuid":authStore.getUser().uuid, "patient_uuid":this.params["patient_uuid"], "des":this.inputContent, - "create_date":this.creat_time, - // "img1":this.base64Array[0] + "create_date":this.creat_time }) this.base64Array.forEach((base64Str: string, index: number) => { if (index < 8) { @@ -258,7 +262,7 @@ export struct AddAndEditRecordComp { } this.photos.push(...selectedUris); this.maxSelectNumber = 8 - this.photos.length - ChangeUtil.convertUrisOrUrlsToBase64(this.photos).then(base64Array => { + this.convertUrisOrUrlsToBase64(this.photos).then(base64Array => { console.info('转换结果:', base64Array+'转换个数:'+base64Array.length) this.base64Array = base64Array }).catch((err:BusinessError) => { @@ -293,6 +297,251 @@ export struct AddAndEditRecordComp { } }) } + + // async convertUrisOrUrlsToBase64(items: string[]): Promise { + // const results: string[] = []; + // + // for (const item of items) { + // try { + // let arrayBuffer: ArrayBuffer; + // let mimeType = 'image/jpeg'; // 默认MIME类型 + // // 处理本地文件URI + // if (item.startsWith('file://')) {// || !item.includes('://') + // let imageBuffers: ArrayBuffer = new ArrayBuffer(1); + // const imagePackerApi = image.createImagePacker(); + // const packOpts: image.PackingOption = { format: "image/jpeg", quality: 1 }; + // let imageSource: image.ImageSource | undefined = undefined + // let fileSource = await fileIo.open(item, fileIo.OpenMode.READ_ONLY) + // imageSource = image.createImageSource(fileSource.fd) + // let compressedImageInfo = compressedImage(await imageSource.createPixelMap(), 200) + // let compressedImageUri = (await compressedImageInfo).imageUri + // // let compressedImageSize = (await compressedImageInfo).imageByteLength + // // let compressedsImage = this.loadCompressedImage(compressedImageUri) + // arrayBuffer = await this.readLocalFile(compressedImageUri) + // imageBuffers = await imagePackerApi.packing(imageSource, packOpts); + // let base64Str = buffer.from(imageBuffers).toString('base64') + // results.push(base64Str) + // imageSource.release() + // imagePackerApi.release() + // // arrayBuffer = await ChangeUtil.readLocalFile(item); + // } + // // 处理网络URL + // else if (item.startsWith('http://') || item.startsWith('https://') || ChangeUtil.isImageFileByRegex(item)) { + // arrayBuffer = await ChangeUtil.downloadNetworkResource(item); + // } + // // 处理其他类型资源 + // else { + // throw new Error(`Unsupported URI scheme: ${item}`); + // } + // // 关键优化:添加图片压缩步骤[6,8](@ref) + // // const compressedBuffer = await ChangeUtil.compression( + // // arrayBuffer, + // // mimeType, + // // 0.5 // 压缩质量为50% + // // ); + // // // 转换为Base64 + // // results.push(ChangeUtil.convertToBase64(compressedBuffer)); + // } catch (err) { + // console.error(`转换失败: ${JSON.stringify(err)}`); + // results.push(''); // 失败时返回空字符串 + // } + // } + // return results; + // } + // async loadCompressedImage(uri: string): Promise { + // try { + // let file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY); + // let imageSource = image.createImageSource(file.fd); + // return await imageSource.createPixelMap(); + // } catch (err) { + // let error = err as BusinessError; + // throw new Error(`Image loading failed: ${error.message}`); + // } + // } + // async readLocalFile(uri: string): Promise { + // try { + // // 打开文件 + // const file = fs.openSync(uri, fs.OpenMode.READ_ONLY); + // // 获取文件大小 + // const stat = fs.statSync(file.fd); + // const size = stat.size; + // // 创建缓冲区并读取数据 + // const buffer = new ArrayBuffer(size); + // fs.readSync(file.fd, buffer); + // // 关闭文件 + // fs.closeSync(file); + // return buffer; + // } catch (err) { + // throw new Error(`文件读取失败: ${JSON.stringify(err)}`); + // } + // } + async convertUrisOrUrlsToBase64(items: string[]): Promise { + const results: string[] = []; + const MAX_SIZE_KB = 500; // 最大500KB + + for (const item of items) { + try { + let base64Str: string; + + // 处理本地文件URI + if (item.startsWith('file://')) { + base64Str = await this.processLocalImage(item, MAX_SIZE_KB); + } + // 处理网络URL + else if (item.startsWith('http://') || item.startsWith('https://') || ChangeUtil.isImageFileByRegex(item)) { + base64Str = await this.processNetworkImage(item, MAX_SIZE_KB); + } + // 处理其他类型资源 + else { + throw new Error(`不支持的URI格式: ${item}`); + } + + results.push(base64Str); + } catch (err) { + console.error(`转换失败: ${JSON.stringify(err)}`); + results.push(''); // 失败时返回空字符串 + } + } + return results; + } + + // 处理本地图片 + private async processLocalImage(uri: string, maxSizeKB: number): Promise { + try { + // 创建图片源 + const fileSource = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY); + const imageSource = image.createImageSource(fileSource.fd); + + // 获取原始图片信息 + const pixelMap = await imageSource.createPixelMap(); + const originalWidth = pixelMap.getPixelBytesNumber(); + + // 计算压缩比例,目标500KB以内 + let quality = 0.8; + let targetWidth = 800; // 初始目标宽度 + + // 如果原始图片太大,逐步压缩 + if (originalWidth > maxSizeKB * 1024) { + quality = Math.min(0.6, (maxSizeKB * 1024) / originalWidth); + targetWidth = Math.min(800, Math.sqrt((maxSizeKB * 1024) / originalWidth) * 100); + } + + // 压缩图片 + const compressedImageInfo = await compressedImage(pixelMap, targetWidth); + const compressedUri = compressedImageInfo.imageUri; + + // 读取压缩后的图片 + const compressedBuffer = await this.readLocalFile(compressedUri); + + // 如果还是太大,进一步压缩 + if (compressedBuffer.byteLength > maxSizeKB * 1024) { + const imagePackerApi = image.createImagePacker(); + const packOpts: image.PackingOption = { + format: "image/jpeg", + quality: Math.max(0.3, quality * 0.8) // 进一步降低质量 + }; + + const finalBuffer = await imagePackerApi.packing(imageSource, packOpts); + const finalBase64 = buffer.from(finalBuffer).toString('base64'); + + // 清理资源 + imagePackerApi.release(); + imageSource.release(); + await fileIo.close(fileSource.fd); + + return finalBase64; + } + + // 转换为Base64 + const base64Str = buffer.from(compressedBuffer).toString('base64'); + + // 清理资源 + imageSource.release(); + await fileIo.close(fileSource.fd); + + return base64Str; + + } catch (err) { + throw new Error(`本地图片处理失败: ${JSON.stringify(err)}`); + } + } + + // 处理网络图片 + private async processNetworkImage(url: string, maxSizeKB: number): Promise { + try { + // 下载网络资源 + const arrayBuffer = await ChangeUtil.downloadNetworkResource(url); + + // 如果下载的图片太大,进行压缩 + if (arrayBuffer.byteLength > maxSizeKB * 1024) { + // 创建临时文件来压缩 + const tempUri = await this.saveToTempFile(arrayBuffer); + const compressedBase64 = await this.processLocalImage(tempUri, maxSizeKB); + + // 清理临时文件 + await this.deleteTempFile(tempUri); + + return compressedBase64; + } + + // 直接转换为Base64 + return buffer.from(arrayBuffer).toString('base64'); + + } catch (err) { + throw new Error(`网络图片处理失败: ${JSON.stringify(err)}`); + } + } + + // 保存到临时文件 + private async saveToTempFile(arrayBuffer: ArrayBuffer): Promise { + try { + // 使用应用缓存目录作为临时目录 + const context = getContext(this); + const tempDir = context.cacheDir; + const tempFileName = `temp_${Date.now()}.jpg`; + const tempPath = `${tempDir}/${tempFileName}`; + + const file = await fileIo.open(tempPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY); + await fileIo.write(file.fd, arrayBuffer); + await fileIo.close(file.fd); + + return tempPath; + } catch (err) { + throw new Error(`临时文件保存失败: ${JSON.stringify(err)}`); + } + } + + // 删除临时文件 + private async deleteTempFile(uri: string): Promise { + try { + await fileIo.unlink(uri); + } catch (err) { + console.warn(`临时文件删除失败: ${JSON.stringify(err)}`); + } + } + + // 优化后的本地文件读取方法 + async readLocalFile(uri: string): Promise { + try { + // 使用异步方式读取,避免阻塞 + const file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY); + const stat = await fileIo.stat(uri); + const size = stat.size; + + // 限制文件大小,避免内存溢出 + if (size > 10 * 1024 * 1024) { // 10MB限制 + throw new Error('文件过大,超过10MB限制'); + } + + const buffer = new ArrayBuffer(size); + await fileIo.read(file.fd, buffer); + await fileIo.close(file); + + return buffer; + } catch (err) { + throw new Error(`文件读取失败: ${JSON.stringify(err)}`); + } + } } interface recordList {