輕松上手-圖片壓縮秘籍

作者:狼哥
團(tuán)隊:堅果派
團(tuán)隊介紹:堅果派由堅果等人創(chuàng)建,團(tuán)隊擁有12個華為HDE帶領(lǐng)熱愛HarmonyOS/OpenHarmony的開發(fā)者,以及若干其他領(lǐng)域的三十余位萬粉博主運(yùn)營。專注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服務(wù)、倉頡。團(tuán)隊成員聚集在北京,上海,南京,深圳,廣州,寧夏等地,目前已開發(fā)鴻蒙原生應(yīng)用,三方庫60+,歡迎交流。

介紹

    圖片壓縮功能在現(xiàn)代數(shù)字生活中扮演著至關(guān)重要的角色,其好處多不勝數(shù)。在填寫報名表時,常遇到對上傳圖片大小的嚴(yán)格限制,通過圖片壓縮,可以迅速減小文件體積,確保順利提交,避免因文件過大而延誤申請。同樣,在發(fā)表文章或分享至社交媒體時,圖片壓縮能顯著提升加載速度,減少用戶等待時間,提升閱讀體驗。此外,壓縮后的圖片更便于存儲與傳輸,節(jié)省寶貴的存儲空間和網(wǎng)絡(luò)帶寬??傊瑘D片壓縮功能不僅解決了大小限制的問題,還優(yōu)化了網(wǎng)絡(luò)體驗,是現(xiàn)代信息交流的得力助手。

效果預(yù)覽

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

工程目錄

├──entry/src/main/ets                         // 代碼區(qū)
│  ├──dialog
│  │  └──ImagePicker.ets                      // 圖片選擇
│  ├──entryability
│  │  └──EntryAbility.ets 
│  ├──model
│  │  ├──ImageModel.ets                       // 圖片操作
│  └──pages
│     └──Index.ets                            // 首頁
└──entry/src/main/resources                   // 應(yīng)用資源目錄

具體實現(xiàn)

1. 權(quán)限添加

配置文件module.json5里添加讀取圖片及視頻權(quán)限和修改圖片或視頻權(quán)限。

"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獲取檢索結(jié)果中的所有文件資產(chǎn)方便展示;

    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 點擊“圖片壓縮”按鈕查看壓縮后的圖片。效果圖中是按照固定目標(biāo)大小為500kb,實際壓縮大小小于等于500kb,不一定準(zhǔn)確為500kb,此案例是固定了壓縮大小為500kb,可以提供一個文本輸入框或下拉框提供一些選擇要壓縮大小。

3.2. 先判斷設(shè)置圖片質(zhì)量參數(shù)quality為0時,packing能壓縮到的圖片最小字節(jié)大小是否滿足指定的圖片壓縮大小。如果滿足,則使用packing方式二分查找最接近指定圖片壓縮目標(biāo)大小的quality來壓縮圖片。如果不滿足,則使用scale對圖片先進(jìn)行縮放,采用while循環(huán)每次遞減0.4倍縮放圖片,再用packing(圖片質(zhì)量參數(shù)quality設(shè)置0)獲取壓縮圖片大小,最終查找到最接近指定圖片壓縮目標(biāo)大小的縮放倍數(shù)的圖片壓縮數(shù)據(jù)。
async compressedImage(sourcePixelMap: image.PixelMap, maxCompressedImageSize: number): Promise<PixelMap> {
    // 創(chuàng)建圖像編碼ImagePacker對象
    const imagePackerApi = image.createImagePacker();
    // 定義圖片質(zhì)量參數(shù)
    const IMAGE_QUALITY = 0;
    // 設(shè)置編碼輸出流和編碼參數(shù)。圖片質(zhì)量參數(shù)quality范圍0-100。
    const packOpts: image.PackingOption = { format: "image/jpeg", quality: IMAGE_QUALITY };
    // 通過PixelMap進(jìn)行編碼。compressedImageData為打包獲取到的圖片文件流。
    let compressedImageData: ArrayBuffer = await imagePackerApi.packing(sourcePixelMap, packOpts);
    // 壓縮目標(biāo)圖像字節(jié)長度
    const maxCompressedImageByte = maxCompressedImageSize * 1024;
    // TODO 知識點:圖片壓縮。先判斷設(shè)置圖片質(zhì)量參數(shù)quality為0時,packing能壓縮到的圖片最小字節(jié)大小是否滿足指定的圖片壓縮大小。如果滿足,則使用packing方式二分查找最接近指定圖片壓縮目標(biāo)大小的quality來壓縮圖片。如果不滿足,則使用scale對圖片先進(jìn)行縮放,采用while循環(huán)每次遞減0.4倍縮放圖片,再用packing(圖片質(zhì)量參數(shù)quality設(shè)置0)獲取壓縮圖片大小,最終查找到最接近指定圖片壓縮目標(biāo)大小的縮放倍數(shù)的圖片壓縮數(shù)據(jù)。
    if (maxCompressedImageByte > compressedImageData.byteLength) {
      // 使用packing二分壓縮獲取圖片文件流
      compressedImageData =
        await this.packingImage(compressedImageData, sourcePixelMap, IMAGE_QUALITY, maxCompressedImageByte);
    } else {
      // 使用scale對圖片先進(jìn)行縮放,采用while循環(huán)每次遞減0.4倍縮放圖片,再用packing(圖片質(zhì)量參數(shù)quality設(shè)置0)獲取壓縮圖片大小,最終查找到最接近指定圖片壓縮目標(biāo)大小的縮放倍數(shù)的圖片壓縮數(shù)據(jù)
      let imageScale = 1; // 定義圖片寬高的縮放倍數(shù),1表示原比例。
      const REDUCE_SCALE = 0.4; // 圖片縮小倍數(shù)
      // 判斷壓縮后的圖片大小是否大于指定圖片的壓縮目標(biāo)大小,如果大于,繼續(xù)降低縮放倍數(shù)壓縮。
      while (compressedImageData.byteLength > maxCompressedImageByte) {
        if (imageScale > 0) {
          // 性能知識點: 由于scale會直接修改圖片PixelMap數(shù)據(jù),所以不適用二分查找scale縮放倍數(shù)。這里采用循環(huán)遞減0.4倍縮放圖片,來查找確定最適
          // 合的縮放倍數(shù)。如果對圖片壓縮質(zhì)量要求不高,建議調(diào)高每次遞減的縮放倍數(shù)reduceScale,減少循環(huán),提升scale壓縮性能。
          imageScale = imageScale - REDUCE_SCALE; // 每次縮放倍數(shù)減0.4
          // 使用scale對圖片進(jìn)行縮放
          await sourcePixelMap.scale(imageScale, imageScale);
          // packing壓縮
          compressedImageData = await this.packing(sourcePixelMap, IMAGE_QUALITY);
        } else {
          // imageScale縮放小于等于0時,沒有意義,結(jié)束壓縮。這里不考慮圖片縮放倍數(shù)小于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 二分方式循環(huán)壓縮
async packingImage(compressedImageData: ArrayBuffer, sourcePixelMap: image.PixelMap, imageQuality: number,
    maxCompressedImageByte: number): Promise<ArrayBuffer> {
    // 圖片質(zhì)量參數(shù)范圍為0-100,這里以10為最小二分單位創(chuàng)建用于packing二分圖片質(zhì)量參數(shù)的數(shù)組。
    const packingArray: number[] = [];
    const DICHOTOMY_ACCURACY = 10;
    // 性能知識點: 如果對圖片壓縮質(zhì)量要求不高,建議調(diào)高最小二分單位dichotomyAccuracy,減少循環(huán),提升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]; // 獲取二分中間位置的圖片質(zhì)量值
      // 根據(jù)傳入的圖片質(zhì)量參數(shù)進(jìn)行packing壓縮,返回壓縮后的圖片文件流數(shù)據(jù)。
      compressedImageData = await this.packing(sourcePixelMap, imageQuality);
      // 判斷查找一個盡可能接近但不超過壓縮目標(biāo)的壓縮大小
      if (compressedImageData.byteLength <= maxCompressedImageByte) {
        // 二分目標(biāo)值在右半邊,繼續(xù)在更高的圖片質(zhì)量參數(shù)(即mid + 1)中搜索
        left = mid + 1;
        // 判斷mid是否已經(jīng)二分到最后,如果二分完了,退出
        if (mid === packingArray.length - 1) {
          break;
        }
        // 獲取下一次二分的圖片質(zhì)量參數(shù)(mid+1)壓縮的圖片文件流數(shù)據(jù)
        compressedImageData = await this.packing(sourcePixelMap, packingArray[mid + 1]);
        // 判斷用下一次圖片質(zhì)量參數(shù)(mid+1)壓縮的圖片大小是否大于指定圖片的壓縮目標(biāo)大小。如果大于,說明當(dāng)前圖片質(zhì)量參數(shù)(mid)壓縮出來的
        // 圖片大小最接近指定圖片的壓縮目標(biāo)大小。傳入當(dāng)前圖片質(zhì)量參數(shù)mid,得到最終目標(biāo)圖片壓縮數(shù)據(jù)。
        if (compressedImageData.byteLength > maxCompressedImageByte) {
          compressedImageData = await this.packing(sourcePixelMap, packingArray[mid]);
          break;
        }
      } else {
        // 目標(biāo)值不在當(dāng)前范圍的右半部分,將搜索范圍的右邊界向左移動,以縮小搜索范圍并繼續(xù)在下一次迭代中查找左半部分。
        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. 界面布局
使用垂直布局顯示,上面圖片初始化為圖庫第一張圖片,點擊圖片可以彈窗顯示圖庫圖片提供切換選擇要壓縮圖片,中間是圖片壓縮和保存圖片兩個按鈕,下面是壓縮后圖片預(yù)覽圖。
  // 圖庫上圖片
  @State imgData: Array<photoAccessHelper.PhotoAsset> = [];
  // 選擇圖庫圖片的下標(biāo)
  @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;

              // 以只讀方式打開指定下標(biāo)圖片
              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. 權(quán)限申請

在頁面生命周期aboutToAppear函數(shù)時,調(diào)用權(quán)限申請,并獲取圖庫數(shù)據(jù)。

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

總結(jié)

本案例參考  [圖片壓縮方案](https://gitee.com/harmonyos-cases/cases/tree/master/CommonAppDevelopment/feature/imagecompression) 在packing方式壓縮圖片時,使用二分查找最接近指定圖片壓縮目標(biāo)大小的圖片質(zhì)量quality來壓縮圖片,提升查找性能。

相關(guān)權(quán)限

讀取圖片及視頻權(quán)限:ohos.permission.READ_IMAGEVIDEO

修改圖片或視頻權(quán)限:ohos.permission.WRITE_IMAGEVIDEO

約束與限制

1.本示例僅支持標(biāo)準(zhǔn)系統(tǒng)上運(yùn)行,支持設(shè)備:華為手機(jī)。

2.HarmonyOS系統(tǒng):HarmonyOS NEXT Developer Beta1及以上。

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容