作者:狼哥
团队:坚果派
团队介绍:坚果派由坚果等人创建,团队拥有12个华为HDE带领热爱HarmonyOS/OpenHarmony的开发者,以及若干其他领域的三十余位万粉博主运营。专注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服务、仓颉。团队成员聚集在北京,上海,南京,深圳,广州,宁夏等地,目前已开发鸿蒙原生应用,三方库60+,欢迎交流。

介绍

​ 图片压缩功能在现代数字生活中扮演着至关重要的角色,其好处多不胜数。在填写报名表时,常遇到对上传图片大小的严格限制,通过图片压缩,可以迅速减小文件体积,确保顺利提交,避免因文件过大而延误申请。同样,在发表文章或分享至社交媒体时,图片压缩能显著提升加载速度,减少用户等待时间,提升阅读体验。此外,压缩后的图片更便于存储与传输,节省宝贵的存储空间和网络带宽。总之,图片压缩功能不仅解决了大小限制的问题,还优化了网络体验,是现代信息交流的得力助手。

效果预览

工程目录

├──entry/src/main/ets                         // 代码区
│  ├──dialog
│  │  └──ImagePicker.ets                      // 图片选择
│  ├──entryability
│  │  └──EntryAbility.ets 
│  ├──model
│  │  ├──ImageModel.ets                       // 图片操作
│  └──pages
│     └──Index.ets                            // 首页
└──entry/src/main/resources                   // 应用资源目录

具体实现

1. 权限添加

配置文件module.json5里添加读取图片及视频权限和修改图片或视频权限。

"requestPermissions": [
      {
        "name": "ohos.permission.WRITE_MEDIA",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        },
        "reason": "$string:WRITE_MEDIA"
      },
      {
        "name": "ohos.permission.MEDIA_LOCATION",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        },
        "reason": "$string:MEDIA_LOCATION"
      },
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        },
        "reason": "$string:READ_IMAGEVIDEO"
      },
      {
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        },
        "reason": "$string:WRITE_IMAGEVIDEO"
      }
    ]
2. 图片选择对话

获取本地图片:首先使用getPhotoAccessHelper获取相册管理模块实例,然后使用getAssets方法获取文件资源,最后使用getAllObjects获取检索结果中的所有文件资产方便展示;

    let photoList: Array<photoAccessHelper.PhotoAsset> = [];
  
    let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
    let fetchOptions: photoAccessHelper.FetchOptions = {
      fetchColumns: [],
      predicates: predicates
    }

    let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> = await this.phAccessHelper.getAssets(fetchOptions);
    if (fetchResult != undefined) {
      let photoAsset: Array<photoAccessHelper.PhotoAsset> = await fetchResult.getAllObjects();
      if (photoAsset != undefined && photoAsset.length > 0) {
        for (let i = 0; i < photoAsset.length; i++) {
          if (photoAsset[i].photoType === 1) {
            photoList.push(photoAsset[i]);
          }
        }
      }
    }

自定义对话框显示获取到的本地图片

import { photoAccessHelper } from '@kit.MediaLibraryKit';

@CustomDialog
export struct ImagePicker {
  @Link index: number;
  private imagesData: Array<photoAccessHelper.PhotoAsset> = [];
  public controller: CustomDialogController;
  @State selected: number = 0;

  build() {
    Column() {
      List({ space: 5 }) {
        ForEach(this.imagesData, (item: photoAccessHelper.PhotoAsset, index) => {
          ListItem() {
            Stack({ alignContent: Alignment.TopEnd }) {...}
        }, (item: photoAccessHelper.PhotoAsset) => JSON.stringify(item))
      }
      .width('95%')
      .height(160)
      .listDirection(Axis.Horizontal)

      Row() {...}
      .margin({ bottom: 10, top: 10 })
    }
    .width('100%')
    .padding({ top: 16, left: 16, right: 16 })
  }
}
3. 图片压缩

​ 3.1 点击“图片压缩”按钮查看压缩后的图片。效果图中是按照固定目标大小为500kb,实际压缩大小小于等于500kb,不一定准确为500kb,此案例是固定了压缩大小为500kb,可以提供一个文本输入框或下拉框提供一些选择要压缩大小。

​ 3.2. 先判断设置图片质量参数quality为0时,packing能压缩到的图片最小字节大小是否满足指定的图片压缩大小。如果满足,则使用packing方式二分查找最接近指定图片压缩目标大小的quality来压缩图片。如果不满足,则使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据。

async compressedImage(sourcePixelMap: image.PixelMap, maxCompressedImageSize: number): Promise<PixelMap> {
    // 创建图像编码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 * 1024;
    // TODO 知识点:图片压缩。先判断设置图片质量参数quality为0时,packing能压缩到的图片最小字节大小是否满足指定的图片压缩大小。如果满足,则使用packing方式二分查找最接近指定图片压缩目标大小的quality来压缩图片。如果不满足,则使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据。
    if (maxCompressedImageByte > compressedImageData.byteLength) {
      // 使用packing二分压缩获取图片文件流
      compressedImageData =
        await this.packingImage(compressedImageData, sourcePixelMap, IMAGE_QUALITY, maxCompressedImageByte);
    } 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);
        } else {
          // imageScale缩放小于等于0时,没有意义,结束压缩。这里不考虑图片缩放倍数小于reduceScale的情况。
          break;
        }
      }
    }

    let pixelMap = await image.createImageSource(compressedImageData).createPixelMap();
    return pixelMap;
  }

​ 3.3 packing压缩

  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;
  }

​ 3.4 packing 二分方式循环压缩

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;
  }
4.保存图片

​ 点击“保存图片”按钮把压缩后的图片保存到图库里。

  async savePixelMap(pm: PixelMap) {
    if (this.phAccessHelper === null) {
      return;
    }
    const imagePackerApi: image.ImagePacker = image.createImagePacker();
    const packOpts: image.PackingOption = { format: 'image/jpeg', quality: 30 };
    try {
      const buffer: ArrayBuffer = await imagePackerApi.packing(pm, packOpts);
      let options: photoAccessHelper.CreateOptions = {
        title: new Date().getTime().toString()
      };

      let photoUri: string = await this.phAccessHelper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg', options);
      let file: fileIo.File = fileIo.openSync(photoUri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
      await fileIo.write(file.fd, buffer);
      fileIo.closeSync(file);
      promptAction.showToast({message: '保存成功!'})
    } catch (err) {
      promptAction.showToast({message: '保存失败!'})
      console.error('xxx', err)
    }
  }
5. 界面布局

​ 使用垂直布局显示,上面图片初始化为图库第一张图片,点击图片可以弹窗显示图库图片提供切换选择要压缩图片,中间是图片压缩和保存图片两个按钮,下面是压缩后图片预览图。

  // 图库上图片
  @State imgData: Array<photoAccessHelper.PhotoAsset> = [];
  // 选择图库图片的下标
  @StorageLink('index') @Watch('onImageChange') index: number = 0;
  private imageModel: ImageModel = new ImageModel();
  @State compressedImg: PixelMap | null = null;
  // 这里500是希望压缩到500K大小
  private targetSize: number = 500;
Column() {
      Image(this.imgData[this.index]?.uri)
        .objectFit(ImageFit.Contain)
        .width('100%')
        .aspectRatio(1)
        .margin(20)
        .onClick(async () => {
          this.imgData = await this.imageModel.getAllImg();
          setTimeout(() => {
            this.dialogController.open();
          }, 200);
        })

      Stack() {
        Divider()
          .width('100%')
          .color(Color.Orange)
        Row({ space: 10 }) {
          Button('图片压缩')
            .onClick(async () => {
              this.compressedImg = null;

              // 以只读方式打开指定下标图片
              await fileIo.open(this.imgData[this.index].uri, fileIo.OpenMode.READ_ONLY).then(async (file: fileIo.File) => {
                let fd: number = file.fd;
                // 获取图片源
                let imageSource: PixelMap = await image.createImageSource(fd).createPixelMap();
                // 这里500是希望压缩到500K大小
                this.compressedImg = await this.imageModel.compressedImage(imageSource, this.targetSize);
              });

            })
          Button('保存图片')
            .onClick(async () => {
              if (this.compressedImg) {
                await this.imageModel.savePixelMap(this.compressedImg)
              }
            })
        }
      }
      .width('100%')
      .height(30)
      Scroll() {
        Column() {
          Image(this.compressedImg)
            .objectFit(ImageFit.Contain)
        }
        .width('90%')
        .padding(10)
      }
      .edgeEffect(EdgeEffect.Spring)
    }
    .height('100%')
    .width('100%')
6. 权限申请

在页面生命周期aboutToAppear函数时,调用权限申请,并获取图库数据。

const PERMISSIONS: Array<Permissions> = [
  'ohos.permission.READ_MEDIA',
  'ohos.permission.WRITE_MEDIA',
  'ohos.permission.MEDIA_LOCATION',
  'ohos.permission.MANAGE_MISSIONS',
  'ohos.permission.WRITE_IMAGEVIDEO'
];
async aboutToAppear() {
    await abilityAccessCtrl.createAtManager().requestPermissionsFromUser(getContext(this), PERMISSIONS);
    this.imgData = await this.imageModel.getAllImg();
  }

总结

​ 本案例参考 图片压缩方案 在packing方式压缩图片时,使用二分查找最接近指定图片压缩目标大小的图片质量quality来压缩图片,提升查找性能。

相关权限

读取图片及视频权限:ohos.permission.READ_IMAGEVIDEO

修改图片或视频权限:ohos.permission.WRITE_IMAGEVIDEO

约束与限制

1.本示例仅支持标准系统上运行,支持设备:华为手机。

2.HarmonyOS系统:HarmonyOS NEXT Developer Beta1及以上。

3.DevEco Studio版本:DevEco Studio NEXT Developer Beta1及以上。

4.HarmonyOS SDK版本:HarmonyOS NEXT Developer Beta1 SDK及以上


狼哥
1 声望0 粉丝