HarmonyOS技术精讲-Image Kit:元数据读取 - 获取EXIF信息

开篇:为什么要在 HarmonyOS 里读 EXIF?
在 HarmonyOS NEXT 中处理图片时,经常需要获取拍摄参数、GPS 坐标等元数据。相册里按地点分类、专业相机显示拍摄参数、图库导出保留时间戳——这些功能都依赖 EXIF(可交换图像文件格式) 信息。官方提供了 Image Kit 的 ImageMetadata 接口,但实际使用中不少人发现:官方示例能跑,换到自己的图片就读取不到数据,或者返回空字符串。问题不在 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让用户选择文件,此处为了简化直接写固定路径。 ImageSource的release()必须执行,否则fd不会被释放导致资源泄漏。getImageProperty返回Promise<string | undefined>,空字符串表示该属性不存在。
常见问题(踩坑记录)
坑 1:getImageProperty 返回空字符串
现象:明明图片在电脑上能看到 EXIF,但在 HarmonyOS 里调用读取不到。
原因:属性 key 的拼写必须严格使用官方定义的 exif: 前缀。例如 exif:FNumber 是正确的,Exif:FNumber 或 FNumber 都会返回空。另外部分第三方相机写入的 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;
}
}
最佳实践
-
统一处理异常,避免闪退
所有异步读取操作都要用 try-catch 包裹,并在 catch 中给出友好提示。不要直接抛出。 -
使用 Promise 而不是回调
createImageMetadata和getImageProperty都支持 Promise 语法,比 callback 更清晰,便于错误链式处理。 -
善用 Map 存储属性
返回的 EXIF 属性数量不固定,用Map可以灵活访问,避免定义固定结构体。 -
不要假设所有图片都有 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/100 → 0.01。
Demo 入口
完整项目结构:
entry/src/main/ets/
├── pages/
│ └── Index.ets (主页面)
├── utils/
│ └── ExifReader.ts (工具函数)
Index.ets 已在上文给出。实际中使用文件管理器选择图片,可结合 photoAccessHelper 或 filePicker 实现。
示例代码地址:项目地址
总结:读取 EXIF 并不复杂,核心是理解 ImageMetadata.getImageProperty 的 key 命名规则和资源释放时机。本文给出的代码可直接用于 HarmonyOS NEXT 项目,如果需要更完整的文件选择交互,可以在此基础上扩展。
更多推荐


所有评论(0)