鸿蒙其实提供了两种图片压缩方式
1、尺寸压缩:scale/scaleSync
说明:根据输入的宽高的缩放倍数对图片进行缩放。
注意:
建议宽高的缩放倍数取非负数,否则会产生翻转效果。
宽高的缩放倍数 = 缩放后的图片宽高 / 缩放前的图片宽高。
2、质量压缩:packing
说明:将图像压缩或重新编码。
注意:
仅对JPEG图片和HEIF图片生效,WebP、PNG等图片均为无损编码无法压缩。
因为jpeg不支持透明通道,若使用带透明通道的数据编码jpeg格式,透明色将变为黑色。
尺寸压缩:
//图片压缩
scaleImage(images: image.PixelMap, scaleSize: number | null): Promise<image.PixelMap> {
return new Promise(async (resolve, reject) => {
if (!scaleSize) {
scaleSize = 500;
}
try {
let minImageData = await this.scaleSize(images,scaleSize)
let imageSource = image.createImageSource(minImageData);
let opts: image.DecodingOptions = { editable: true };
let showPixelMap = await imageSource.createPixelMap(opts);
resolve(showPixelMap);
}
catch (errCode) {
reject(errCode);
}
})
}
/**
* 压缩图片尺寸 * @param sourcePixelMap 原始待压缩图片的PixelMap
* @param maxCompressedImageByte 压缩目标图像字节长度 * @param quality 质量参数0~100
*/
async scaleSize(sourcePixelMap: image.PixelMap, maxCompressedImageByte: number): Promise<ArrayBuffer>{
let imageScale = 1; // 定义图片宽高的缩放倍数,1表示原比例。
let imageData = await this.packing(sourcePixelMap,100)
while (imageData.byteLength / 1024 > maxCompressedImageByte ){
if (imageScale > 0) {
if (imageData.byteLength / 1024 > maxCompressedImageByte * 2) {
imageScale -= 0.5;
}else {
imageScale -= 0.1;
}
sourcePixelMap.scaleSync(imageScale, imageScale)
imageData = await this.packing(sourcePixelMap,quality)
}else {
break;
}
}
return imageData;
}
/**
* packing压缩 只有“image/jpeg"有效 * @param sourcePixelMap 原始待压缩图片的PixelMap
* @param imageQuality 图片质量参数,只针对“image/jpeg"有效 * @param packingFormat 目标格式。当前只支持"image/jpeg"、"image/webp" 和"image/png"
* @returns data 返回压缩后的图片数据*/
async packing(sourcePixelMap: image.PixelMap, imageQuality: number ): Promise<ArrayBuffer> {
const imagePackerApi = image.createImagePacker();
const packOpts: image.PackingOption = { format: 'image/' + 'jpeg', quality: imageQuality };
const data: ArrayBuffer = await imagePackerApi.packing(sourcePixelMap, packOpts);
return data;
}
实际看下效果,我使用原图(3072 X 4096 8M)压缩到500k看下效果:

使用尺寸压缩500k后的图片效果:

发现尺寸压缩后图片的质量非常差,图片模糊文字基本已经无法识别,这时候看看图片的参数(666 x 888 488 kB),这样失真的图片肯定无法使用。
再试试质量压缩的效果:
这里我们先调用this.packing()将图片压缩到最小质量(this.minQuality是设置的最小质量值,取值区间在[0,100],如果太小图片辨识图可能会被破坏,这里我取最小值为15),然后判断最小质量压缩的大小minImageData.byteLength与预期大小scaleSize比较:
1、如果minImageData.byteLength/ 1024比scaleSize小,我们只需要压缩质量就能达到想要的大小。
调用this.packingImage(),使用二分法算法找出最接近scaleSize大小的压缩比例并压缩图片。
2、如果minImageData.byteLength/ 1024比scaleSize还大,我们就需要使用this.packing()将质量压缩到最小,然后再调用尺寸压缩,最大程度保障图片清晰度。
调用this.scaleSize()找出最接近scaleSize大小的压缩比例并压缩图片,这里的this.scaleSize()和上面的方法有所差别,这里显示将图片使用质量压缩到最后后再去做尺寸压缩
我们看看最终的效果

看两个最终压缩后的图片效果,差别相当明显,使用质量压缩的图片只是内存大小减小尺寸没有变化,图片内容清晰可见,这样就完美解决压缩图片的问题
具体实现如下:
//图片压缩
//@scaleSize 预期大小 单位kB
scaleImage(images: image.PixelMap, scaleSize: number | null): Promise<image.PixelMap> {
return new Promise(async (resolve, reject) => {
if (!scaleSize) {
scaleSize = 500;
}
try {
//先取图片质量压缩到最小时图片的大小
let minImageData = await this.packing(images,this.minQuality)
if (minImageData.byteLength / 1024 > scaleSize) {
//质量压缩到最小还无法满足,就需要进行尺寸压缩
minImageData = await this.scaleSize(images,scaleSize,this.minQuality)
}else {
//使用算法计算出最接近scaleSize的压缩图片
minImageData= await this.packingImage(images,scaleSize,this.minQuality)
}
let imageSource = image.createImageSource(minImageData);
let opts: image.DecodingOptions = { editable: true };
let showPixelMap = await imageSource.createPixelMap(opts);
resolve(showPixelMap);
}
catch (errCode) {
reject(errCode);
}
})
}
/**
* 压缩图片尺寸 * @param sourcePixelMap 原始待压缩图片的PixelMap
* @param maxCompressedImageByte 压缩目标图像字节长度 * @param quality 质量参数0~100
*/
async scaleSize(sourcePixelMap: image.PixelMap, maxCompressedImageByte: number,quality: number): Promise<ArrayBuffer>{
let imageScale = 1; // 定义图片宽高的缩放倍数,1表示原比例。
let imageData = await this.packing(sourcePixelMap,quality)//获取最小质量压缩后的图
if (imageData.byteLength / 1024 <= maxCompressedImageByte + 100) {
return imageData;
}
while (imageData.byteLength / 1024 > maxCompressedImageByte + 100){
if (imageScale > 0) {
if (imageData.byteLength / 1024 > maxCompressedImageByte * 2) {
imageScale -= 0.5;
}else {
imageScale -= 0.1;
}
sourcePixelMap.scaleSync(imageScale, imageScale)
imageData = await this.packing(sourcePixelMap,quality)
}else {
break;
}
}
return imageData;
}
/**
* packing二分方式循环压缩 * @param sourcePixelMap 原始待压缩图片的PixelMap
* @param maxCompressedImageByte 压缩目标图像字节长度*/
async packingImage(sourcePixelMap: image.PixelMap, maxCompressedImageByte: number ,minQuality: number) : Promise<ArrayBuffer>{
let imageQuality: number = 0;//图片质量
const DICHOTOMY_ACCURACY = 5;//二分法精度,越小越精确
let compressedImageData = await this.packing(sourcePixelMap, 100);
// 图片质量参数范围为0-100,这里以minBisectUnit为最小二分单位创建用于packing二分图片质量参数的数组。
const packingArray: number[] = [];
// 性能知识点: 如果对图片压缩质量要求不高,建议调高minBisectUnit(对应‘packing最小二分单位’),减少循环,提升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 this.packing(sourcePixelMap, imageQuality);
// 判断查找一个尽可能接近但不超过压缩目标的压缩大小
if (compressedImageData.byteLength/1024 <= maxCompressedImageByte) {
// 二分目标值在右半边,继续在更高的图片质量参数(即mid + 1)中搜索
left = mid + 1;
// 判断mid是否已经二分到最后,如果二分完了,退出
if (mid === packingArray.length - 1) {
break;
}
// 获取下一次二分的图片质量参数(mid+1)压缩的图片文件流数据
compressedImageData = await this.packing(sourcePixelMap, packingArray[mid + 1]);
// 判断用下一次图片质量参数(mid+1)压缩的图片大小是否大于指定图片的压缩目标大小。如果大于,说明当前图片质量参数(mid)压缩出来的
// 图片大小最接近指定图片的压缩目标大小。传入当前图片质量参数mid,得到最终目标图片压缩数据。
if (compressedImageData.byteLength/1024 > maxCompressedImageByte) {
compressedImageData = await this.packing(sourcePixelMap, packingArray[mid]);
break;
}
} else {
// 目标值不在当前范围的右半部分,将搜索范围的右边界向左移动,以缩小搜索范围并继续在下一次迭代中查找左半部分。
right = mid - 1;
}
}
return compressedImageData;
}
/**
* packing压缩 只有“image/jpeg"有效 * @param sourcePixelMap 原始待压缩图片的PixelMap
* @param imageQuality 图片质量参数,只针对“image/jpeg"有效 * @param packingFormat 目标格式。当前只支持"image/jpeg"、"image/webp" 和"image/png"
* @returns data 返回压缩后的图片数据*/
async packing(sourcePixelMap: image.PixelMap, imageQuality: number ): Promise<ArrayBuffer> {
const imagePackerApi = image.createImagePacker();
const packOpts: image.PackingOption = { format: 'image/' + 'jpeg', quality: imageQuality };
const data: ArrayBuffer = await imagePackerApi.packing(sourcePixelMap, packOpts);
return data;
}