图片处理的「最后一公里」——编码,同样关键。编码的核心目标是将处理后的 PixelMap/Picture 对象,压缩成指定格式的文件(如 JPEG、HEIF、GIF),以便保存到本地或网络传输。
华为 Image Kit 提供的 ImagePacker 工具,封装了全套编码能力:支持单图/多图/序列图(GIF)编码,兼容 HDR 格式,还能灵活控制压缩质量。本文将聚焦编码全场景,结合实际开发需求,拆解每个场景的实现步骤、代码示例与避坑要点,让你快速掌握编码技巧。
介绍 HarmonyOS Image Kit 的 ImagePacker 工具在单图、多图(Picture)及 GIF 序列编码中的应用。涵盖核心配置 PackingOption、格式选择、API 版本要求及资源释放等关键点,提供工具类封装与实战代码示例,帮助开发者完成图片从解码到编码保存的全流程闭环。重点讲解了单图编码(PixelMap/ImageSource)、多图编码(Picture)、GIF 序列编码(API18+)的具体实现,以及保存到图库的操作流程和避坑指南,旨在提升图片处理效率与兼容性。
图片处理的「最后一公里」——编码,同样关键。编码的核心目标是将处理后的 PixelMap/Picture 对象,压缩成指定格式的文件(如 JPEG、HEIF、GIF),以便保存到本地或网络传输。
华为 Image Kit 提供的 ImagePacker 工具,封装了全套编码能力:支持单图/多图/序列图(GIF)编码,兼容 HDR 格式,还能灵活控制压缩质量。本文将聚焦编码全场景,结合实际开发需求,拆解每个场景的实现步骤、代码示例与避坑要点,让你快速掌握编码技巧。
在动手编码前,先明确两个核心要素:编码工具 ImagePacker 和配置参数 PackingOption——它们是所有编码操作的基础,选对配置才能避免「编码成功但文件无法打开」的坑。
编码的核心工具是 image.createImagePacker() 创建的实例,它提供了 4 类核心接口,覆盖不同场景:
| 接口名称 | 功能描述 | 支持的输入对象 | 支持的输出格式 | 最低 API 版本 |
|---|---|---|---|---|
| packToData | 编码为 ArrayBuffer(内存流) | PixelMap、ImageSource | JPEG、WebP、PNG、HEIF | - |
| packToFile | 直接编码为文件(写入磁盘) | PixelMap、ImageSource、Picture | JPEG、WebP、PNG、HEIF(Picture 仅支持 JPEG/HEIF) | - |
| packToDataFromPixelmapSequence | 多 PixelMap 序列编码为 GIF 内存流 | PixelMap 数组 | GIF | 18 |
| packToFileFromPixelmapSequence | 多 PixelMap 序列编码为 GIF 文件 | PixelMap 数组 | GIF | 18 |
编码配置 PackingOption 决定了输出文件的格式、质量、动态范围等核心属性,不同场景的配置差异较大,整理成表格方便快速查阅:
| 配置项 | 作用描述 | 可选值/注意事项 | 适用场景 |
|---|---|---|---|
| format | 输出格式(遵循 MIME 标准) | image/jpeg(.jpg/.jpeg)、image/webp(.webp)、image/png(.png)、image/heif(.heif)、image/gif(.gif) | 所有编码场景 |
| quality | 压缩质量(仅有损格式支持) | 0-100,数值越高质量越好、文件越大;PNG 是无损格式,此参数无效 | JPEG、WebP 编码 |
| desiredDynamicRange | 动态范围(HDR 编码控制) | AUTO(自动识别)、SDR、HDR;仅 JPEG 格式支持 HDR 编码 | HDR 图片编码 |
| needsPackProperties | 是否保留多图元数据 | true/false;仅 Picture 多图编码时需要配置 | 多图(Picture)编码 |
核心提醒:格式对应关系必须正确!比如 format: 'image/jpeg' 对应文件扩展名 .jpg 或 .jpeg,如果扩展名写成 .png,会导致文件无法正常打开。
编码流程概览:
下面按「单图→多图→GIF 序列」的顺序,拆解每个场景的实战代码,所有示例均基于官方 API,可直接复用。
单图编码是开发中最频繁的需求,比如将裁剪后的头像保存为 JPEG,将 HDR 图片压缩为 HEIF 格式。支持两种输入对象(PixelMap/ImageSource)和两种输出方式(ArrayBuffer/文件)。
先封装一个编码工具类,避免重复代码,支持不同输入对象和输出方式:
// 图片编码工具类(基于 ImagePacker)
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { Context } from '@kit.AbilityKit';
export class ImageEncoder {
/**
* 通用编码配置生成器
* @param format 目标格式(MIME 标准)
* @param quality 压缩质量(0-100,有损格式有效)
* @param isHdr 是否需要 HDR 编码(仅 JPEG 支持)
* @returns PackingOption
*/
private static getPackingOption(
format: string,
quality: number = 90,
isHdr: boolean = false
): image.PackingOption {
const option: image.PackingOption = {
format,
quality: Math.max(0, Math.min(100, quality)), // 限制质量在 0-100 之间
};
// HDR 编码配置:仅 JPEG 格式支持,需资源本身是 HDR 且设备支持
if (isHdr && format === 'image/jpeg') {
option.desiredDynamicRange = image.PackingDynamicRange.AUTO;
}
return option;
}
/**
* PixelMap 编码为 ArrayBuffer(内存流)
* @param pixelMap 输入位图对象
* @param format 目标格式
* @param quality 压缩质量
* @param isHdr 是否 HDR 编码
* @returns 编码后的 ArrayBuffer
*/
static async encodePixelMapToBuffer(
pixelMap: image.PixelMap,
format: string = 'image/jpeg',
quality: number = 90,
isHdr: boolean = false
): Promise<ArrayBuffer | undefined> {
const imagePacker = image.createImagePacker();
const packOpts = this.getPackingOption(format, quality, isHdr);
try {
const data = await imagePacker.packToData(pixelMap, packOpts);
console.log(`PixelMap 编码为${format}成功,数据长度:${data.byteLength}字节`);
return data;
} catch (error: unknown) {
const err = error as BusinessError;
console.error(`PixelMap 编码失败:code=${err.code}, message=${err.message}`);
return undefined;
}
}
/**
* PixelMap 编码为文件
* @param context 应用上下文
* @param pixelMap 输入位图对象
* @param fileName 输出文件名(含扩展名)
* @param format 目标格式
* @param quality 压缩质量
* @param isHdr 是否 HDR 编码
* @returns 输出文件路径
*/
static async encodePixelMapToFile(
context: Context,
pixelMap: image.PixelMap,
fileName: string,
format: string = 'image/jpeg',
quality: number = 90,
isHdr: boolean = false
): Promise<string | undefined> {
const imagePacker = image.createImagePacker();
const packOpts = this.getPackingOption(format, quality, isHdr);
// 输出路径:应用沙箱缓存目录(无需额外权限)
const outputPath = `${context.cacheDir}/${fileName}`;
try {
// 打开文件:创建 + 读写模式
const file = fs.openSync(outputPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
// 编码并写入文件
await imagePacker.packToFile(pixelMap, file.fd, packOpts);
console.log(`PixelMap 编码为文件成功:${outputPath}`);
// 关闭文件句柄
file.closeSync();
return outputPath;
} catch (error: unknown) {
const err = error as BusinessError;
console.error(`PixelMap 编码为文件失败:code=${err.code}, message=${err.message}`);
return undefined;
}
}
/**
* ImageSource 编码为文件(跳过 PixelMap,直接编码原文件)
* @param context 应用上下文
* @param imageSource 图片源对象
* @param fileName 输出文件名
* @param format 目标格式
* @param quality 压缩质量
* @returns 输出文件路径
*/
static async encodeImageSourceToFile(
context: Context,
imageSource: image.ImageSource,
fileName: string,
format: string = 'image/png',
quality: number = 90
): Promise<string | undefined> {
const imagePacker = image.createImagePacker();
const packOpts = this.getPackingOption(format, quality);
const outputPath = `${context.cacheDir}/${fileName}`;
try {
const file = fs.openSync(outputPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
await imagePacker.packToFile(imageSource, file.fd, packOpts);
file.closeSync();
console.log(`ImageSource 编码为文件成功:${outputPath}`);
return outputPath;
} catch (error: unknown) {
const err = error as BusinessError;
console.error(`ImageSource 编码失败:code=${err.code}, message=${err.message}`);
return undefined;
}
}
}
// 完整流程:解码→编辑→编码保存
async function imageProcessFullFlow(context: Context, inputFileName: string) {
// 1. 解码逻辑:获取 PixelMap
const pixelMap = await decodeToPixelMap(context, inputFileName);
if (!pixelMap) return;
// 2. 编辑逻辑:裁剪 + 缩放
const cropRegion = { x: 0, y: 0, size: { width: 500, height: 500 } };
const croppedMap = await ImageEditor.cropImage(pixelMap, cropRegion);
if (!croppedMap) {
pixelMap.release();
return;
}
// 3. 编码保存为 JPEG 文件(质量 95,非 HDR)
const outputPath = await ImageEncoder.encodePixelMapToFile(
context,
croppedMap,
'edited_avatar.jpg',
'image/jpeg',
95,
false
);
// 4. 编码为 PNG 内存流(无损格式,用于网络上传)
const pngBuffer = await ImageEncoder.encodePixelMapToBuffer(croppedMap, 'image/png');
if (pngBuffer) {
// 此处可调用网络接口上传
console.log(`PNG 内存流准备完成,可用于上传:${pngBuffer.byteLength}字节`);
}
// 5. 释放资源(避免内存泄漏)
pixelMap.release();
croppedMap.release();
}
多图编码针对 Picture 对象(含主图、辅助图、元数据),仅支持编码为 JPEG 或 HEIF 格式,适合 HDR 合成、多图关联数据存储等场景。
// 多图编码工具函数(Picture → 文件/ArrayBuffer)
export class PictureEncoder {
/**
* Picture 编码为文件
* @param context 应用上下文
* @param picture 多图对象
* @param fileName 输出文件名
* @param format 目标格式(仅支持 image/jpeg、image/heif)
* @param quality 压缩质量
* @returns 输出文件路径
*/
static async encodeToFile(
context: Context,
picture: image.Picture,
fileName: string,
format: 'image/jpeg' | 'image/heif' = 'image/jpeg',
quality: number = 90
): Promise<string | undefined> {
// 校验格式(多图仅支持 JPEG/HEIF)
if (!['image/jpeg', 'image/heif'].includes(format)) {
console.error('多图编码仅支持 image/jpeg 和 image/heif 格式');
return undefined;
}
const imagePacker = image.createImagePacker();
const packOpts: image.PackingOption = {
format,
quality,
desiredDynamicRange: image.PackingDynamicRange.AUTO,
needsPackProperties: true, // 保留多图元数据,必须设置为 true
};
const outputPath = `${context.cacheDir}/${fileName}`;
try {
const file = fs.openSync(outputPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
await imagePacker.packToFile(picture, file.fd, packOpts);
file.closeSync();
console.log(`Picture 多图编码成功:${outputPath}`);
return outputPath;
} catch (error: unknown) {
const err = error as BusinessError;
console.error(`Picture 编码失败:code=${err.code}, message=${err.message}`);
return undefined;
}
}
/**
* Picture 编码为 ArrayBuffer
* @param picture 多图对象
* @param format 目标格式
* @param quality 压缩质量
* @returns 编码后的 ArrayBuffer
*/
static async encodeToBuffer(
picture: image.Picture,
format: 'image/jpeg' | 'image/heif' = 'image/heif',
quality: number = 90
): Promise<ArrayBuffer | undefined> {
if (!['image/jpeg', 'image/heif'].includes(format)) {
console.error('多图编码仅支持 image/jpeg 和 image/heif 格式');
return undefined;
}
const imagePacker = image.createImagePacker();
const packOpts: image.PackingOption = {
format,
quality,
needsPackProperties: true,
};
try {
const data = await imagePacker.packing(picture, packOpts); // 多图编码用 packing 接口
console.log(`Picture 编码为内存流成功:${data.byteLength}字节`);
return data;
} catch (error: unknown) {
const err = error as BusinessError;
console.error(`Picture 编码为 Buffer 失败:${err.message}`);
return undefined;
}
}
}
// 调用示例:多图解码后直接编码
async function pictureEncodeDemo(context: Context, inputFileName: string) {
// 1. 多图解码逻辑:获取 Picture 对象
const picture = await decodeToPicture(context, inputFileName);
if (!picture) return;
// 2. 编码为 HEIF 格式文件(保留多图元数据)
const heifPath = await PictureEncoder.encodeToFile(
context,
picture,
'multi_image.heif',
'image/heif',
95
);
// 3. 释放 Picture 资源
picture.release();
}
从 API version 18 开始,Image Kit 支持将多个 PixelMap 序列编码为 GIF 格式(动态图),适合短视频帧、动画序列等场景。
// GIF 序列编码工具(API18+)
export class GifEncoder {
/**
* 多个 PixelMap 编码为 GIF 文件
* @param context 应用上下文
* @param pixelMaps PixelMap 数组(按帧顺序排列)
* @param fileName 输出文件名(扩展名.gif)
* @param quality 压缩质量(0-100)
* @returns 输出文件路径
*/
static async encodeToGifFile(
context: Context,
pixelMaps: image.PixelMap[],
fileName: string,
quality: number = 80
): Promise<string | undefined> {
// 校验 API 版本(需 API18+)
const apiVersion = context.apiVersion;
if (apiVersion < 18) {
console.error('GIF 序列编码需要 API version 18 及以上');
return undefined;
}
// 校验输入:至少需要 1 个 PixelMap
if (pixelMaps.length === 0) {
console.error('GIF 编码需要至少 1 个 PixelMap 帧');
return undefined;
}
const imagePacker = image.createImagePacker();
const packOpts: image.PackingOption = {
format: 'image/gif', // GIF 格式的 MIME 类型
quality,
};
const outputPath = `${context.cacheDir}/${fileName}`;
try {
const file = fs.openSync(outputPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
// 调用序列编码接口:PackToFileFromPixelmapSequence
await imagePacker.packToFileFromPixelmapSequence(pixelMaps, file.fd, packOpts);
file.closeSync();
console.log(`GIF 序列编码成功:${outputPath},共${pixelMaps.length}帧`);
return outputPath;
} catch (error: unknown) {
const err = error as BusinessError;
console.error(`GIF 编码失败:code=${err.code}, message=${err.message}`);
return undefined;
} finally {
// 释放所有帧的 PixelMap 资源
pixelMaps.forEach((pmap) => pmap.release());
}
}
/**
* 多个 PixelMap 编码为 GIF 内存流(用于网络传输)
* @param pixelMaps PixelMap 数组
* @param quality 压缩质量
* @returns GIF 内存流
*/
static async encodeToGifBuffer(
pixelMaps: image.PixelMap[],
quality: number = 80
): Promise<ArrayBuffer | undefined> {
if (pixelMaps.length === 0) {
console.error('GIF 编码需要至少 1 个 PixelMap 帧');
return undefined;
}
const imagePacker = image.createImagePacker();
const packOpts: image.PackingOption = {
format: 'image/gif',
quality,
};
try {
const data = await imagePacker.packToDataFromPixelmapSequence(pixelMaps, packOpts);
console.log(`GIF 内存流编码成功:${data.byteLength}字节`);
return data;
} catch (error: unknown) {
const err = error as BusinessError;
console.error(`GIF 内存流编码失败:${err.message}`);
return undefined;
} finally {
pixelMaps.forEach((pmap) => pmap.release());
}
}
}
// 调用示例:生成 3 帧 GIF 动画
async function generateGifDemo(context: Context) {
// 1. 准备 3 个 PixelMap 帧(模拟动画帧,实际可从视频帧或图片序列获取)
const frame1 = await decodeToPixelMap(context, 'frame1.jpg');
const frame2 = await decodeToPixelMap(context, 'frame2.jpg');
const frame3 = await decodeToPixelMap(context, 'frame3.jpg');
const frames = [frame1, frame2, frame3].filter(Boolean) as image.PixelMap[];
// 2. 编码为 GIF 文件
const gifPath = await GifEncoder.encodeToGifFile(context, frames, 'animation.gif', 85);
}
编码生成文件后,若需让用户在系统图库中查看,需结合 Media Library Kit 的接口。核心流程如下:
createAsset 接口,将沙箱文件导入图库;注意:导入图库需申请 ohos.permission.WRITE_IMAGEVIDEO 权限,且需在 config.json 中声明。示例代码框架如下:
// 保存到图库(基于文档说明的流程框架)
import { mediaLibrary } from '@kit.MediaLibraryKit';
async function saveToGallery(context: Context, filePath: string) {
try {
// 1. 获取 MediaLibrary 实例
const ml = mediaLibrary.getMediaLibrary(context);
// 2. 定义图库资产参数(图片类型)
const assetInfo = {
uri: filePath, // 沙箱中编码后的文件路径
type: mediaLibrary.MediaType.IMAGE,
displayName: 'edited_image.jpg',
};
// 3. 导入到图库(具体 API 以 Media Library Kit 文档为准)
const asset = await ml.createAsset(assetInfo);
console.log(`图片已保存到图库,资产 ID:${asset.assetId}`);
// 4. 可选:删除沙箱临时文件
fs.unlinkSync(filePath);
} catch (error) {
console.error(`保存到图库失败:${error}`);
}
}
| 常见错误 | 后果 | 解决方案 |
|---|---|---|
| MIME 格式与文件扩展名不匹配(如 format: 'image/jpeg',扩展名.png) | 文件无法打开 | 严格对应:image/jpeg→.jpg/.jpeg;image/png→.png;image/gif→.gif |
| 多图编码选择 PNG 格式 | 编码失败(抛出异常) | 多图仅支持 image/jpeg 和 image/heif |
| HDR 编码选择 PNG 格式 | 无效(HDR 仅支持 JPEG) | HDR 编码时 format 必须设为 image/jpeg |
context.apiVersion;PixelMap、Picture、ImageSource 对象,尤其是 GIF 序列的多帧 PixelMap,不释放会导致内存溢出;file.closeSync() 关闭文件句柄,避免文件被占用。packToFile 直接写入文件,避免 packToData 生成大尺寸 ArrayBuffer 占用过多内存;DataAbility 提供访问,避免直接暴露文件路径。到这里,HarmonyOS Image Kit 的核心能力已全部覆盖——从「解码(文件→PixelMap/Picture)」到「编辑(裁剪/缩放/滤镜)」,再到「编码(对象→文件/内存流)」,形成了完整的图片处理闭环。
核心工具与场景对应关系:
ImageSource → 单图用 PixelMap,多图用 Picture;PixelMap 的内置方法 → 基础编辑全覆盖;ImagePacker → 单图/多图/GIF 全场景支持。掌握这些能力后,无论是简单的图片显示、复杂的 HDR 处理,还是动态 GIF 生成,都能高效实现。建议在实际开发中,结合本文的工具类封装,根据业务场景选择合适的编码格式和配置,同时注意权限申请和资源释放,确保应用性能稳定。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online