HarmonyOS 鸿蒙Next中压缩图片

HarmonyOS 鸿蒙Next中压缩图片 有没有压缩图片的示例demo?

4 回复
  1. 拉起图库选择要压缩的图片。使用photoAccessHelper.PhotoViewPicker创建图库选择器实例photoViewPicker,调用photoViewPicker.select()接口拉起图库界面进行图片选择,获取图片大小。
async selectPhotoFromAlbum() {
  // 创建图库选项实例
  const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
  // 设置选择的媒体文件类型为Image
  photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
  // 设置选择媒体文件的最大数目
  photoSelectOptions.maxSelectNumber = 1;
  // 创建图库选择器实例
  const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
  // 调用photoViewPicker.select()接口拉起图库界面进行图片选择,图片选择成功后,返回photoSelectResult结果集。
  photoViewPicker.select(photoSelectOptions).then((photoSelectResult) => {
    // select返回的uri权限是只读权限,需要将uri写入全局变量@State中即可进行读取文件数据操作。
    this.uris = photoSelectResult.photoUris;
    this.photoCount = this.uris.length;
    if (this.photoCount > 0) {
      const ALBUM_PATH: string = photoSelectResult.photoUris[0];
      // 找到最后一个点(.)的索引位置
      let lastDotIndex = ALBUM_PATH.lastIndexOf('.');
      // 使用slice方法从最后一个点之后的位置开始截取字符串到末尾
      this.beforeCompressFmt =
        ALBUM_PATH.slice(lastDotIndex + 1) === 'jpg' ? 'jpeg' : ALBUM_PATH.slice(lastDotIndex + 1);
      this.afterCompressFmt = this.beforeCompressFmt;
      // 读取选择图片的buffer
      const file = fs.openSync(ALBUM_PATH, fs.OpenMode.READ_ONLY);
      // 获取选择图片的字节长度
      this.beforeCompressByteLength = fs.statSync(file.fd).size;
      fs.closeSync(file);
    }
  }).catch((err: BusinessError) => {
    hilog.error(0x0000, TAG, `PhotoViewPicker.select failed :, error code: ${err.code}, message: ${err.message}.`);
  })
}
  1. 手动模式压缩图片。先获取从图库选择的图片uris,然后将图片数据读取到buffer。通过createImageSource(buffer)创建图片源实例,设置解码参数DecodingOptions,传入createPixelMap创建PixelMap图片对象originalPixelMap。使用scale进行图片尺寸压缩,使用packing进行图片质量压缩。
manualCompression() {
  const ALBUM_PATH: string = this.uris[0];
  const file = fs.openSync(ALBUM_PATH, fs.OpenMode.READ_ONLY);
  let buffer = new ArrayBuffer(fs.statSync(file.fd).size);
  fs.readSync(file.fd, buffer);
  fs.closeSync(file);
  const decodingOptions: image.DecodingOptions = { editable: true };
  const imageSource: image.ImageSource = image.createImageSource(buffer);
  imageSource.createPixelMap(decodingOptions).then(async (originalPixelMap: image.PixelMap) => {
    // 使用scale对图片进行缩放。入参分别为图片宽高的缩放倍数
    await originalPixelMap.scale(this.imageScaleVal / 100, this.imageScaleVal / 100);
    // savePixelMap用于把压缩后的图片保存到图库时使用。由于保存图片时调用的packToFile内部会做类似packing的处理,所以这里只保存scale缩放尺寸后的PixelMap。
    this.savePixelMap = originalPixelMap;
    // packing压缩图片
    let compressedImageData: ArrayBuffer =
      await this.packing(originalPixelMap, this.imageQualityVal, this.afterCompressFmt);
    // 压缩后的ArrayBuffer数据转PixelMap
    let imageSource = image.createImageSource(compressedImageData);
    let opts: image.DecodingOptions = { editable: true };
    // showPixelMap用于显示压缩后的图片
    this.showPixelMap = await imageSource.createPixelMap(opts);
    // showCompressFormat用于显示压缩后的图片格式
    this.showCompressFormat = this.afterCompressFmt;
    // 显示估算packing压缩后的图片大小。该图片在内存中作为ArrayBuffer数据的压缩后大小,这一数值并不直接等同于该图片在最终保存到相册时的实际文件大小。
    this.afterCompressionSize = (compressedImageData.byteLength / BYTE_CONVERSION).toFixed(1);
    promptAction.showToast({ message: $r('app.string.image_compression_compress_completed') });
  }).catch((err: BusinessError) => {
    hilog.error(0x0000, TAG, `Failed to create PixelMap, error code: ${err.code}, message: ${err.message}.`);
  });
}
  1. 自动模式(指定压缩目标大小)优先压缩图片尺寸。优先使用scale对图片进行尺寸缩放,采用while循环每次递减reduceScaleVal倍数(对应‘scale每次缩小倍数’)进行尺寸缩放,再用packing(其中图片质量参数quality根据‘最低图片质量’设置)获取压缩后的图片大小,最终查找压缩到最接近指定图片压缩目标的大小,并获取图片压缩数据用于后续图片保存。
async scalePriorityCompress(sourcePixelMap: image.PixelMap, maxCompressedImageSize: number, quality: number) {
  // scale压缩图片尺寸。采用while循环每次递减reduceScaleVal,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据。
  let imageScale = 1; // 定义图片宽高的缩放倍数,1表示原比例。
  const REDUCE_SCALE = this.reduceScaleVal;
  const AFTER_COMPRESS_FMT = this.afterCompressFmt;
  // 判断压缩后的图片大小是否大于指定图片的压缩目标大小,如果大于,继续降低缩放倍数压缩。
  while (compressedImageData.byteLength > maxCompressedImageSize * BYTE_CONVERSION) {
    if (imageScale > 0) {
      // 性能知识点:由于scale会直接修改图片PixelMap数据,所以不适用二分查找scale缩放倍数。这里采用循环递减reduceScaleVal缩放图片,
      // 来查找确定最适合的缩放倍数。如果对图片压缩质量要求不高,建议调高每次递减的缩放倍数,减少循环,提升scale压缩性能。
      imageScale = imageScale - REDUCE_SCALE; // 每次缩放倍数
      // 使用scale对图片尺寸进行缩放
      await sourcePixelMap.scale(imageScale, imageScale);
      // packing压缩
      compressedImageData = await this.packing(sourcePixelMap, quality, AFTER_COMPRESS_FMT);
    } else {
      // imageScale缩放倍数小于等于0时,没有意义,结束压缩。
      break;
    }
  }
}
  1. 自动模式(指定压缩目标大小)优先压缩图片质量。先判断设置图片质量参数为0时,packing能压缩到的图片最小字节大小compressedImageData.byteLength是否满足指定的图片压缩大小。 如果满足,则使用packing方式二分查找最接近指定图片压缩目标大小的quality来压缩图片。如果不满足,则图片质量按最低0进行设置,并调用scalePriorityCompress进行scale尺寸压缩。
// 优先压缩图片质量
async qualityPriorityCompress(sourcePixelMap: image.PixelMap, maxCompressedImageSize: number) {
  let compressedImageData: ArrayBuffer =
    await this.packing(sourcePixelMap, IMAGE_QUALITY_ZERO, this.afterCompressFmt);
  // 先判断图片质量参数设置最低0能否满足目标大小。如果能满足目标大小,则直接使用packing二分图片质量。如果不满足,则质量参数固定为0,进行scale尺寸压缩
  if (compressedImageData.byteLength <= maxCompressedImageSize * BYTE_CONVERSION) {
    // 满足目标大小,直接使用packing二分
    await this.packingImage(sourcePixelMap, compressedImageData, maxCompressedImageSize * BYTE_CONVERSION);
  } else {
    // 不满足目标大小,质量参数设置为0,再进行scale尺寸压缩
    await this.scalePriorityCompress(sourcePixelMap, maxCompressedImageSize, IMAGE_QUALITY_ZERO);
  }
  // 更新显示压缩后的图片格式
  this.showCompressFormat = this.afterCompressFmt;
}

// packing二分方式循环压缩
async packingImage(sourcePixelMap: image.PixelMap, compressedImageData: ArrayBuffer, maxCompressedImageByte: number) {
  let imageQuality: number = 0;
  const DICHOTOMY_ACCURACY = this.minBisectUnit;
  // 图片质量参数范围为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; // 定义二分搜索范围的右边界
  const AFTER_COMPRESS_FMT = this.afterCompressFmt;
  // 二分压缩图片
  while (left <= right) {
    const mid = Math.floor((left + right) / 2); // 定义二分搜索范围的中间位置
    imageQuality = packingArray[mid]; // 获取二分中间位置的图片质量值
    // 根据传入的图片质量参数进行packing压缩,返回压缩后的图片文件流数据。
    compressedImageData = await this.packing(sourcePixelMap, imageQuality, AFTER_COMPRESS_FMT);
    // 判断查找一个尽可能接近但不超过压缩目标的压缩大小
    if (compressedImageData.byteLength <= maxCompressedImageByte) {
      // 二分目标值在右半边,继续在更高的图片质量参数(即mid + 1)中搜索
      left = mid + 1;
      // 判断mid是否已经二分到最后,如果二分完了,退出
      if (mid === packingArray.length - 1) {
        break;
      }
      // 获取下一次二分的图片质量参数(mid+1)压缩的图片文件流数据
      compressedImageData = await this.packing(sourcePixelMap, packingArray[mid + 1], AFTER_COMPRESS_FMT);
      // 判断用下一次图片质量参数(mid+1)压缩的图片大小是否大于指定图片的压缩目标大小。如果大于,说明当前图片质量参数(mid)压缩出来的
      // 图片大小最接近指定图片的压缩目标大小。传入当前图片质量参数mid,得到最终目标图片压缩数据。
      if (compressedImageData.byteLength > maxCompressedImageByte) {
        compressedImageData = await this.packing(sourcePixelMap, packingArray[mid], AFTER_COMPRESS_FMT);
        break;
      }
    } else {
      // 目标值不在当前范围的右半部分,将搜索范围的右边界向左移动,以缩小搜索范围并继续在下一次迭代中查找左半部分。
      right = mid - 1;
    }
  }
  // ...
}
  1. 压缩后的图片数据保存到相册。通过photoAccessHelper.getPhotoAccessHelper获取相册管理模块的实例,使用createAsset创建图片资源,然后使用createImagePacker创建ImagePacker实例。 最后调用imagePacker.packToFile传入压缩后的PixelMap图片源,对应的图片格式和质量参数packOpts,编码后打包进图片文件,图片将自动保存到相册。需要说明packToFile内部会进行packing操作,所以传入packToFile的PixelMap对象只是scale尺寸缩放后的图片数据,最终需要压缩的图片质量通过packOpts进行设置。
async saveImageToAlbum(): Promise<void> {
  // 获取相册管理模块的实例
  const HELPER = photoAccessHelper.getPhotoAccessHelper(this.context);
  // 指定待创建的文件类型、后缀和创建选项,创建图片资源
  const URI = await HELPER.createAsset(photoAccessHelper.PhotoType.IMAGE, this.afterCompressFmt);
  let file = await fs.open(URI, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  let imagePacker = image.createImagePacker();
  let packOpts: image.PackingOption = {
    format: 'image/' + this.afterCompressFmt,
    quality: this.isAutoMode ? this.autoModeQuality : this.imageQualityVal
  };
  // 指定打包参数,将PixelMap图片源编码后直接打包进文件
  imagePacker.packToFile(this.savePixelMap, file.fd, packOpts, async (err: BusinessError) => {
    if (err) {
      hilog.error(0x0000, TAG, `Failed to pack the image to file, error code: ${err.code}, message: ${err.message}.`);
    } else {
      promptAction.showToast({ message: $r('app.string.image_compression_save_image_msg') });
    }
    // TODO知识点:使用packToFile方法,需要调用imagePacker.release主动释放imagePacker,打开图库时才能看到新存入的图片
    await fs.close(file.fd).finally(() => {
      imagePacker.release();
    })
  })
}

完整Demo示例:图片压缩方案

更多关于HarmonyOS 鸿蒙Next中压缩图片的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


/**
 *图片压缩
 */
import fileIo from '@ohos.file.fs';
import { image } from '@kit.ImageKit';
import { LogUtil } from './LogUtil';
import { AppUtil } from './AppUtil';
import { buffer } from '@kit.ArkTS';
import fs from '@ohos.file.fs';

export class CompressPhotoUtil {
  /**
   * 压缩图片
   * @param filePathOriginal 原始图片的uri 通过Picker选择器得到的uri
   * @param filePathCompress 压缩图片的地址
   * @param quality 压缩比例 0~100 百分比越小压缩程度越厉害
   * @returns 压缩图片的地址 地址为空时代表压缩失败
   */
  static async compressPhoto(filePathOriginal: string, quality?: number,filePathCompress?:string): Promise<string> {
    let fileOriginal: fileIo.File | undefined //原始文件
    let fileTemporary: fileIo.File | undefined //临时存储文件,针对原始文件无法压缩的问题
    let fileCompress: fileIo.File | undefined //压缩文件
    let imageSource: image.ImageSource | undefined
    let imagePacker: image.ImagePacker | undefined
    try {
      LogUtil.debug('压缩图片')
      LogUtil.debug('原始图片:'+filePathOriginal)
      imagePacker = image.createImagePacker()
      fileOriginal = fileIo.openSync(filePathOriginal)
      let nameOriginal = fileOriginal.name
      let type = nameOriginal.substring(nameOriginal.lastIndexOf('.') + 1) //文件类型
      let sizeOriginal = fileIo.statSync(fileOriginal.fd).size / 1000 //原始文件大小
      //目前压缩图片只支持jpeg、webp、png等格式,不是该格式的图片,需要先将原始图片保存到临时文件上,然后在使用临时文件进行压缩
      if (type != 'jpeg' && type != 'webp' && type != 'png') {
        type = 'jpeg'
        let filePathTemporary = AppUtil.getContext().cacheDir + "/" + 'temporary.'+type
        fileTemporary = fileIo.openSync(filePathTemporary, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
        //将原始图片复制到临时文件上,方便后续的压缩处理
        fileIo.copyFileSync(fileOriginal.fd, fileTemporary.fd)
        LogUtil.debug('临时图片:' + filePathTemporary)
        imageSource = image.createImageSource(fileTemporary.fd)
      } else {
        imageSource = image.createImageSource(fileOriginal.fd)
      }
      if(!filePathCompress){
        filePathCompress = `${AppUtil.getContext().filesDir}/compress${new Date().getTime()}.jpg`;
      }
      if (fileIo.accessSync(filePathCompress)) {
        fileIo.unlinkSync(filePathCompress)
      }
      fileCompress = fileIo.openSync(filePathCompress, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
      if (!quality) {
        quality = 50
        let sizeUnit = 1000
        if (sizeOriginal <= sizeUnit) {
          quality = 70
        }
        if (sizeUnit < sizeOriginal && sizeOriginal <= sizeUnit * 10) {
          quality = 30
        }
        if (sizeUnit * 10 < sizeOriginal) {
          quality = 10
        }
      }
      //开始压缩图片
      await imagePacker.packToFile(imageSource, fileCompress.fd, { format: "image/" + type, quality: quality })
      //释放资源
      fileIo.closeSync(fileOriginal.fd)
      fileIo.closeSync(fileCompress.fd)
      if(fileIo.accessSync(filePathCompress)&&fileTemporary){
        fileIo.closeSync(fileTemporary.fd)
      }
      LogUtil.debug('压缩比例:' + quality)
      LogUtil.debug('压缩图片:'+filePathCompress)
      return filePathCompress
    } catch (err) {
      LogUtil.debug('压缩图片异常:' + JSON.stringify(err))
      return ''
    } finally {
      LogUtil.debug('图片压缩结束')
    }

}

  static  async getImageBase64WithUri(uri:string): Promise<string> {
    const file = await fs.open(uri, fs.OpenMode.READ_ONLY);
    const imageSource : image.ImageSource = image.createImageSource(file.fd);
    const imagePackerApi = image.createImagePacker();
    // 设置打包参数
    // format:当前仅支持打包为JPEG、WebP 和 png 格式
    // quality:JPEG 编码输出图片质量
    // bufferSize:图片大小,默认 10M
    const packOpts: image.PackingOption = { format: "image/jpeg", quality: 100 };
    let imageBuffer: ArrayBuffer = new ArrayBuffer(1);
    let resultBase64Str = ''
    try {
      // 图片压缩或重新打包
      imageBuffer = await imagePackerApi.packing(imageSource, packOpts);
      let base64Str = buffer.from(imageBuffer).toString('base64')
      resultBase64Str=base64Str

    } catch (err) {
      console.error(`Invoke getImageArrayBufferWithUri failed, err: ${JSON.stringify(err)}`);
    }
    return resultBase64Str;
  }
}

在HarmonyOS Next中,可通过image模块的imagePacker接口实现图片压缩。使用createImagePacker()初始化打包器,设置格式参数如JPEG质量比,调用packing()方法处理源图像数据并输出压缩后的字节流。支持调整尺寸和质量参数,压缩过程不依赖Java或C,仅用ArkTS编写。

在HarmonyOS Next中,可以使用image模块的ImagePacker和压缩选项来实现图片压缩。以下是一个简单的示例:

import image from '@ohos.multimedia.image';

// 获取图片源(例如从文件或资源)
let imageSource = ...; // 通过image.createImageSource()获取

// 创建打包选项并设置压缩质量
let packOpts = {
  format: "image/jpeg", // 输出格式
  quality: 80 // 压缩质量(0-100)
};

// 创建ImagePacker实例
let imagePacker = image.createImagePacker();

// 执行压缩
imagePacker.packing(imageSource, packOpts)
  .then(data => {
    // 处理压缩后的数据(ArrayBuffer)
    console.log('压缩成功');
  })
  .catch(error => {
    console.error('压缩失败: ' + error);
  });

关键点:

  • 通过quality参数控制压缩质量
  • 支持JPEG/PNG等常见格式
  • 需要先获取ImageSource对象

建议查阅官方文档中ImagePacker的完整API说明,了解更多的配置选项和错误处理方式。

回到顶部