在这里插入图片描述

开篇:为什么要在 HarmonyOS 里读 EXIF?

在 HarmonyOS NEXT 中处理图片时,经常需要获取拍摄参数、GPS 坐标等元数据。相册里按地点分类、专业相机显示拍摄参数、图库导出保留时间戳——这些功能都依赖 EXIF(可交换图像文件格式) 信息。官方提供了 Image KitImageMetadata 接口,但实际使用中不少人发现:官方示例能跑,换到自己的图片就读取不到数据,或者返回空字符串。问题不在 API 本身,而在对属性 key 的拼写、图片格式兼容性、以及生命周期的处理上。

本文会从零实现一个读取 EXIF 信息的完整模块,包括提取 GPS 坐标。代码可直接复制到你的 HarmonyOS 项目中运行。


它解决什么问题?适合什么场景?

EXIF 能提供什么?

属性 对应 Key 示例值
光圈 exif:FNumber 2.8
快门速度 exif:ExposureTime 1/1000
ISO exif:ISOSpeedRatings 400
焦距 exif:FocalLength 50mm
GPS 纬度 exif:GPSLatitude 39.9042
GPS 经度 exif:GPSLongitude 116.4074

适用场景:

  • 相册 APP 显示拍摄参数。
  • 地图类应用根据 GPS 坐标标记照片位置。
  • 专业摄影应用读取相机设置。

不适用场景:

  • 截屏图片、网络下载的图片通常没有 EXIF。
  • 非 JPEG 格式(如 PNG、WebP)一般不含 EXIF 数据。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(真机或支持摄像头权限的模拟器)

注意:模拟器默认不含相机生成的真实 EXIF 图片,建议真机测试。可以在真机上用相机拍一张照片,然后通过本程序读取。


核心实现:读取 EXIF 并打印

1. 申请文件读取权限

module.json5 中添加:

"requestPermissions": [
  {
    "name": "ohos.permission.READ_MEDIA"
  }
]

2. 创建 ImageSource,获取 ImageMetadata

ImageSource 负责解码图片,createImageMetadata 方法返回 ImageMetadata 对象。注意:ImageSource 需要指定图片的 URI(文件路径),并且要在使用后关闭释放资源。

完整代码(entry/src/main/ets/utils/ExifReader.ts):

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

/**
 * 读取图片的 EXIF 属性值
 * @param fileUri 图片文件 URI,例如 /storage/media/100/xxx.jpg
 * @returns 包含 EXIF 属性的 Map,key 为属性名,value 为字符串
 */
export async function readExifFromFile(fileUri: string): Promise<Map<string, string>> {
  const resultMap = new Map<string, string>();

  // 打开文件获取文件描述符
  const file = fileIo.openSync(fileUri, fileIo.OpenMode.READ_ONLY);
  const fd = file.fd;

  // 创建 ImageSource
  const imageSource = image.createImageSource(fd);

  try {
    // 获取 ImageMetadata
    const metadata = await imageSource.createImageMetadata();

    // 要读取的 EXIF 属性 key 列表
    const keys = [
      'exif:FNumber',
      'exif:ExposureTime',
      'exif:ISOSpeedRatings',
      'exif:FocalLength',
      'exif:GPSLatitude',
      'exif:GPSLongitude',
      'exif:DateTimeOriginal',
      'exif:Make',
      'exif:Model',
    ];

    for (const key of keys) {
      const value = await metadata.getImageProperty(key);
      if (value !== undefined && value !== '') {
        resultMap.set(key, value);
      }
    }
  } finally {
    // 释放 ImageSource 资源
    imageSource.release();
    fileIo.closeSync(file);
  }

  return resultMap;
}

/**
 * 从 Map 中提取 GPS 坐标(返回 [纬度, 经度])
 * @param exifMap 包含 exif:GPSLatitude 和 exif:GPSLongitude 的 Map
 * @returns [纬度, 经度] 数组,如果缺失则返回 null
 */
export function getGpsCoordinates(exifMap: Map<string, string>): [number, number] | null {
  const latStr = exifMap.get('exif:GPSLatitude');
  const lonStr = exifMap.get('exif:GPSLongitude');
  if (latStr === undefined || lonStr === undefined) {
    return null;
  }
  const lat = parseFloat(latStr);
  const lon = parseFloat(lonStr);
  if (isNaN(lat) || isNaN(lon)) {
    return null;
  }
  return [lat, lon];
}

3. 在 UI 页面调用并显示

创建 entry/src/main/ets/pages/Index.ets

import { readExifFromFile, getGpsCoordinates } from '../utils/ExifReader';

@Entry
@Component
struct Index {
  @State exifText: string = '点击按钮读取 EXIF';

  build() {
    Column() {
      Button('读取 EXIF (选择一个图片)')
        .onClick(async () => {
          // 实际项目中应使用文件选择器,这里直接用固定路径示例
          const testFile = '/storage/media/100/IMG_20250301_123456.jpg';
          try {
            const exifMap = await readExifFromFile(testFile);
            if (exifMap.size === 0) {
              this.exifText = '未找到任何 EXIF 数据';
              return;
            }

            let text = '';
            // 打印至少 5 个属性
            const printKeys = ['exif:FNumber', 'exif:ExposureTime', 'exif:ISOSpeedRatings',
              'exif:FocalLength', 'exif:GPSLatitude', 'exif:GPSLongitude'];
            printKeys.forEach(key => {
              const val = exifMap.get(key);
              text += `${key} = ${val ?? '无'}\n`;
            });

            // 显示 GPS 坐标
            const gps = getGpsCoordinates(exifMap);
            if (gps) {
              text += `GPS 坐标: 纬度 ${gps[0]}, 经度 ${gps[1]}\n`;
            }

            this.exifText = text;
          } catch (error) {
            this.exifText = `读取失败: ${error.message}`;
          }
        })
        .margin(20)

      Text(this.exifText)
        .fontSize(18)
        .padding(16)
    }
    .width('100%')
    .height('100%')
  }
}

注意事项:

  • 实际项目中需要通过 @ohos.file.picker 让用户选择文件,此处为了简化直接写固定路径。
  • ImageSourcerelease() 必须执行,否则 fd 不会被释放导致资源泄漏。
  • getImageProperty 返回 Promise<string | undefined>,空字符串表示该属性不存在。

常见问题(踩坑记录)

坑 1:getImageProperty 返回空字符串

现象:明明图片在电脑上能看到 EXIF,但在 HarmonyOS 里调用读取不到。

原因:属性 key 的拼写必须严格使用官方定义的 exif: 前缀。例如 exif:FNumber 是正确的,Exif:FNumberFNumber 都会返回空。另外部分第三方相机写入的 EXIF 标签可能不标准,例如 exif:ExposureTime 在某些图片中存储的是分数格式字符串(如 1/100),getImageProperty 会原样返回,解析时需注意。

解决方案:使用官方文档列出的 key 列表。对于特殊格式,建议先打印所有支持的属性(可以遍历已知 key 列表),但不要直接假设存在。

坑 2:读取后页面返回时发生崩溃

现象:在页面 A 调用 readExifFromFile 后立即返回页面 B,几秒后 APP 闪退。

原因readExifFromFile 是异步函数,finally 中执行的 imageSource.release() 在页面销毁后才执行,如果 ImageSource 已被释放但回调里仍然使用,会导致空指针。HarmonyOS 的 ImageSource 对象在释放后再次调用其方法会抛出异常。

解决方案:在异步操作前记录当前页面的 isActive 状态,操作完成后判断页面是否还存活。

// 在页面组件中
const pageActive = true; // 通过生命周期获取
try {
  const exifMap = await readExifFromFile(uri);
  if (!pageActive) {
    return; // 页面已销毁,不要更新 UI
  }
  // 更新 UI
} catch (e) {
  // 忽略
}

坑 3:权限已申请但仍提示无权限

原因READ_MEDIA 权限需要用户手动授权。在 HAR 包中,如果调用了 fileIo.openSync 但未获得授权,会抛出 201 错误。

解决方案:在调用文件操作前使用 @ohos.abilityAccessCtrl 检查权限,并弹出授权弹窗。推荐使用 AbilityAccessCtrl.requestPermissions 请求。

import { abilityAccessCtrl, common } from '@kit.AbilityKit';

async function requestPermission(context: common.UIAbilityContext): Promise<boolean> {
  const atManager = abilityAccessCtrl.createAtManager();
  try {
    await atManager.requestPermissionsFromUser(context, ['ohos.permission.READ_MEDIA']);
    return true;
  } catch {
    return false;
  }
}

最佳实践

  1. 统一处理异常,避免闪退
    所有异步读取操作都要用 try-catch 包裹,并在 catch 中给出友好提示。不要直接抛出。

  2. 使用 Promise 而不是回调
    createImageMetadatagetImageProperty 都支持 Promise 语法,比 callback 更清晰,便于错误链式处理。

  3. 善用 Map 存储属性
    返回的 EXIF 属性数量不固定,用 Map 可以灵活访问,避免定义固定结构体。

  4. 不要假设所有图片都有 EXIF
    调用前先检查 exifMap.size,如果为 0 直接显示"无 EXIF 信息"。


FAQ

Q:为什么真机读取的 GPS 坐标是 0?
A:检查手机相机设置中是否开启了"地理位置标签"。默认关闭时不会写入 GPS 坐标。

Q:能否读取 Raw 格式图片的 EXIF?
A:HarmonyOS 当前仅支持 JPEG、HEIF 等格式的 EXIF。Raw 文件(如 DNG)需使用专门的解码库。

Q:模拟器上运行报错"没有文件许可"?
A:模拟器不支持 READ_MEDIA 权限,也无法通过相机拍照获得真实 EXIF。建议使用真机测试。

Q:获取到的 exif:ExposureTime 是 “1/100”,如何转成数字?
A:解析字符串,分割 / 计算浮点数。例如 1/1000.01


Demo 入口

完整项目结构:

entry/src/main/ets/
├── pages/
│   └── Index.ets    (主页面)
├── utils/
│   └── ExifReader.ts (工具函数)

Index.ets 已在上文给出。实际中使用文件管理器选择图片,可结合 photoAccessHelperfilePicker 实现。

示例代码地址:项目地址


总结:读取 EXIF 并不复杂,核心是理解 ImageMetadata.getImageProperty 的 key 命名规则和资源释放时机。本文给出的代码可直接用于 HarmonyOS NEXT 项目,如果需要更完整的文件选择交互,可以在此基础上扩展。

Logo

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

更多推荐