Jelajahi Sumber

feat(ImageCropper): 增加图片裁剪和压缩功能- 在 ImageCropperView 中添加宽高比和最大压缩大小参数
- 实现图片压缩功能,支持指定压缩目标大小
- 在 UserDetailView 和 YWQMineView 中集成图片裁剪和压缩- 更新基础库,添加 ImageHelper 工具类

徐勤民 1 bulan lalu
induk
melakukan
13a7ca4e87
4 mengubah file dengan 177 tambahan dan 58 penghapusan
  1. 1 0
      CHANGELOG.md
  2. 1 0
      Index.ets
  3. 70 58
      README.md
  4. 105 0
      src/main/ets/utils/ImageHelper.ets

+ 1 - 0
CHANGELOG.md

@@ -1,6 +1,7 @@
 # [v1.0.10] 2025.xx.xx
 
 > - `ToolsHelper.showConfirmDialog()`&`ToolsHelper.showAlertDialog()`添加自定义`UI`功能
+> - 添加一个`ImageHelper`,处理图片相关
 >
 
 # [v1.0.9] 2025.04.06

+ 1 - 0
Index.ets

@@ -8,6 +8,7 @@ export { Base64Helper } from './src/main/ets/utils/Base64Helper'
 export { CharHelper } from './src/main/ets/utils/CharHelper'
 export { CrashHelper } from './src/main/ets/utils/CrashHelper'
 export { FileHelper } from './src/main/ets/utils/FileHelper'
+export { ImageHelper } from './src/main/ets/utils/ImageHelper'
 export { PickerHelper } from './src/main/ets/utils/PickerHelper'
 export { StrHelper } from './src/main/ets/utils/StrHelper'
 export { LogHelper } from './src/main/ets/utils/LogHelper'

+ 70 - 58
README.md

@@ -12,17 +12,17 @@ ohpm install @szyx/sdk_base
 // 初始化
 import { WindowHelper } from '@szyx/sdk_base';
 export default class AppAbility extends UIAbility {
-    onWindowStageCreate(windowStage: window.WindowStage): void {
-
-        windowStage.loadContent('pages/Index', (err) => {
-            if (err.code) {
-                hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
-                return;
-            }
-        });
-        // 这行代码
-        GlobalContext.setContext(this.context)
-    }
+  onWindowStageCreate(windowStage: window.WindowStage): void {
+
+    windowStage.loadContent('pages/Index', (err) => {
+      if (err.code) {
+        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
+        return;
+      }
+    });
+    // 这行代码
+    GlobalContext.setContext(this.context)
+  }
 }
 ```
 
@@ -58,11 +58,11 @@ PreferencesHelper.put(StorageKeys.CLIENT_ID, value)
 
 // 获取存储的数据
 PreferencesHelper.get(StorageKeys.CLIENT_ID).then(res => {
-    console.log('>>>>>', res)
+  console.log('>>>>>', res)
 })
 // 删除存储的数据
 PreferencesHelper.delete(StorageKeys.CLIENT_ID).then(() => {
-    console.log('>>>>>')
+  console.log('>>>>>')
 })
 ```
 
@@ -157,7 +157,7 @@ ToolsHelper.showAlertDialog({
   msg: '提示信息',
   action: {
     onClick: () => {
-      
+
     }
   }
 })
@@ -174,8 +174,6 @@ ToolsHelper.showAlertDialog({
 >
 > `注意,传入参数(options: AlertBean)是固定的`
 
-
-
 ### 1.4.[ValidatorHelper](./src/main/ets/utils/ValidatorHelper.ets)
 
 > 常用正则
@@ -219,8 +217,8 @@ import { XWebHelper } from '@szyx/sdk_base';
 const XWebview = import('../pages/XWebview');
 
 XWebHelper.openWeb({
-    url: 'https://www.baidu.com',
-    title: '百度一下',
+  url: 'https://www.baidu.com',
+  title: '百度一下',
 })
 ```
 
@@ -328,6 +326,20 @@ import { TimeHelper } from '@szyx/sdk_base'
 TimeHelper.getMonthDays()
 ```
 
+### 1.8.[ImageHelper](./src/main/ets/utils/ImageHelper.ets)
+
+#### 1.8.1 压缩图片到指定大小
+
+```tsx
+import { ImageHelper } from '@szyx/sdk_base'
+
+ImageHelper.compressedImage(pixelMap, this.maxCompressedImageSize).then((data: ArrayBuffer) => {
+  // 压缩后到图片数据
+}).catch((err: BusinessError) => {
+  ToolsHelper.showMessage(err.message)
+})
+```
+
 ## 2.[Dialog](./src/main/ets/dialog)
 
 ### 2.1.弹出list选中弹窗
@@ -338,50 +350,50 @@ import { XDialogList } from '../dialog/XDialogList';
 
 @Component 
 struct MyView{
-    // 控制器,控制开关
-    dialogController: XDialogController = {} as XDialogController
+  // 控制器,控制开关
+  dialogController: XDialogController = {} as XDialogController
 
-    build()
+  build()
+  {
+    Column()
     {
-        Column()
-        {
-            Button
-            ({ buttonStyle: ButtonStyleMode.TEXTUAL })
-            {
-                Image($r('sys.media.ohos_ic_public_more'))
-                    .width(26).height(26)
-            }
-            .
-            width(65)
-                .onClick(() => {
-                if (this.dialogController != null) {
-                    this.dialogController.open()
-                }
-            })
-
-            XDialogList({
-                // 控制器
-                controller: this.dialogController,
-                // 标题(可选)
-                title: '选择您的操作',
-                // 选择内容列表
-                values: ['刷新', '浏览器打开', '分享', '复制地址'],
-                // 用户选择事件
-                onSelected: (index: number, value: string) => {
-                    ToolsHelper.showMessage(`用户选择了第${index}个,内容为:${value}`)
-                },
-                // 用户取消事件
-                onCancel: () => {
-                    ToolsHelper.showMessage('用户取消操作')
-                },
-                // 是否可取消(点击空白处,或者物理返回键)
-                autoCancel: true
-            })
-
+      Button
+      ({ buttonStyle: ButtonStyleMode.TEXTUAL })
+      {
+        Image($r('sys.media.ohos_ic_public_more'))
+          .width(26).height(26)
+      }
+      .
+      width(65)
+        .onClick(() => {
+        if (this.dialogController != null) {
+          this.dialogController.open()
         }
-        .
-        width('100%').height('100%')
+      })
+
+      XDialogList({
+        // 控制器
+        controller: this.dialogController,
+        // 标题(可选)
+        title: '选择您的操作',
+        // 选择内容列表
+        values: ['刷新', '浏览器打开', '分享', '复制地址'],
+        // 用户选择事件
+        onSelected: (index: number, value: string) => {
+          ToolsHelper.showMessage(`用户选择了第${index}个,内容为:${value}`)
+        },
+        // 用户取消事件
+        onCancel: () => {
+          ToolsHelper.showMessage('用户取消操作')
+        },
+        // 是否可取消(点击空白处,或者物理返回键)
+        autoCancel: true
+      })
+
     }
+    .
+    width('100%').height('100%')
+  }
 }
 
 ```

+ 105 - 0
src/main/ets/utils/ImageHelper.ets

@@ -0,0 +1,105 @@
+import { image } from '@kit.ImageKit';
+
+export class ImageHelper {
+  private constructor() {
+  }
+
+  /**
+   * 图片压缩
+   * @param sourcePixelMap:原始待压缩图片的PixelMap对象
+   * @param maxCompressedImageSize:指定图片的压缩目标大小,单位kb
+   * @returns ArrayBuffer:返回最终压缩后的图片信息
+   */
+  static async compressedImage(sourcePixelMap: image.PixelMap, maxCompressedImageSize: number): Promise<ArrayBuffer> {
+    const imagePackerApi = image.createImagePacker();
+    const IMAGE_QUALITY = 0;
+    const packOpts: image.PackingOption = { format: "image/png", quality: IMAGE_QUALITY };
+    // 通过PixelMap进行编码。compressedImageData为打包获取到的图片文件流。
+    let compressedImageData: ArrayBuffer = await imagePackerApi.packing(sourcePixelMap, packOpts);
+    // 压缩目标图像字节长度
+    const maxCompressedImageByte = maxCompressedImageSize * 1024;
+    // 图片压缩。先判断设置图片质量参数quality为0时,packing能压缩到的图片最小字节大小是否满足指定的图片压缩大小。如果满足,则使用packing方式二分查找最接近指定图片压缩目标大小的quality来压缩图片。如果不满足,则使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据。
+    if (maxCompressedImageByte > compressedImageData.byteLength) {
+      // 使用packing二分压缩获取图片文件流
+      compressedImageData =
+        await ImageHelper.packingImage(compressedImageData, sourcePixelMap, IMAGE_QUALITY, maxCompressedImageByte);
+    } else {
+      // 使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据
+      let imageScale = 1;
+      const REDUCE_SCALE = 0.4;
+      // 判断压缩后的图片大小是否大于指定图片的压缩目标大小,如果大于,继续降低缩放倍数压缩。
+      while (compressedImageData.byteLength > maxCompressedImageByte) {
+        if (imageScale > 0) {
+          // 性能知识点: 由于scale会直接修改图片PixelMap数据,所以不适用二分查找scale缩放倍数。这里采用循环递减0.4倍缩放图片,来查找确定最适合的缩放倍数。如果对图片压缩质量要求不高,建议调高每次递减的缩放倍数reduceScale,减少循环,提升scale压缩性能。
+          imageScale = imageScale - REDUCE_SCALE;
+          await sourcePixelMap.scale(imageScale, imageScale);
+          compressedImageData = await ImageHelper.packing(sourcePixelMap, IMAGE_QUALITY);
+        } else {
+          // imageScale缩放小于等于0时,没有意义,结束压缩。这里不考虑图片缩放倍数小于reduceScale的情况。
+          break;
+        }
+      }
+    }
+    return compressedImageData
+  }
+
+  /**
+   * packing压缩
+   * @param sourcePixelMap:原始待压缩图片的PixelMap
+   * @param imageQuality:图片质量参数
+   * @returns data:返回压缩后的图片数据
+   */
+
+  static 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压缩后的图片数据
+   */
+  static 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 ImageHelper.packing(sourcePixelMap, imageQuality);
+      // 判断查找一个尽可能接近但不超过压缩目标的压缩大小
+      if (compressedImageData.byteLength <= maxCompressedImageByte) {
+        left = mid + 1;
+        if (mid === packingArray.length - 1) {
+          break;
+        }
+        // 获取下一次二分的图片质量参数(mid+1)压缩的图片文件流数据
+        compressedImageData = await ImageHelper.packing(sourcePixelMap, packingArray[mid + 1]);
+        // 判断用下一次图片质量参数(mid+1)压缩的图片大小是否大于指定图片的压缩目标大小。如果大于,说明当前图片质量参数(mid)压缩出来的图片大小最接近指定图片的压缩目标大小。传入当前图片质量参数mid,得到最终目标图片压缩数据。
+        if (compressedImageData.byteLength > maxCompressedImageByte) {
+          compressedImageData = await ImageHelper.packing(sourcePixelMap, packingArray[mid]);
+          break;
+        }
+      } else {
+        // 目标值不在当前范围的右半部分,将搜索范围的右边界向左移动,以缩小搜索范围并继续在下一次迭代中查找左半部分。
+        right = mid - 1;
+      }
+    }
+    return compressedImageData;
+  }
+}