鸿蒙NEXT开发图片相关工具类(ArkTs)

发布于:2025-04-20 ⋅ 阅读:(17) ⋅ 点赞:(0)
import fs from '@ohos.file.fs';
import { image } from '@kit.ImageKit';
import { resourceManager } from '@kit.LocalizationKit';
import { Base64Util } from './Base64Util';
import { FileUtil } from './FileUtil';
import { ResUtil } from './ResUtil';
import { DateUtil } from './DateUtil';
import { PreviewUtil } from './PreviewUtil';

/**
 * 图片工具类
 * 提供图片处理相关功能,如压缩、转换、保存等。
 * author: 鸿蒙布道师
 * since: 2025/04/17
 */
export class ImageUtil {
  /**
   * 将 Base64 字符串转换为 PixelMap
   * @param base64 Base64 格式的图片字符串
   * @returns Promise<image.PixelMap>
   */
  static async base64ToPixelMap(base64: string): Promise<image.PixelMap> {
    if (!base64) throw new Error("Base64 string cannot be empty.");
    const reg = /data:image\/\w+;base64,/;
    const base64Str = base64.replace(reg, '');
    const arrayBuffer = Base64Util.decodeSync(base64Str).buffer as ArrayBuffer;
    const imageSource = image.createImageSource(arrayBuffer);
    return imageSource.createPixelMap({ editable: false });
  }
  /**
   * 将 PixelMap 转换为 Base64 字符串
   * @param pixelMap 图片的 PixelMap 对象
   * @param format 图片格式,默认为 'image/png'
   * @returns Promise<string>
   */
  static async pixelMapToBase64Str(pixelMap: image.PixelMap, format: string = 'image/png'): Promise<string> {
    if (!pixelMap) throw new Error("PixelMap cannot be null.");
    const packOpts: image.PackingOption = { format, quality: 100 };
    const arrayBuffer = await ImageUtil.packingFromPixelMap(pixelMap, packOpts);
    const base64Str = Base64Util.encodeToStrSync(new Uint8Array(arrayBuffer));
    const headStr = `data:${format};base64,`;
    return base64Str.startsWith(headStr) ? base64Str : headStr + base64Str;
  }
  /**
   * 保存 PixelMap 到指定路径
   * @param pixelMap 图片的 PixelMap 对象
   * @param path 保存路径
   * @param name 文件名
   * @param format 图片格式,默认为 'image/png'
   * @returns Promise<string> 返回文件保存路径
   */
  static async savePixelMap(pixelMap: image.PixelMap, path: string, name: string, format: string = 'image/png'): Promise<string> {
    if (!path || !name) throw new Error("Path and name cannot be empty.");
    if (!FileUtil.accessSync(path)) FileUtil.mkdirSync(path);

    const filePath = `${path}${FileUtil.separator}${name}`;
    const file = FileUtil.openSync(filePath);
    const packOpts: image.PackingOption = { format, quality: 100 };
    await ImageUtil.packToFileFromPixelMap(pixelMap, file.fd, packOpts);
    FileUtil.closeSync(file.fd);
    return filePath;
  }
  /**
   * 保存 ImageSource 到指定路径
   * @param source 图片的 ImageSource 对象
   * @param path 保存路径
   * @param name 文件名
   * @param format 图片格式,默认为 'image/png'
   * @returns Promise<string> 返回文件保存路径
   */
  static async saveImageSource(source: image.ImageSource, path: string, name: string, format: string = 'image/png'): Promise<string> {
    if (!path || !name) throw new Error("Path and name cannot be empty.");
    if (!FileUtil.accessSync(path)) FileUtil.mkdirSync(path);

    const filePath = `${path}${FileUtil.separator}${name}`;
    const file = FileUtil.openSync(filePath);
    const packOpts: image.PackingOption = { format, quality: 100 };
    await ImageUtil.packToFileFromImageSource(source, file.fd, packOpts);
    FileUtil.closeSync(file.fd);
    return filePath;
  }
  /**
   * 创建 ImageSource 对象
   * @param src 图片资源(路径、资源 ID、ArrayBuffer 或 RawFileDescriptor)
   * @param options 可选的 SourceOptions
   * @returns image.ImageSource
   */
  static createImageSource(
    src: string | number | ArrayBuffer | resourceManager.RawFileDescriptor,
    options?: image.SourceOptions
  ): image.ImageSource {
    if (!src) throw new Error("Source cannot be empty.");
    if (typeof src === "string") {
      return options ? image.createImageSource(src, options) : image.createImageSource(src);
    } else if (typeof src === "number") {
      return options ? image.createImageSource(src, options) : image.createImageSource(src);
    } else if (src instanceof ArrayBuffer) {
      return options ? image.createImageSource(src, options) : image.createImageSource(src);
    } else {
      // Assume it's a RawFileDescriptor
      return options ? image.createImageSource(src, options) : image.createImageSource(src);
    }
  }
  /**
   * 使用增量方式创建 ImageSource
   * @param buf 图片数据缓冲区
   * @param options 可选的 SourceOptions
   * @returns image.ImageSource
   */
  static createIncrementalSource(buf: ArrayBuffer, options?: image.SourceOptions): image.ImageSource {
    if (!buf) throw new Error("Buffer cannot be empty.");
    return options ? image.CreateIncrementalSource(buf, options) : image.CreateIncrementalSource(buf);
  }
  /**
   * 将 PixelMap 打包为 ArrayBuffer
   * @param source PixelMap 对象
   * @param options 打包选项
   * @returns Promise<ArrayBuffer>
   */
  static packingFromPixelMap(source: image.PixelMap, options: image.PackingOption): Promise<ArrayBuffer> {
    const imagePacker = image.createImagePacker();
    return imagePacker.packing(source, options).finally(() => imagePacker.release());
  }
  /**
   * 将 ImageSource 打包为 ArrayBuffer
   * @param source ImageSource 对象
   * @param options 打包选项
   * @returns Promise<ArrayBuffer>
   */
  static packingFromImageSource(source: image.ImageSource, options: image.PackingOption): Promise<ArrayBuffer> {
    const imagePacker = image.createImagePacker();
    return imagePacker.packing(source, options).finally(() => imagePacker.release());
  }
  /**
   * 将 PixelMap 打包并写入文件
   * @param source PixelMap 对象
   * @param fd 文件描述符
   * @param options 打包选项
   * @returns Promise<void>
   */
  static packToFileFromPixelMap(source: image.PixelMap, fd: number, options: image.PackingOption): Promise<void> {
    const imagePacker = image.createImagePacker();
    return imagePacker.packToFile(source, fd, options).finally(() => imagePacker.release());
  }
  /**
   * 将 ImageSource 打包并写入文件
   * @param source ImageSource 对象
   * @param fd 文件描述符
   * @param options 打包选项
   * @returns Promise<void>
   */
  static packToFileFromImageSource(source: image.ImageSource, fd: number, options: image.PackingOption): Promise<void> {
    const imagePacker = image.createImagePacker();
    return imagePacker.packToFile(source, fd, options).finally(() => imagePacker.release());
  }
  /**
   * 从资源获取 PixelMap
   * @param resource 资源对象
   * @param options 解码选项
   * @returns Promise<image.PixelMap>
   */
  static async getPixelMapFromMedia(resource: Resource, options?: image.DecodingOptions): Promise<image.PixelMap> {
    const uint8Array = await ResUtil.getMediaContent(resource);
    return ImageUtil.createImageSource(uint8Array.buffer).createPixelMap(options);
  }
  /**
   * 压缩图片
   * @param sourcePixelMap 原始 PixelMap
   * @param maxImgSize 最大图片大小(单位:KB)
   * @param imageFormat 图片格式,默认为 'image/jpeg'
   * @returns Promise<ArrayBuffer>
   */
  static async compressedImage(sourcePixelMap: image.PixelMap, maxImgSize: number, imageFormat: string = "image/jpeg"): Promise<ArrayBuffer> {
    if (!sourcePixelMap) throw new Error("PixelMap cannot be null.");
    if (maxImgSize <= 0) throw new Error("Max image size must be greater than 0.");
    const imagePacker = image.createImagePacker();
    const packOpts: image.PackingOption = { format: imageFormat, quality: 0 };
    let compressedImageData = await imagePacker.packing(sourcePixelMap, packOpts);
    const maxCompressedImageByte = maxImgSize * 1024;
    if (compressedImageData.byteLength > maxCompressedImageByte) {
      compressedImageData = await ImageUtil.compressWithScale(sourcePixelMap, maxCompressedImageByte, imageFormat);
    } else {
      compressedImageData = await ImageUtil.compressWithQuality(sourcePixelMap, maxCompressedImageByte, imageFormat);
    }

    imagePacker.release();
    return compressedImageData;
  }
  /**
   * 将 PixelMap 打包为 ArrayBuffer
   * @param sourcePixelMap 图片的 PixelMap 对象
   * @param imageQuality 图片质量
   * @param imageFormat 图片格式,默认为 'image/jpeg'
   * @returns Promise<ArrayBuffer>
   */
  static async packing(
    sourcePixelMap: image.PixelMap,
    imageQuality: number,
    imageFormat: string = "image/jpeg"
  ): Promise<ArrayBuffer> {
    const imagePacker = image.createImagePacker();
    const packOpts: image.PackingOption = { format: imageFormat, quality: imageQuality };
    const data: ArrayBuffer = await imagePacker.packing(sourcePixelMap, packOpts);
    imagePacker.release();
    return data;
  }
  /**
   * 使用质量压缩图片
   * @param sourcePixelMap 原始 PixelMap
   * @param maxCompressedImageByte 最大字节数
   * @param imageFormat 图片格式
   * @returns Promise<ArrayBuffer>
   */
  private static async compressWithQuality(
    sourcePixelMap: image.PixelMap,
    maxCompressedImageByte: number,
    imageFormat: string
  ): Promise<ArrayBuffer> {
    // 生成从 0 到 100 的质量数组,步长为 10
    const qualities: number[] = [];
    for (let i = 0; i <= 10; i++) {
      qualities.push(i * 10);
    }
    let left = 0, right = qualities.length - 1;
    // 使用二分查找法寻找最佳质量
    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const quality = qualities[mid];
      const data: ArrayBuffer = await ImageUtil.packing(sourcePixelMap, quality, imageFormat);

      if (data.byteLength <= maxCompressedImageByte) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }
    // 返回找到的最佳质量对应的压缩图片数据
    return ImageUtil.packing(sourcePixelMap, qualities[right], imageFormat);
  }
  /**
   * 使用缩放压缩图片
   * @param sourcePixelMap 原始 PixelMap
   * @param maxCompressedImageByte 最大字节数
   * @param imageFormat 图片格式
   * @returns Promise<ArrayBuffer>
   */
  private static async compressWithScale(
    sourcePixelMap: image.PixelMap,
    maxCompressedImageByte: number,
    imageFormat: string
  ): Promise<ArrayBuffer> {
    let scale = 1, step = 0.4;
    while (true) {
      scale -= step;
      if (scale <= 0) break;

      await sourcePixelMap.scale(scale, scale);
      const data = await ImageUtil.packing(sourcePixelMap, 0, imageFormat);

      if (data.byteLength <= maxCompressedImageByte) {
        return data;
      }
    }
    throw new Error("Failed to compress image within the specified size limit.");
  }
  /**
   * 压缩图片并保存
   * @param uri 图片 URI
   * @param maxSize 最大图片大小(单位:KB)
   * @param imageFormat 图片格式
   * @returns Promise<string> 返回保存路径
   */
  static async compressPhoto(uri: string, maxSize: number, imageFormat?: string): Promise<string> {
    if (!uri) throw new Error("URI cannot be empty.");
    if (maxSize <= 0) throw new Error("Max size must be greater than 0.");
    imageFormat = imageFormat ?? PreviewUtil.getMimeType(FileUtil.getFileExtention(uri));
    const srcFile = await FileUtil.open(uri, fs.OpenMode.READ_ONLY);
    const pixelMap = await ImageUtil.createImageSource(srcFile.fd).createPixelMap();
    const arrayBuffer = await ImageUtil.compressedImage(pixelMap, maxSize, imageFormat);
    const filePath = FileUtil.getTempDirPath(DateUtil.getTodayStr("yyyy-MM-dd"), srcFile.name);
    await FileUtil.writeEasy(filePath, arrayBuffer, false);
    FileUtil.closeSync(srcFile.fd);
    pixelMap.release();
    return filePath;
  }
}
代码如下:
import fs from '@ohos.file.fs';
import { image } from '@kit.ImageKit';
import { resourceManager } from '@kit.LocalizationKit';
import { Base64Util } from './Base64Util';
import { FileUtil } from './FileUtil';
import { ResUtil } from './ResUtil';
import { DateUtil } from './DateUtil';
import { PreviewUtil } from './PreviewUtil';

/**
 * 图片工具类
 * 提供图片处理相关功能,如压缩、转换、保存等。
 * author: 鸿蒙布道师
 * since: 2025/04/17
 */
export class ImageUtil {
  /**
   * 将 Base64 字符串转换为 PixelMap
   * @param base64 Base64 格式的图片字符串
   * @returns Promise<image.PixelMap>
   */
  static async base64ToPixelMap(base64: string): Promise<image.PixelMap> {
    if (!base64) throw new Error("Base64 string cannot be empty.");
    const reg = /data:image\/\w+;base64,/;
    const base64Str = base64.replace(reg, '');
    const arrayBuffer = Base64Util.decodeSync(base64Str).buffer as ArrayBuffer;
    const imageSource = image.createImageSource(arrayBuffer);
    return imageSource.createPixelMap({ editable: false });
  }
  /**
   * 将 PixelMap 转换为 Base64 字符串
   * @param pixelMap 图片的 PixelMap 对象
   * @param format 图片格式,默认为 'image/png'
   * @returns Promise<string>
   */
  static async pixelMapToBase64Str(pixelMap: image.PixelMap, format: string = 'image/png'): Promise<string> {
    if (!pixelMap) throw new Error("PixelMap cannot be null.");
    const packOpts: image.PackingOption = { format, quality: 100 };
    const arrayBuffer = await ImageUtil.packingFromPixelMap(pixelMap, packOpts);
    const base64Str = Base64Util.encodeToStrSync(new Uint8Array(arrayBuffer));
    const headStr = `data:${format};base64,`;
    return base64Str.startsWith(headStr) ? base64Str : headStr + base64Str;
  }
  /**
   * 保存 PixelMap 到指定路径
   * @param pixelMap 图片的 PixelMap 对象
   * @param path 保存路径
   * @param name 文件名
   * @param format 图片格式,默认为 'image/png'
   * @returns Promise<string> 返回文件保存路径
   */
  static async savePixelMap(pixelMap: image.PixelMap, path: string, name: string, format: string = 'image/png'): Promise<string> {
    if (!path || !name) throw new Error("Path and name cannot be empty.");
    if (!FileUtil.accessSync(path)) FileUtil.mkdirSync(path);

    const filePath = `${path}${FileUtil.separator}${name}`;
    const file = FileUtil.openSync(filePath);
    const packOpts: image.PackingOption = { format, quality: 100 };
    await ImageUtil.packToFileFromPixelMap(pixelMap, file.fd, packOpts);
    FileUtil.closeSync(file.fd);
    return filePath;
  }
  /**
   * 保存 ImageSource 到指定路径
   * @param source 图片的 ImageSource 对象
   * @param path 保存路径
   * @param name 文件名
   * @param format 图片格式,默认为 'image/png'
   * @returns Promise<string> 返回文件保存路径
   */
  static async saveImageSource(source: image.ImageSource, path: string, name: string, format: string = 'image/png'): Promise<string> {
    if (!path || !name) throw new Error("Path and name cannot be empty.");
    if (!FileUtil.accessSync(path)) FileUtil.mkdirSync(path);

    const filePath = `${path}${FileUtil.separator}${name}`;
    const file = FileUtil.openSync(filePath);
    const packOpts: image.PackingOption = { format, quality: 100 };
    await ImageUtil.packToFileFromImageSource(source, file.fd, packOpts);
    FileUtil.closeSync(file.fd);
    return filePath;
  }
  /**
   * 创建 ImageSource 对象
   * @param src 图片资源(路径、资源 ID、ArrayBuffer 或 RawFileDescriptor)
   * @param options 可选的 SourceOptions
   * @returns image.ImageSource
   */
  static createImageSource(
    src: string | number | ArrayBuffer | resourceManager.RawFileDescriptor,
    options?: image.SourceOptions
  ): image.ImageSource {
    if (!src) throw new Error("Source cannot be empty.");
    if (typeof src === "string") {
      return options ? image.createImageSource(src, options) : image.createImageSource(src);
    } else if (typeof src === "number") {
      return options ? image.createImageSource(src, options) : image.createImageSource(src);
    } else if (src instanceof ArrayBuffer) {
      return options ? image.createImageSource(src, options) : image.createImageSource(src);
    } else {
      // Assume it's a RawFileDescriptor
      return options ? image.createImageSource(src, options) : image.createImageSource(src);
    }
  }
  /**
   * 使用增量方式创建 ImageSource
   * @param buf 图片数据缓冲区
   * @param options 可选的 SourceOptions
   * @returns image.ImageSource
   */
  static createIncrementalSource(buf: ArrayBuffer, options?: image.SourceOptions): image.ImageSource {
    if (!buf) throw new Error("Buffer cannot be empty.");
    return options ? image.CreateIncrementalSource(buf, options) : image.CreateIncrementalSource(buf);
  }
  /**
   * 将 PixelMap 打包为 ArrayBuffer
   * @param source PixelMap 对象
   * @param options 打包选项
   * @returns Promise<ArrayBuffer>
   */
  static packingFromPixelMap(source: image.PixelMap, options: image.PackingOption): Promise<ArrayBuffer> {
    const imagePacker = image.createImagePacker();
    return imagePacker.packing(source, options).finally(() => imagePacker.release());
  }
  /**
   * 将 ImageSource 打包为 ArrayBuffer
   * @param source ImageSource 对象
   * @param options 打包选项
   * @returns Promise<ArrayBuffer>
   */
  static packingFromImageSource(source: image.ImageSource, options: image.PackingOption): Promise<ArrayBuffer> {
    const imagePacker = image.createImagePacker();
    return imagePacker.packing(source, options).finally(() => imagePacker.release());
  }
  /**
   * 将 PixelMap 打包并写入文件
   * @param source PixelMap 对象
   * @param fd 文件描述符
   * @param options 打包选项
   * @returns Promise<void>
   */
  static packToFileFromPixelMap(source: image.PixelMap, fd: number, options: image.PackingOption): Promise<void> {
    const imagePacker = image.createImagePacker();
    return imagePacker.packToFile(source, fd, options).finally(() => imagePacker.release());
  }
  /**
   * 将 ImageSource 打包并写入文件
   * @param source ImageSource 对象
   * @param fd 文件描述符
   * @param options 打包选项
   * @returns Promise<void>
   */
  static packToFileFromImageSource(source: image.ImageSource, fd: number, options: image.PackingOption): Promise<void> {
    const imagePacker = image.createImagePacker();
    return imagePacker.packToFile(source, fd, options).finally(() => imagePacker.release());
  }
  /**
   * 从资源获取 PixelMap
   * @param resource 资源对象
   * @param options 解码选项
   * @returns Promise<image.PixelMap>
   */
  static async getPixelMapFromMedia(resource: Resource, options?: image.DecodingOptions): Promise<image.PixelMap> {
    const uint8Array = await ResUtil.getMediaContent(resource);
    return ImageUtil.createImageSource(uint8Array.buffer).createPixelMap(options);
  }
  /**
   * 压缩图片
   * @param sourcePixelMap 原始 PixelMap
   * @param maxImgSize 最大图片大小(单位:KB)
   * @param imageFormat 图片格式,默认为 'image/jpeg'
   * @returns Promise<ArrayBuffer>
   */
  static async compressedImage(sourcePixelMap: image.PixelMap, maxImgSize: number, imageFormat: string = "image/jpeg"): Promise<ArrayBuffer> {
    if (!sourcePixelMap) throw new Error("PixelMap cannot be null.");
    if (maxImgSize <= 0) throw new Error("Max image size must be greater than 0.");
    const imagePacker = image.createImagePacker();
    const packOpts: image.PackingOption = { format: imageFormat, quality: 0 };
    let compressedImageData = await imagePacker.packing(sourcePixelMap, packOpts);
    const maxCompressedImageByte = maxImgSize * 1024;
    if (compressedImageData.byteLength > maxCompressedImageByte) {
      compressedImageData = await ImageUtil.compressWithScale(sourcePixelMap, maxCompressedImageByte, imageFormat);
    } else {
      compressedImageData = await ImageUtil.compressWithQuality(sourcePixelMap, maxCompressedImageByte, imageFormat);
    }

    imagePacker.release();
    return compressedImageData;
  }
  /**
   * 将 PixelMap 打包为 ArrayBuffer
   * @param sourcePixelMap 图片的 PixelMap 对象
   * @param imageQuality 图片质量
   * @param imageFormat 图片格式,默认为 'image/jpeg'
   * @returns Promise<ArrayBuffer>
   */
  static async packing(
    sourcePixelMap: image.PixelMap,
    imageQuality: number,
    imageFormat: string = "image/jpeg"
  ): Promise<ArrayBuffer> {
    const imagePacker = image.createImagePacker();
    const packOpts: image.PackingOption = { format: imageFormat, quality: imageQuality };
    const data: ArrayBuffer = await imagePacker.packing(sourcePixelMap, packOpts);
    imagePacker.release();
    return data;
  }
  /**
   * 使用质量压缩图片
   * @param sourcePixelMap 原始 PixelMap
   * @param maxCompressedImageByte 最大字节数
   * @param imageFormat 图片格式
   * @returns Promise<ArrayBuffer>
   */
  private static async compressWithQuality(
    sourcePixelMap: image.PixelMap,
    maxCompressedImageByte: number,
    imageFormat: string
  ): Promise<ArrayBuffer> {
    // 生成从 0 到 100 的质量数组,步长为 10
    const qualities: number[] = [];
    for (let i = 0; i <= 10; i++) {
      qualities.push(i * 10);
    }
    let left = 0, right = qualities.length - 1;
    // 使用二分查找法寻找最佳质量
    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const quality = qualities[mid];
      const data: ArrayBuffer = await ImageUtil.packing(sourcePixelMap, quality, imageFormat);

      if (data.byteLength <= maxCompressedImageByte) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }
    // 返回找到的最佳质量对应的压缩图片数据
    return ImageUtil.packing(sourcePixelMap, qualities[right], imageFormat);
  }
  /**
   * 使用缩放压缩图片
   * @param sourcePixelMap 原始 PixelMap
   * @param maxCompressedImageByte 最大字节数
   * @param imageFormat 图片格式
   * @returns Promise<ArrayBuffer>
   */
  private static async compressWithScale(
    sourcePixelMap: image.PixelMap,
    maxCompressedImageByte: number,
    imageFormat: string
  ): Promise<ArrayBuffer> {
    let scale = 1, step = 0.4;
    while (true) {
      scale -= step;
      if (scale <= 0) break;

      await sourcePixelMap.scale(scale, scale);
      const data = await ImageUtil.packing(sourcePixelMap, 0, imageFormat);

      if (data.byteLength <= maxCompressedImageByte) {
        return data;
      }
    }
    throw new Error("Failed to compress image within the specified size limit.");
  }
  /**
   * 压缩图片并保存
   * @param uri 图片 URI
   * @param maxSize 最大图片大小(单位:KB)
   * @param imageFormat 图片格式
   * @returns Promise<string> 返回保存路径
   */
  static async compressPhoto(uri: string, maxSize: number, imageFormat?: string): Promise<string> {
    if (!uri) throw new Error("URI cannot be empty.");
    if (maxSize <= 0) throw new Error("Max size must be greater than 0.");
    imageFormat = imageFormat ?? PreviewUtil.getMimeType(FileUtil.getFileExtention(uri));
    const srcFile = await FileUtil.open(uri, fs.OpenMode.READ_ONLY);
    const pixelMap = await ImageUtil.createImageSource(srcFile.fd).createPixelMap();
    const arrayBuffer = await ImageUtil.compressedImage(pixelMap, maxSize, imageFormat);
    const filePath = FileUtil.getTempDirPath(DateUtil.getTodayStr("yyyy-MM-dd"), srcFile.name);
    await FileUtil.writeEasy(filePath, arrayBuffer, false);
    FileUtil.closeSync(srcFile.fd);
    pixelMap.release();
    return filePath;
  }
}


网站公告

今日签到

点亮在社区的每一天
去签到