【解决方案】harmonyOS 图片压缩

图片压缩在应用开发中是一个非常常见的需求,特别是在处理用户上传图片时,需要上传指定大小以内的图片。目前图片压缩支持jpeg、webp、png格式。本例中以jpeg图片为例介绍如何通过packing和scale实现图片压缩到目标大小以内

使用说明

  1. 进入页面,输入图片压缩目标大小,点击“图片压缩”按钮查看压缩后的图片。效果图中输入图片压缩目标大小为10kb,实际压缩小于等于10kb

实现思路

  1. 获取图片。从资源管理器获取要压缩的图片,创建ImageSource实例,设置解码参数DecodingOptions,使用createPixelMap获取PixelMap图片对象。

    // 获取压缩前图片大小,用于页面上图片显示
      async aboutToAppear() {
        // this.compress()
    
        const context: Context = getContext(this);
        const resourceMgr: resourceManager.ResourceManager = context.resourceManager;
        // 获取待压缩的图片
    
        resourceMgr.getRawFileContent(this.imageSrc).then((fileData: Uint8Array) => {
          // 获取图片的ArrayBuffer
          const buffer = fileData.buffer.slice(0);
          this.sourceImageByteLength = buffer.byteLength;
          this.beforeCompressionSize = (this.sourceImageByteLength / BYTE_CONVERSION).toFixed(1);
        }).catch((err: BusinessError) => {
          console.log(TAG, `Failed to get RawFileContent with error message: ${err.message}, error code: ${err.code}`);
        });
      }
  2. 图片压缩。先判断设置图片质量参数quality为0时,packing能压缩到的图片最小字节大小是否满足指定的图片压缩大小。如果满足,则使用packing方式二分查找最接近指定图片压缩目标大小的quality来压缩图片。如果不满足,则使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据 

  3. /**
       * 从rawfile中获取需要压缩的图片image_compression_before.jpeg,压缩
       */
      imageCompression(): void {
        // 获取图片。从资源管理器获取要压缩的图片,创建ImageSource实例,设置解码参数DecodingOptions,使用createPixelMap获取PixelMap图片对象。
        // 获取resourceManager资源管理器
        const resourceMgr: resourceManager.ResourceManager = this.context.resourceManager;
        // 获取资源管理器后,再调用resourceMgr.getRawFileContent()获取资源文件的ArrayBuffer。
        resourceMgr.getRawFileContent(this.imageSrc).then((fileData: Uint8Array) => {
          // 获取图片的ArrayBuffer
          const buffer = fileData.buffer.slice(0);
          // 创建ImageSource实例
          const imageSource: image.ImageSource = image.createImageSource(buffer);
          // 设置解码参数DecodingOptions,解码获取PixelMap图片对象。
          const decodingOptions: image.DecodingOptions = {
            sampleSize: 1, // 缩略图采样大小
            rotate: 0, // 旋转角度
            editable: true, // 是否可编辑。当取值为false时,图片不可二次编辑,如crop等操作将失败
            desiredSize: { width: 1000, height: 2000 }, // 期望输出大小image.size
            desiredPixelFormat: 3, // 解码的像素格式。3表示RGBA_8888
          }
          // 创建pixelMap
          imageSource.createPixelMap(decodingOptions).then((originalPixelMap: image.PixelMap) => {
            this.beforeImgSrc = originalPixelMap
            // 压缩图片
            imageCompressionMethod.compressedImage(originalPixelMap, this.maxCompressedImageSize, 0)
              .then((showImage: CompressedImageInfo) => {
                // 获取压缩后的图片信息
    
                this.compressedImageSrc = fileUri.getUriFromPath(showImage.imageUri);
                this.compressedByteLength = showImage.imageByteLength;
                this.afterCompressionSize = (this.compressedByteLength / BYTE_CONVERSION).toFixed(1);
                // 图片压缩后的大小如果未能达到指定压缩目标大小。提示修改代码中的图片缩小倍数(REDUCE_SCALE),以便进一步压缩图片大小。
                if (this.compressedByteLength / BYTE_CONVERSION > this.maxCompressedImageSize) {
                  AlertDialog.show({
                    message: '图片压缩后的大小未能达到指定压缩目标大小。请尝试修改代码中的图片缩小倍数(REDUCE_SCALE),以便进一步压缩图片大小',
                    alignment: DialogAlignment.Center
                  });
                }
              })
          }).catch((err: BusinessError) => {
            console.log(TAG, `Failed to create PixelMap with error message: ${err.message}, error code: ${err.code}`);
          });
        }).catch((err: BusinessError) => {
          console.log(TAG, `Failed to get RawFileContent with error message: ${err.message}, error code: ${err.code}`);
        });
      }

4.压缩图片方法

//button一压缩图片
  compressedImage() {
    if (this.maxCompressedImageSize === 0) {
      AlertDialog.show({
        message: "请输入大于0的值",
        alignment: DialogAlignment.Center
      });
      return;
    }
    if (this.maxCompressedImageSize * BYTE_CONVERSION > this.sourceImageByteLength) {
      if (this.sourceImageByteLength === 0) {
        AlertDialog.show({
          message: "图片获取失败",
          alignment: DialogAlignment.Center
        });
      } else {
        AlertDialog.show({
          message: "符合压缩要求,无需压缩",
          alignment: DialogAlignment.Center
        });
      }
      return;
    }
    // 重置压缩后的图片路径
    this.compressedImageSrc = '';
    // 从rawfile中获取图片,图片压缩
    this.imageCompression();
  }

 5.相册图片选择

//button相册选择图片压缩
  async getPictureSelect() {
    //  1. 实例化选择参数对象
    const options = new picker.PhotoSelectOptions()
    options.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE // 表示从相册中选择图片
    options.maxSelectNumber = 1 // 表示只选择一张

    //   2. 实例化选择器对象
    const pickerView = new picker.PhotoViewPicker()
    //   3. 调用选择器对象上的select方法传入参数对象即可完成选择
    let res = await pickerView.select(options)

    //   4. 判断用户取消了选择图片,则组织下面代码的继续运行
    if (res.photoUris.length === 0) {
      promptAction.showToast({ message: "用户取消图片选择" })
      return
    }
    //   4.1 准备好一个图片的完整路径
    let ext = 'jpg' // 图片扩展名
    let fileName = Date.now().toString() // 图片名称
    let cacheDir = getContext().cacheDir // 获取应用程序的缓存目录
    let fullPath = cacheDir + '/' + fileName + '.' + ext // 完整的图片路径
    //   4.2 利用fileIo拷贝图片
    let file = fileIo.openSync(res.photoUris[0], fileIo.OpenMode.READ_ONLY)
    fileIo.copyFileSync(file.fd, fullPath)

    let imgResource = image.createImageSource(fullPath)

    // 设置解码参数DecodingOptions,解码获取PixelMap图片对象。
    let originalPixelMap = await imgResource.createPixelMap()

    imageCompressionMethod.compressedImage(originalPixelMap, this.maxCompressedImageSize, 0)
      .then((showImage: CompressedImageInfo) => {
        AlertDialog.show({ message: `showImage.imageUri:${showImage.imageUri}` })
        // 获取压缩后的图片信息
        this.compressedImageSrc = fileUri.getUriFromPath(showImage.imageUri);
        this.compressedByteLength = showImage.imageByteLength;
        this.afterCompressionSize = (this.compressedByteLength / BYTE_CONVERSION).toFixed(1);
        // 图片压缩后的大小如果未能达到指定压缩目标大小。提示修改代码中的图片缩小倍数(REDUCE_SCALE),以便进一步压缩图片大小。
        if (this.compressedByteLength / BYTE_CONVERSION > this.maxCompressedImageSize) {
          AlertDialog.show({
            message: '图片压缩后的大小未能达到指定压缩目标大小。请尝试修改代码中的图片缩小倍数(REDUCE_SCALE),以便进一步压缩图片大小',
            alignment: DialogAlignment.Center
          });
        }
      }).catch((err: BusinessError) => {
      console.log(TAG, `Failed to create PixelMap with error message: ${err.message}, error code: ${err.code}`);
    });
  }

6.ArkUI渲染

  build() {
    Column({ space: SPACE_TEN }) {
      this.titleBar();

      Row({ space: SPACE_TEN }) {
        Text("输入图片压缩目标大小(kb):")
          .fontSize(20)
        TextInput({
          placeholder: '',
          text: $$this.strMaxCompressedImageSize // $$运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。
        })
          .type(InputType.NUMBER_DECIMAL)// 带小数点的数字输入模式。支持数字,小数点(只能存在一个小数点)。
          .fontSize(20)
          .width("30%")
          .onChange((value: string) => {
            // 由于TextInput组件的InputType.NUMBER_DECIMAL能力对输入的0个数和'.'的位置没有限制,会存在'000.8','008'和'.008'也能输入的情况,所以需要手动限制'0'和'.'。
            if (value.charAt(0) === '.') {
              // 如果字符串第一个字符是'.'时,TextInput的值重置为''
              this.strMaxCompressedImageSize = '';
            } else if (value.charAt(0) === '0' && value.length > 1 && value.charAt(1) !== '.') {
              // value长度为2时,第一个字符是'0',第二个字符输入还是'0',则TextInput重置为'0'。否则,用空字符串替换字符串开头所有0。比如,数字8006,删除'8'后,TextInput显示为6。
              this.strMaxCompressedImageSize =
                (value.length === 2 && value.charAt(1) === '0') ? '0' : value.replace(/^0+/, '');
            } else {
              this.strMaxCompressedImageSize = value;
            }
            this.maxCompressedImageSize = Number(value);
          })
      }

      Row() {
        Button("压缩图片")
          .width("30%")
          .onClick(() => {
            this.compressedImage()
          })
        Button('选择图片')
          .width("30%")
          .onClick(() => {
            this.getPictureSelect()
          })
      }.width('100%').justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Text("压缩前图片大小(kb): ")
          .fontSize(20)
        Text(this.beforeCompressionSize)
          .fontSize(20)
      }

      Image(this.beforeImgSrc)// Image(this.imageSrc)
        .width("100%")
        .height("30%")
        .objectFit(ImageFit.Contain)

      Row() {
        Text("压缩后图片大小(kb): ")
          .fontSize(20)
        Text(this.afterCompressionSize)
          .fontSize(20)
      }

      Image(this.compressedImageSrc)
        .objectFit(ImageFit.Contain)
        .width("100%")
        .height("30%")
    }.alignItems(HorizontalAlign.Start).padding({ left: 20, right: 20 })
  }

  @Builder
  titleBar() {
    Row() {
      Image($r("app.media.ic_left_row"))
        .width(22)
        .aspectRatio(1)

        .margin({ left: 15 })
        .onClick(() => {
          router.back()
        })
    }
    .width("100%")
    .margin({ bottom: 20 })
    .justifyContent(FlexAlign.SpaceBetween)
  }

7.图片压缩、图片保存类ImageCompressionMethod方法封装

import { image } from '@kit.ImageKit'
import { fileIo } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';


const TAG = 'Image_Compress';
const BYTE_CONVERSION: number = 1024; // 字节转换

// 压缩后的图片信息类,用于刷新显示压缩后的图片和图片字节长度
export class CompressedImageInfo {
  imageUri: string = ""; // 压缩后图片保存位置的uri
  imageByteLength: number = 0; // 压缩后图片字节长度
  imageBase64: string = ''
}


class ImageCompressionMethod {
  // 设置解码参数DecodingOptions
  private decodingOptions: image.DecodingOptions = {
    sampleSize: 1, // 缩略图采样大小
    rotate: 0, // 旋转角度
    editable: true, // 是否可编辑。当取值为false时,图片不可二次编辑,如crop等操作将失败
    desiredSize: { width: 1920, height: 1080 }, // 期望输出大小image.size
    desiredPixelFormat: 3, // 解码的像素格式。3表示RGBA_8888
  }

  //根据传入图片url进行压缩,返回压缩后的图片info
  public async urlImageCompress(url: string, maxCompressedImageSize: number, isBase64: number) {
    let afterComPreImgInfo: CompressedImageInfo = { imageUri: '', imageByteLength: 0, imageBase64: '' }
    // 创建pixelMap后进行图片压缩
    try {
      // 打开文件创建pixelMap
      let file = fileIo.openSync(url, fileIo.OpenMode.READ_ONLY)
      const imgResource = image.createImageSource(file.fd);
      let originalPixelMap = await imgResource.createPixelMap(this.decodingOptions)
      afterComPreImgInfo = await this.compressedImage(originalPixelMap, maxCompressedImageSize, isBase64)
      return afterComPreImgInfo
    } catch (err) {
      console.log(TAG, `Failed to create PixelMap with error message: ${err.message}, error code: ${err.code}`);
    }

    return afterComPreImgInfo
  }

  /**
   * 图片压缩,保存
   * @param sourcePixelMap:原始待压缩图片的PixelMap对象
   * @param maxCompressedImageSize:指定图片的压缩目标大小,单位kb
   * @returns compressedImageInfo:返回最终压缩后的图片信息
   */
  public async compressedImage(sourcePixelMap: image.PixelMap, maxCompressedImageSize: number,
    isBase64: number): Promise<CompressedImageInfo> {
    // 创建图像编码ImagePacker对象
    const imagePackerApi = image.createImagePacker();
    // 定义图片质量参数
    const IMAGE_QUALITY = 0;
    // 设置编码输出流和编码参数。图片质量参数quality范围0-100。
    const packOpts: image.PackingOption = { format: "image/jpeg", quality: IMAGE_QUALITY };
    // 通过PixelMap进行编码。compressedImageData为打包获取到的图片文件流。
    let compressedImageData: ArrayBuffer = await imagePackerApi.packing(sourcePixelMap, packOpts);
    // 压缩目标图像字节长度
    const maxCompressedImageByte = maxCompressedImageSize * BYTE_CONVERSION;
    // 图片压缩。先判断设置图片质量参数quality为0时,packing能压缩到的图片最小字节大小是否满足指定的图片压缩大小。如果满足,则使用packing方式二分查找最接近指定图片压缩目标大小的quality来压缩图片。如果不满足,则使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据。
    let base64: string = ''
    if (maxCompressedImageByte > compressedImageData.byteLength) {
      // 使用packing二分压缩获取图片文件流
      compressedImageData =
        await this.packingImage(compressedImageData, sourcePixelMap, IMAGE_QUALITY, maxCompressedImageByte);
      if (isBase64 === 1) {
        let arrayBuffer = new Uint8Array(compressedImageData)
        let help = new util.Base64Helper
        base64 = help.encodeToStringSync(arrayBuffer)
      }
    } else {
      // 使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据
      let imageScale = 1; // 定义图片宽高的缩放倍数,1表示原比例。
      const REDUCE_SCALE = 0.4; // 图片缩小倍数
      // 判断压缩后的图片大小是否大于指定图片的压缩目标大小,如果大于,继续降低缩放倍数压缩。
      while (compressedImageData.byteLength > maxCompressedImageByte) {
        if (imageScale > 0) {
          // 性能知识点: 由于scale会直接修改图片PixelMap数据,所以不适用二分查找scale缩放倍数。这里采用循环递减0.4倍缩放图片,来查找确定最适
          // 合的缩放倍数。如果对图片压缩质量要求不高,建议调高每次递减的缩放倍数reduceScale,减少循环,提升scale压缩性能。
          imageScale = imageScale - REDUCE_SCALE; // 每次缩放倍数减0.4
          // 使用scale对图片进行缩放
          await sourcePixelMap.scale(imageScale, imageScale);
          // packing压缩
          compressedImageData = await this.packing(sourcePixelMap, IMAGE_QUALITY);
          if (isBase64 === 1) {
            let arrayBuffer = new Uint8Array(compressedImageData)
            let help = new util.Base64Helper
            base64 = help.encodeToStringSync(arrayBuffer)
          }
        } else {
          // imageScale缩放小于等于0时,没有意义,结束压缩。这里不考虑图片缩放倍数小于reduceScale的情况。
          break;
        }
      }
    }
    // 保存图片,返回压缩后的图片信息。
    const compressedImageInfo: CompressedImageInfo = await this.saveImage(compressedImageData, base64);
    return compressedImageInfo;
  }

  /**
   * 图片保存
   * @param compressedImageData:压缩后的图片数据
   * @returns compressedImageInfo:返回压缩后的图片信息
   */
  private async saveImage(compressedImageData: ArrayBuffer, base64: string): Promise<CompressedImageInfo> {
    const context: Context = getContext();
    // 定义要保存的压缩图片uri。image_compression_after.jpeg表示压缩后的图片。
    const compressedImageUri: string = context.filesDir + '/' + `${Date.now()}.jpeg`;
    try {
      const res = fileIo.accessSync(compressedImageUri);
      if (res) {
        // 如果图片image_compression_after.jpeg已存在,则删除
        fileIo.unlinkSync(compressedImageUri);
      }
    } catch (err) {
      console.log(TAG, `AccessSync failed with error message: ${err.message}, error code: ${err.code}`);
    }

    // 压缩图片数据写入文件
    const file: fileIo.File = fileIo.openSync(compressedImageUri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
    fileIo.writeSync(file.fd, compressedImageData);
    fileIo.closeSync(file);
    // 获取压缩图片信息
    let compressedImageInfo: CompressedImageInfo = new CompressedImageInfo();
    compressedImageInfo.imageUri = compressedImageUri;
    compressedImageInfo.imageByteLength = compressedImageData.byteLength;
    compressedImageInfo.imageBase64 = base64
    return compressedImageInfo;
  }

  /**
   * packing压缩
   * @param sourcePixelMap:原始待压缩图片的PixelMap
   * @param imageQuality:图片质量参数
   * @returns data:返回压缩后的图片数据
   */
  private 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;
  }

  /**
   * packing二分方式循环压缩
   * @param compressedImageData:图片压缩的ArrayBuffer
   * @param sourcePixelMap:原始待压缩图片的PixelMap
   * @param imageQuality:图片质量参数
   * @param maxCompressedImageByte:压缩目标图像字节长度
   * @returns compressedImageData:返回二分packing压缩后的图片数据
   */
  private async packingImage(compressedImageData: ArrayBuffer, sourcePixelMap: image.PixelMap, imageQuality: number,
    maxCompressedImageByte: number): Promise<ArrayBuffer> {
    // 图片质量参数范围为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 this.packing(sourcePixelMap, imageQuality);
      // 判断查找一个尽可能接近但不超过压缩目标的压缩大小
      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]);
        // 判断用下一次图片质量参数(mid+1)压缩的图片大小是否大于指定图片的压缩目标大小。如果大于,说明当前图片质量参数(mid)压缩出来的
        // 图片大小最接近指定图片的压缩目标大小。传入当前图片质量参数mid,得到最终目标图片压缩数据。
        if (compressedImageData.byteLength > maxCompressedImageByte) {
          compressedImageData = await this.packing(sourcePixelMap, packingArray[mid]);
          break;
        }
      } else {
        // 目标值不在当前范围的右半部分,将搜索范围的右边界向左移动,以缩小搜索范围并继续在下一次迭代中查找左半部分。
        right = mid - 1;
      }
    }
    return compressedImageData;
  }
}

const imageCompressionMethod = new ImageCompressionMethod()

export { imageCompressionMethod }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值