图像超分结果保存与分享:把 4 倍高清写回相册的完整链路【鸿蒙心迹】

摘要
本文是「HarmonyOS 端侧图像超分」系列的第三篇,聚焦超分结果的落地环节:如何把内存中的 PixelMap 编码成图片、写入沙箱、存入系统相册,并通过 Want 拉起系统分享面板发给他人。文章先给出「编码 → 落盘 → 进相册 → 分享」的链路与规格对照表,再提供一份完整可运行的 ArkTS 示例,涵盖 import 语句、UIAbilityContext 获取、requestPermissionsFromUser 权限动态申请封装,以及从 PixelMap 到存相册再到分享的 saveAndShare 主流程,关键行均附中文注释。最后总结 quality 取值、权限声明、URI 转换、大图内存峰值等实战要点,并附常见失败排查表。适合已完成超分接入、正准备把结果交付给用户的开发者阅读。
目录
一、背景与痛点
上两篇讲了超分接入和踩坑,但用户最终要的是"能用":把超分后的 4 倍高清存进相册、发给朋友,或放进自己的素材库。可 PixelMap 只是内存里的一张位图,从"算出来"到"落了地",中间还隔着:编码成图片、写入沙箱、存入媒体库、走分享。任何一步姿势不对,结果就是存不进、分享按钮变灰,甚至大图直接 OOM。
本文把超分结果的保存与分享完整链路打通,并附上可直接复用的代码。
二、链路与规格
| 步骤 | 能力 | 要点 |
|---|---|---|
| 编码 | image.ImagePacker | 把 PixelMap 编码为 JPEG/PNG 字节 |
| 落盘 | @kit.CoreFileKit 的 fileIo | 写应用沙箱,先有路径才能分享 |
| 进相册 | photoAccessHelper(媒体库) | 需 READ_IMAGEVIDEO/WRITE_IMAGEVIDEO 权限 |
| 分享 | Want 系统分享面板 | URI 必须是分享可访问的 file:// |
关键认知:沙箱文件对其他 App 不可见,分享或存相册时必须转成媒体库 URI 或分享 URI,不能直接传沙箱绝对路径。
三、核心代码(编码 + 存相册 + 分享)
import { image } from '@kit.ImageKit';
import { fileIo, fileUri } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { Want, common } from '@kit.AbilityKit';
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 需要动态申请的媒体库权限:读 + 写
const PERMISSIONS: Permissions[] = [
'ohos.permission.READ_IMAGEVIDEO',
'ohos.permission.WRITE_IMAGEVIDEO',
];
/**
* 动态申请媒体库权限
* @param ctx UIAbilityContext,用于拉起权限弹窗
* @returns 是否全部授权成功
*/
async function requestGalleryPermission(ctx: common.UIAbilityContext): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
try {
// requestPermissionsFromUser 会弹出系统授权框,返回每条权限的授权结果
const result = await atManager.requestPermissionsFromUser(ctx, PERMISSIONS);
// authResults 中 0 表示已授权,-1 表示拒绝
return result.authResults.every((code: number) => code === 0);
} catch (err) {
const e = err as BusinessError;
console.error(`requestPermissionsFromUser failed, code: ${e.code}, msg: ${e.message}`);
return false;
}
}
/**
* 1. PixelMap → JPEG 字节
* @param pm 超分后的 PixelMap
* @param quality 编码质量,1~100,建议 90
*/
async function pack(pm: image.PixelMap, quality: number = 90): Promise<ArrayBuffer> {
const packer = image.createImagePacker();
try {
// format 指定输出格式,quality 越高体积越大
return await packer.packing(pm, { format: 'image/jpeg', quality });
} finally {
// 编码完成后释放 packer,避免资源泄漏
await packer.release();
}
}
/**
* 2. 字节 → 应用沙箱文件
* @param ctx UIAbilityContext,用于取沙箱目录
* @param buf 图片字节
* @param name 文件名,如 superres_001.jpg
* @returns 沙箱绝对路径
*/
async function saveSandbox(ctx: common.UIAbilityContext, buf: ArrayBuffer, name: string): Promise<string> {
// filesDir 是应用私有目录,其他 App 无法直接访问
const path = `${ctx.filesDir}/${name}`;
const f = await fileIo.open(path, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
try {
await fileIo.write(f.fd, buf);
await fileIo.fsync(f.fd); // 刷盘,确保内容真正写入
} finally {
await fileIo.close(f.fd); // 必须关闭 fd,否则文件可能不完整
}
return path;
}
/**
* 3. 字节 → 媒体库(相册可见)
* @param ctx UIAbilityContext
* @param buf 图片字节
* @returns 媒体库 URI,形如 file://media/Photo/...
*/
async function toGallery(ctx: common.UIAbilityContext, buf: ArrayBuffer): Promise<string> {
const helper = photoAccessHelper.getPhotoAccessHelper(ctx);
// PhotoType.IMAGE 表示图片资产,'jpeg' 为扩展名
const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpeg');
const f = await fileIo.open(uri, fileIo.OpenMode.WRITE_ONLY);
try {
await fileIo.write(f.fd, buf);
await fileIo.fsync(f.fd);
} finally {
await fileIo.close(f.fd);
}
return uri;
}
/**
* 4. 分享(Want 拉起系统分享面板)
* @param ctx UIAbilityContext
* @param sandboxPath 沙箱文件路径
*/
async function share(ctx: common.UIAbilityContext, sandboxPath: string): Promise<void> {
const want: Want = {
action: 'ohos.want.action.sendData', // 系统分享动作
type: 'image/jpeg', // MIME 类型需与文件格式一致
uri: fileUri.getUriFromPath(sandboxPath), // 沙箱路径必须转成可分享 URI
};
try {
await ctx.startAbility(want);
} catch (err) {
const e = err as BusinessError;
console.error(`startAbility failed, code: ${e.code}, msg: ${e.message}`);
}
}
/**
* 主流程:PixelMap → 编码 → 沙箱 → 相册 → 分享
* @param ctx UIAbilityContext
* @param pm 超分后的 PixelMap
*/
export async function saveAndShare(ctx: common.UIAbilityContext, pm: image.PixelMap): Promise<void> {
// 步骤 0:先申请权限,未授权直接返回
const granted = await requestGalleryPermission(ctx);
if (!granted) {
console.warn('媒体库权限未授予,无法存相册');
return;
}
const fileName = `superres_${Date.now()}.jpg`;
try {
// 步骤 1:编码为 JPEG 字节
const buf = await pack(pm, 90);
// 步骤 2:写入沙箱,得到可分享路径
const sandboxPath = await saveSandbox(ctx, buf, fileName);
// 步骤 3:写入媒体库,相册可见
const galleryUri = await toGallery(ctx, buf);
console.info(`已存入相册: ${galleryUri}`);
// 步骤 4:拉起系统分享面板
await share(ctx, sandboxPath);
} finally {
// 步骤 5:释放 PixelMap,降低大图内存峰值
await pm.release();
}
}
调用方式(在页面或 UIAbility 中):
// 在 EntryAbility 或页面中获取 UIAbilityContext
const ctx = getContext(this) as common.UIAbilityContext;
// pm 为超分模块输出的 PixelMap
saveAndShare(ctx, pm);
四、实战要点
要点 1:quality 别拉满,90 足够。
quality: 100 体积翻倍、肉眼难分。4 倍超分图本身已是大图,90 兼顾清晰与体积。
要点 2:存相册先拿媒体库权限。
没声明 WRITE_IMAGEVIDEO 就 createAsset,直接失败。权限在 module.json5 声明并动态申请。
要点 3:分享用 getUriFromPath 转 URI。
直接传沙箱路径,接收方跨应用读不到。必须用 fileUri.getUriFromPath 转成可分享 URI。
要点 4:大图注意内存峰值。
4 倍超分后像素量是原图的 16 倍,编码期内存峰值很高。超分→编码→释放 PixelMap 要一气呵成,不要同时持有多张 PixelMap。
要点 5:常见失败排查。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
createAsset 报权限错误 | 未声明或未动态申请 WRITE_IMAGEVIDEO | 在 module.json5 声明,运行时用 requestPermissionsFromUser 申请 |
| 相册里能看到但打不开 | 写入后未关闭 fd,文件未落盘 | 确保 fileIo.close(f.fd) 执行,必要时 fileIo.fsync |
| 分享面板灰掉或无应用可选 | 传了沙箱绝对路径,或 type 与文件格式不符 | 用 fileUri.getUriFromPath 转 URI,type 与编码格式保持一致 |
| 编码后图片全黑/花屏 | PixelMap 已被释放或跨线程使用 | 编码完成前不要 release(),跨线程传递需确认 PixelMap 生命周期 |
| 大图编码 OOM | 同时持有多张 4 倍超分 PixelMap | 串行处理,编码后立即释放,必要时降低超分倍率 |
五、小结与边界
超分结果的落地三件事:编码选 90、沙箱先落盘、相册/分享走 URI。它把"端侧增强"变成用户真正可用的资产。落地记住:quality 别满、权限先备、URI 必转、大图防 OOM。
总结一下:整条链路可以概括为「一次编码、两处落盘、一次分享」——用 ImagePacker 以 quality 90 编码,先写沙箱拿到可分享路径,再写媒体库让相册可见,最后用 fileUri.getUriFromPath 转成 URI 交给 Want。代码层面把权限申请、编码、落盘、入库、分享拆成独立函数,主流程 saveAndShare 只负责编排与 finally 中释放 PixelMap,既便于复用,也把大图 OOM 的风险控制在最小范围。按本文的姿势落地,超分结果就能从「算得出来」变成「存得进去、发得出去」。
本文为原创首发实战笔记,基于 HarmonyOS 7.0.0(API 26)图像超分 + 媒体库/分享能力,代码示例为调用范式示意,实际接口以华为官方文档为准。
更多推荐


所有评论(0)