上传图片
This commit is contained in:
@@ -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<CompressedImageInfo> {
|
||||
// 创建图像编码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<ArrayBuffer> {
|
||||
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<ArrayBuffer> {
|
||||
// 图片质量参数范围为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<CompressedImageInfo> {
|
||||
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<string> = [
|
||||
// COMPRESS_IMAGE_URI
|
||||
// ];
|
||||
// let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [
|
||||
// {
|
||||
// title: 'test', // 可选。
|
||||
// fileNameExtension: 'jpg',
|
||||
// photoType: photoAccessHelper.PhotoType.IMAGE,
|
||||
// subtype: photoAccessHelper.PhotoSubtype.DEFAULT, // 可选。
|
||||
// }
|
||||
// ];
|
||||
// // 获取最终图片压缩数据compressedImageData,保存图片。
|
||||
// // 压缩图片数据写入文件
|
||||
// // 基于弹窗授权的方式获取媒体库的目标uri。
|
||||
// let desFileUris: Array<string> = 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}`);
|
||||
// }
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user