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