一、理解基本概念

1.1 什么是PixelMap?

在HarmonyOS中,PixelMap可以理解为对图片像素数据信息进行描述的逻辑运算类型,它类似于Android中的Bitmap,是对图像像素数据进行管理的核心数据结构。

PixelMap主要用于:

  • 图片的显示与渲染

  • 图像处理与编辑

  • 内存中的图片数据操作

1.2 什么是Uri?

Uri(Uniform Resource Identifier)本质上是带"file://"头的文件存储地址,是用来指向文件存储路径的字符串。

Uri的典型格式:

text

file://com.example.temptest/data/storage/el2/base/haps/entry/files/test.jpg

1.3 为什么不能直接转换?

核心结论:PixelMap和Uri无法直接转化。因为Uri是文件的存储路径,而PixelMap是内存中的像素数据。想要将PixelMap转换为Uri,必须先将PixelMap以文件形式持久化到存储中,然后获取该文件的Uri。


二、核心转换方案:沙箱存储法

2.1 完整代码实现

typescript

import { image } from '@kit.ImageKit';
import { fileIo as fs, fileUri } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

/**
 * PixelMap转为图片Uri
 * @param pixelMap 要转换的PixelMap对象
 * @returns 返回图片的Uri字符串
 */
public async PixelMapToUri(pixelMap: image.PixelMap): Promise<string> {
  // 1. 获取应用沙箱路径
  let fileDir = getContext().getApplicationContext().filesDir;
  
  // 2. 创建图片存储目录(如果不存在)
  let fileSavePath = fileDir + "/image";
  try {
    fs.accessSync(fileSavePath);
  } catch {
    // 目录不存在,创建目录
    fs.mkdirSync(fileSavePath, true);
  }
  
  // 3. 生成唯一文件名(使用时间戳)
  let fileName = new Date().getTime() + ".png";
  let fullPath = fileSavePath + "/" + fileName;
  
  // 4. 创建ImagePacker实例,用于打包PixelMap
  let packer = image.createImagePacker();
  let packOpts: image.PackingOption = {
    format: "image/png",      // 目标格式,可选:image/jpeg、image/webp、image/png、image/heif
    quality: 100              // 图片质量,0-100
  };
  
  // 5. 打开文件(如果不存在则创建)
  let targetFile = fs.openSync(fullPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
  let fd = targetFile.fd;
  
  // 6. 将PixelMap打包到文件
  await packer.packToFile(pixelMap, fd, packOpts);
  
  // 7. 关闭文件
  fs.closeSync(fd);
  
  // 8. 根据文件路径生成Uri
  let targetUri = fileUri.getUriFromPath(targetFile.path);
  
  return targetUri;
}

2.2 代码逐行解析

第1步:获取沙箱路径

typescript

let fileDir = getContext().getApplicationContext().filesDir;

filesDir是应用沙箱中的文件目录,应用拥有该目录的完整读写权限,无需额外申请权限。

第2步:创建目录

typescript

fs.accessSync(fileSavePath);  // 检查目录是否存在
fs.mkdirSync(fileSavePath, true);  // 创建目录,recursive=true表示递归创建

第3步:生成唯一文件名

typescript

let fileName = new Date().getTime() + ".png";

使用时间戳确保每次生成的文件名唯一,避免覆盖已有文件。

第4步:配置打包参数

typescript

let packOpts: image.PackingOption = {
  format: "image/png",
  quality: 100
};
  • format:支持"image/jpeg"、"image/webp"、"image/png"、"image/heif"(12+)

  • 注意:JPEG不支持透明通道,透明色会变为黑色

第5-7步:写入文件

typescript

let targetFile = fs.openSync(fullPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
await packer.packToFile(pixelMap, fd, packOpts);
fs.closeSync(fd);

第8步:生成Uri

typescript

let targetUri = fileUri.getUriFromPath(targetFile.path);

使用fileUri.getUriFromPath()将文件路径转换为标准的Uri格式。


三、进阶转换方式

3.1 通过ArrayBuffer中转(两步法)

如果packToFile不适用,可以采用"先转ArrayBuffer,再写文件"的方式:

typescript

async function pixelMapToUriViaBuffer(pixelMap: image.PixelMap): Promise<string> {
  // 1. 创建ImagePacker实例
  let imagePackerApi = image.createImagePacker();
  
  // 2. 打包为ArrayBuffer
  let packOpts: image.PackingOption = { 
    format: "image/jpeg", 
    quality: 98 
  };
  let arrayBuffer: ArrayBuffer = await imagePackerApi.packing(pixelMap, packOpts);
  
  // 3. 获取沙箱路径
  let fileDir = getContext().getApplicationContext().filesDir;
  let filePath = fileDir + `/${Date.now()}.jpg`;
  
  // 4. 写入文件
  let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  fs.writeSync(file.fd, arrayBuffer);
  fs.closeSync(file);
  
  // 5. 生成Uri
  return fileUri.getUriFromPath(filePath);
}

这种方法适用于需要先对二进制数据进行处理后再保存的场景。

3.2 保存到系统相册并获取Uri

如果需要将图片保存到系统相册而非应用沙箱,可以使用photoAccessHelper

typescript

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';

async function savePixelMapToAlbum(pixelMap: image.PixelMap): Promise<string> {
  const context = getContext() as common.UIAbilityContext;
  let helper = photoAccessHelper.getPhotoAccessHelper(context);
  
  // 1. 在相册中创建图片文件
  let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png');
  
  // 2. 打包PixelMap为ArrayBuffer
  let imagePackerApi = image.createImagePacker();
  let packOpts: image.PackingOption = { 
    format: "image/png", 
    quality: 100 
  };
  let imageBuffer = await imagePackerApi.packing(pixelMap, packOpts);
  
  // 3. 写入相册文件
  let file = fileIo.openSync(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
  fileIo.writeSync(file.fd, imageBuffer);
  fileIo.closeSync(file.fd);
  
  // 4. 返回Uri
  return uri;
}

注意createAsset有5-10秒的时间限制,需要在调用后及时写入数据。


四、双向转换:Uri转PixelMap

为了完整理解,补充Uri转PixelMap的方法:

typescript

import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';

async function uriToPixelMap(uri: string): Promise<image.PixelMap> {
  // 1. 以只读方式打开文件
  const file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
  
  // 2. 通过文件描述符创建图片资源
  const imageSource = image.createImageSource(file.fd);
  
  // 3. 获取图片信息
  const imageInfo: image.ImageInfo = await imageSource.getImageInfo();
  
  // 4. 配置解码参数
  const options: image.DecodingOptions = {
    editable: true,
    desiredSize: { 
      height: imageInfo.size.height, 
      width: imageInfo.size.width 
    }
  };
  
  // 5. 创建PixelMap
  const pixelMap: image.PixelMap = await imageSource.createPixelMap(options);
  
  // 6. 关闭文件
  fileIo.closeSync(file);
  
  return pixelMap;
}

此方法常用于从相册选择图片后展示到界面的场景。


五、常见问题与解决方案

5.1 保存的图片空白但有文件大小

问题:使用readPixelsToBuffer直接保存PixelMap数据导致图片空白。

原因readPixelsToBuffer读取的是原始像素数据(RGB数组),不是经过编码的图片格式。直接写入文件无法被图片查看器识别。

解决方案:必须使用ImagePacker.packing()进行编码后再保存。

typescript

// 错误方式 ❌
const buffer = new ArrayBuffer(pixelMap.getPixelBytesNumber());
await pixelMap.readPixelsToBuffer(buffer);
fs.writeSync(file.fd, buffer);  // 写入原始像素数据,图片无法正常显示

// 正确方式 ✅
const buffer = await imagePackerApi.packing(pixelMap, packOpts);
fs.writeSync(file.fd, buffer);  // 写入编码后的图片数据

5.2 透明通道丢失问题

使用JPEG格式时,透明通道会被黑色填充。如果需要保留透明度,请使用PNG格式:

typescript

let packOpts: image.PackingOption = {
  format: "image/png",  // 使用PNG保留透明通道
  quality: 100
};

5.3 Image组件无法显示沙箱路径

问题:将应用沙箱路径直接传给Image组件不显示。

解决方案:需要将文件路径转换为Uri格式:

typescript

import { fileUri } from '@kit.CoreFileKit';

const path = getContext().filesDir + '/avatar.png';
const imageUri = fileUri.getUriFromPath(path);
// imageUri格式: file:///data/storage/el2/base/.../avatar.png

// 在Image组件中使用
Image(imageUri)

5.4 保存到相册需要权限吗?

使用SaveButton控件可以免申请权限保存图片到相册:

typescript

SaveButton()
  .onClick(async (event, result) => {
    if (result === SaveButtonOnClickResult.SUCCESS) {
      // 这里执行保存操作,无需WRITE_MEDIA权限
      await savePhotoToGallery();
    }
  })

如果不使用SaveButton,则需要申请ohos.permission.WRITE_MEDIA权限。


六、完整工具类封装

typescript

import { image } from '@kit.ImageKit';
import { fileIo, fileUri } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { common } from '@kit.AbilityKit';

export class ImageConverter {
  
  /**
   * PixelMap转沙箱Uri
   */
  static async pixelMapToSandboxUri(
    pixelMap: image.PixelMap, 
    format: 'jpeg' | 'png' = 'png',
    quality: number = 100
  ): Promise<string> {
    const fileDir = getContext().getApplicationContext().filesDir;
    const dirPath = fileDir + '/images';
    
    // 创建目录
    try {
      fileIo.accessSync(dirPath);
    } catch {
      fileIo.mkdirSync(dirPath, true);
    }
    
    const ext = format === 'jpeg' ? 'jpg' : 'png';
    const mimeType = format === 'jpeg' ? 'image/jpeg' : 'image/png';
    const filePath = dirPath + `/${Date.now()}.${ext}`;
    
    const packer = image.createImagePacker();
    const packOpts: image.PackingOption = { 
      format: mimeType, 
      quality: quality 
    };
    
    const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
    await packer.packToFile(pixelMap, file.fd, packOpts);
    fileIo.closeSync(file.fd);
    
    return fileUri.getUriFromPath(filePath);
  }
  
  /**
   * PixelMap转相册Uri
   */
  static async pixelMapToAlbumUri(
    pixelMap: image.PixelMap,
    context: common.UIAbilityContext
  ): Promise<string> {
    const helper = photoAccessHelper.getPhotoAccessHelper(context);
    const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png');
    
    const packer = image.createImagePacker();
    const packOpts: image.PackingOption = { 
      format: 'image/png', 
      quality: 100 
    };
    const imageBuffer = await packer.packing(pixelMap, packOpts);
    
    const file = fileIo.openSync(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
    fileIo.writeSync(file.fd, imageBuffer);
    fileIo.closeSync(file.fd);
    
    return uri;
  }
  
  /**
   * Uri转PixelMap
   */
  static async uriToPixelMap(uri: string): Promise<image.PixelMap> {
    const file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
    const imageSource = image.createImageSource(file.fd);
    const imageInfo = await imageSource.getImageInfo();
    const options: image.DecodingOptions = {
      editable: true,
      desiredSize: { 
        height: imageInfo.size.height, 
        width: imageInfo.size.width 
      }
    };
    const pixelMap = await imageSource.createPixelMap(options);
    fileIo.closeSync(file);
    return pixelMap;
  }
}

七、总结

核心要点

要点 说明
转换本质 PixelMap → 编码 → 文件写入 → Uri
核心API ImagePacker.packToFile() / fileUri.getUriFromPath()
存储位置 应用沙箱(filesDir)或系统相册(photoAccessHelper)
格式选择 PNG保留透明通道,JPEG文件更小
常见错误 直接用readPixelsToBuffer保存原始像素数据

最佳实践

  1. 优先使用packToFile:一步到位,代码简洁

  2. PNG格式保留透明度:需要透明背景时必选

  3. 使用SaveButton保存相册:免权限申请,用户体验更好

  4. 及时清理临时文件:避免沙箱空间被占满

  5. 路径转Uri后再显示:Image组件需要Uri格式

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐