在元服务开发中,开发者经常需要将业务中动态生成的图片(即内存中的PixelMap对象)保存到用户的系统相册中。但在实际操作中会面临以下阻碍:
ImagePacker:图像编码压缩类,用于将内存中的PixelMap或ImageSource对象打包并压缩为标准的图片二进制文件流(如image/jpeg)。
SaveButton安全控件:系统提供的安全UI组件。在免权限申请的场景下,必须由用户主动点击该按钮,系统安全校验通过后,会在后台静默授予本次操作“临时写入相册”的权限。
photoAccessHelper:相册访问助手,用于在系统媒体库中创建图片或视频资产,并获取对应的文件访问URI。
在元服务中将PixelMap保存到系统相册中,实现思路如下:“图像编码压缩->安全控件换取临时授权->写入媒体库”。
具体实现步骤如下:
使用image.createImagePacker创建图像编码器,配置所需的图片格式和质量参数(如JPEG格式、100质量)。调用packToData方法,将PixelMap对象转换为标准的图片二进制流(ArrayBuffer)。
// 第一步:使用 ImagePacker 将 PixelMap 编码为 JPG 格式的二进制流
const imagePacker = image.createImagePacker();
const packOptions: image.PackingOption = {
format: 'image/jpeg',
quality: 100 // 图片质量 0-100
};
// packing 返回的是 ArrayBuffer 格式的图片文件流
const imageBuffer = await imagePacker.packToData(this.pixelMap, packOptions); 在UI界面的搭建中,使用SaveButton安全控件。在用户的真实点击事件中,校验回调的result是否为SaveButtonOnClickResult.SUCCESS,以确保系统已成功下发临时写入权限。
// 按钮 2:安全控件 SaveButton 用于免权限保存图片
SaveButton({ icon: SaveIconStyle.FULL_FILLED, text: SaveDescription.SAVE_IMAGE })
.width('80%')
.onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => {
// 必须严格校验 result 是否为 SUCCESS,这代表系统完成了安全验证并授予了临时权限
if (result === SaveButtonOnClickResult.SUCCESS) {
await this.savePixelMapToAlbum();
} else {
this.promptAction.showToast({ message: '授权失败,无法保存' });
console.error('SaveButton authorization failed.');
}
}) 在确认获得安全控件授权后,使用photoAccessHelper.getPhotoAccessHelper获取相册实例,调用createAsset创建一条图片资产并得到目标文件的URI。接着使用fileIo以读写模式打开上述URI对应的文件,将第1步生成的二进制流写入该文件描述符(fd)。注意:写入完成后务必调用fileIo.close(file.fd)关闭文件流,否则可能导致系统相册刷新延迟,图库中无法立刻显示刚保存的图片。
// 第二步:通过 photoAccessHelper 创建相册资产并获取 URI
const context = this.uiContext?.getHostContext();
const helper = photoAccessHelper.getPhotoAccessHelper(context);
// 创建一条图像类型的资产,指定后缀名为 jpg,返回对应文件的 URI
const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
// 第三步:使用 fileIo 将编码后的二进制流写入到该 URI 中
// 以读写和创建模式打开文件
const file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
// 写入数据
await fileIo.write(file.fd, imageBuffer);
// 必须关闭文件流,否则相册可能无法及时刷新显示
await fileIo.close(file.fd);
// 提示用户保存成功
this.promptAction.showToast({ message: '已成功保存到系统相册!' });
console.info('Image successfully saved to gallery. URI:', uri); 完整的Index.ets代码如下:
import { image } from '@kit.ImageKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { util } from '@kit.ArkTS';
import { PromptAction } from '@kit.ArkUI';
@Entry
@Component
struct Index {
// 用于界面展示和后续保存的 PixelMap 对象
@State pixelMap: image.PixelMap | undefined = undefined;
promptAction: PromptAction = this.getUIContext().getPromptAction();
uiContext = this.getUIContext();
/**
* 1. 动态生成测试用 PixelMap (为了 Demo 能直接跑起来,无需本地图片)
*/
async generatePixelMap() {
const width = 300;
const height = 300;
const bufferSize = width * height * 4;
const buffer = new ArrayBuffer(bufferSize);
const dataView = new DataView(buffer);
// 填充渐变色数据
for (let i = 0; i < bufferSize; i += 4) {
const idx = i / 4;
const x = idx % width;
const y = Math.floor(idx / width);
dataView.setUint8(i, 255);
dataView.setUint8(i + 1, x % 255);
dataView.setUint8(i + 2, y % 255);
dataView.setUint8(i + 3, 255);
}
try {
// 使用元服务兼容的 1x1 占位图方式创建 ImageSource
const dummyBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAANSURBVBhXY/j/fw8DAwAA/wD/b6y2ZgAAAABJRU5ErkJggg==';
const base64Str = dummyBase64.split(',')[1];
const base64Helper = new util.Base64Helper();
const dummyBuffer = base64Helper.decodeSync(base64Str).buffer as ArrayBuffer;
const imageSource = image.createImageSource(dummyBuffer);
// 解码并分配真实内存
const decodingOpts: image.DecodingOptions = {
editable: true,
desiredSize: { width: width, height: height },
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
};
const pm = await imageSource.createPixelMap(decodingOpts);
// 写入真实数据
await pm.writeBufferToPixels(buffer);
this.pixelMap = pm;
this.promptAction.showToast({ message: '图片生成成功,可以保存了' });
} catch (error) {
console.error(`生成图片失败: ${(error as BusinessError).message}`);
}
}
/**
* 2. 核心保存逻辑:将 PixelMap 编码并存入系统相册
*/
async savePixelMapToAlbum() {
if (!this.pixelMap) {
this.promptAction.showToast({ message: '请先生成图片' });
return;
}
try {
// 第一步:使用 ImagePacker 将 PixelMap 编码为 JPG 格式的二进制流
const imagePacker = image.createImagePacker();
const packOptions: image.PackingOption = {
format: 'image/jpeg',
quality: 100 // 图片质量 0-100
};
// packing 返回的是 ArrayBuffer 格式的图片文件流
const imageBuffer = await imagePacker.packToData(this.pixelMap, packOptions);
// 第二步:通过 photoAccessHelper 创建相册资产并获取 URI
const context = this.uiContext?.getHostContext();
const helper = photoAccessHelper.getPhotoAccessHelper(context);
// 创建一条图像类型的资产,指定后缀名为 jpg,返回对应文件的 URI
const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
// 第三步:使用 fileIo 将编码后的二进制流写入到该 URI 中
// 以读写和创建模式打开文件
const file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
// 写入数据
await fileIo.write(file.fd, imageBuffer);
// 必须关闭文件流,否则相册可能无法及时刷新显示
await fileIo.close(file.fd);
// 提示用户保存成功
this.promptAction.showToast({ message: '已成功保存到系统相册!' });
console.info('Image successfully saved to gallery. URI:', uri);
} catch (error) {
const err = error as BusinessError;
console.error(`保存相册失败: Code ${err.code}, Message: ${err.message}`);
this.promptAction.showToast({ message: `保存失败: ${err.message}` });
}
}
build() {
Column({ space: 20 }) {
Text('保存 PixelMap 图片')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ top: 30, bottom: 20 })
// 图片展示区
if (this.pixelMap) {
Image(this.pixelMap)
.width(250)
.height(250)
.objectFit(ImageFit.Contain)
.border({ width: 1, color: Color.Gray, radius: 8 })
} else {
Column() {
Text('点击下方按钮生成图片')
.fontColor(Color.Gray)
}
.width(250)
.height(250)
.backgroundColor('#F5F5F5')
.justifyContent(FlexAlign.Center)
.border({ width: 1, color: Color.Gray, style: BorderStyle.Dashed, radius: 8 })
}
// 按钮 1:普通按钮用于生成图片
Button('1. 动态生成 PixelMap')
.width('80%')
.onClick(() => {
this.generatePixelMap();
})
// 按钮 2:安全控件 SaveButton 用于免权限保存图片
SaveButton({ icon: SaveIconStyle.FULL_FILLED, text: SaveDescription.SAVE_IMAGE })
.width('80%')
.onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => {
// 必须严格校验 result 是否为 SUCCESS,这代表系统完成了安全验证并授予了临时权限
if (result === SaveButtonOnClickResult.SUCCESS) {
await this.savePixelMapToAlbum();
} else {
this.promptAction.showToast({ message: '授权失败,无法保存' });
console.error('SaveButton authorization failed.');
}
})
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
}
}