在这里插入图片描述

摘要

本文是「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)图像超分 + 媒体库/分享能力,代码示例为调用范式示意,实际接口以华为官方文档为准。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐