依托HarmonyOS 6.1最新特性实现图片编辑APP(六):图片元数据Exif/XMP处理

前言

在完成图片的基础编辑和滤镜处理后,ImageEditor Pro APP还需要一个重要的功能——图片元数据管理。每张照片都携带着大量的元数据信息,包括拍摄时间、设备型号、GPS位置、光圈快门等Exif信息,以及版权、作者、编辑历史等XMP信息。Image Kit 提供了完整的元数据读取和编辑能力。

图片元数据(Image Metadata)是描述图像属性、拍摄信息、格式信息或处理辅助信息的数据集合,可供读取、修改或参与后续图像处理。Exif(可交换图像文件格式)和XMP(可扩展元数据平台)是两种最重要的元数据标准。
在这里插入图片描述

一、图片元数据概念

1.1 Exif元数据

Exif(Exchangeable image file format)是记录照片属性和拍摄信息的元数据格式:

Exif标签 说明 示例值
Make 相机制造商 “HUAWEI”
Model 相机型号 “P60 Pro”
DateTime 拍摄时间 “2026:06:15 14:30:00”
GPSLatitude GPS纬度 “39.9042 N”
GPSLongitude GPS经度 “116.4074 E”
FNumber 光圈值 “f/1.8”
ExposureTime 曝光时间 “1/1000”
ISOSpeedRatings ISO感光度 “100”
FocalLength 焦距 “5.4mm”
Orientation 图片方向 1(正常)/6(旋转90°)

1.2 XMP元数据

XMP(Extensible Metadata Platform)是一种可扩展的元数据格式:

XMP字段 说明 用途
dc:creator 作者 版权信息
dc:rights 版权声明 法律保护
dc:description 图片描述 内容说明
xmp:Rating 评分 图片评级
xmp:Label 标签 分类标识
photoshop:DateCreated 创建日期 时间信息

二、Exif信息读取

2.1 基础Exif读取

import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';

// Exif信息读取工具
class ExifReader {
  private imageSource: image.ImageSource;
  
  constructor(imageSource: image.ImageSource) {
    this.imageSource = imageSource;
  }
  
  // 获取所有Exif标签
  async getAllExifTags(): Promise<Record<string, string> | undefined> {
    try {
      const tags = await this.imageSource.getImageProperty();
      console.info(`Retrieved ${Object.keys(tags).length} Exif tags.`);
      return tags;
    } catch (error) {
      console.error(`Failed to get Exif tags: ${error}`);
      return undefined;
    }
  }
  
  // 获取指定Exif标签
  async getExifTag(tagName: string): Promise<string | undefined> {
    try {
      const value = await this.imageSource.getImagePropertyString(tagName);
      console.info(`Exif tag '${tagName}': ${value}`);
      return value;
    } catch (error) {
      console.error(`Failed to get Exif tag '${tagName}': ${error}`);
      return undefined;
    }
  }
  
  // 获取拍摄信息摘要
  async getCameraInfo(): Promise<CameraInfo> {
    const info: CameraInfo = {
      make: await this.getExifTag('Make') || 'Unknown',
      model: await this.getExifTag('Model') || 'Unknown',
      dateTime: await this.getExifTag('DateTime') || 'Unknown',
      orientation: await this.getExifTag('Orientation') || '1',
      fNumber: await this.getExifTag('FNumber') || 'Unknown',
      exposureTime: await this.getExifTag('ExposureTime') || 'Unknown',
      iso: await this.getExifTag('ISOSpeedRatings') || 'Unknown',
      focalLength: await this.getExifTag('FocalLength') || 'Unknown'
    };
    return info;
  }
  
  // 获取GPS信息
  async getGPSInfo(): Promise<GPSInfo> {
    const gpsInfo: GPSInfo = {
      latitude: await this.getExifTag('GPSLatitude') || 'Unknown',
      latitudeRef: await this.getExifTag('GPSLatitudeRef') || 'N',
      longitude: await this.getExifTag('GPSLongitude') || 'Unknown',
      longitudeRef: await this.getExifTag('GPSLongitudeRef') || 'E',
      altitude: await this.getExifTag('GPSAltitude') || 'Unknown'
    };
    return gpsInfo;
  }
}

interface CameraInfo {
  make: string;
  model: string;
  dateTime: string;
  orientation: string;
  fNumber: string;
  exposureTime: string;
  iso: string;
  focalLength: string;
}

interface GPSInfo {
  latitude: string;
  latitudeRef: string;
  longitude: string;
  longitudeRef: string;
  altitude: string;
}

2.2 图片旋转角度获取

// 旋转角度获取工具
class ImageRotationUtil {
  // 从Exif方向获取旋转角度
  static async getRotationFromExif(imageSource: image.ImageSource): Promise<number> {
    try {
      const orientation = await imageSource.getImagePropertyString('Orientation');
      console.info(`Exif orientation: ${orientation}`);
      
      switch (orientation) {
        case '1': return 0;    // 正常
        case '3': return 180;  // 旋转180度
        case '6': return 90;   // 顺时针旋转90度
        case '8': return 270;  // 顺时针旋转270度
        default: return 0;
      }
    } catch (error) {
      console.error(`Failed to get rotation from Exif: ${error}`);
      return 0;
    }
  }
  
  // 自动校正图片方向
  static async autoOrient(pixelMap: image.PixelMap, imageSource: image.ImageSource): Promise<void> {
    const rotation = await ImageRotationUtil.getRotationFromExif(imageSource);
    if (rotation > 0) {
      await pixelMap.rotate(rotation);
      console.info(`Auto-oriented by ${rotation} degrees.`);
    }
  }
}

三、Exif信息编辑

3.1 修改Exif标签

// Exif信息编辑工具
class ExifEditor {
  private imageSource: image.ImageSource;
  
  constructor(imageSource: image.ImageSource) {
    this.imageSource = imageSource;
  }
  
  // 修改单个Exif标签
  async modifyExifTag(tagName: string, value: string): Promise<boolean> {
    try {
      await this.imageSource.modifyImageProperty(tagName, value);
      console.info(`Exif tag '${tagName}' modified to: ${value}`);
      return true;
    } catch (error) {
      console.error(`Failed to modify Exif tag '${tagName}': ${error}`);
      return false;
    }
  }
  
  // 批量修改Exif标签
  async modifyExifTags(tags: Record<string, string>): Promise<boolean> {
    try {
      for (const [tagName, value] of Object.entries(tags)) {
        await this.imageSource.modifyImageProperty(tagName, value);
      }
      console.info(`Modified ${Object.keys(tags).length} Exif tags.`);
      return true;
    } catch (error) {
      console.error(`Failed to modify Exif tags: ${error}`);
      return false;
    }
  }
  
  // 设置拍摄时间
  async setDateTime(dateTime: string): Promise<boolean> {
    return this.modifyExifTag('DateTime', dateTime);
  }
  
  // 设置图片方向
  async setOrientation(orientation: string): Promise<boolean> {
    return this.modifyExifTag('Orientation', orientation);
  }
  
  // 清除GPS信息(隐私保护)
  async clearGPSInfo(): Promise<boolean> {
    try {
      const gpsTags = ['GPSLatitude', 'GPSLatitudeRef', 'GPSLongitude',
                       'GPSLongitudeRef', 'GPSAltitude', 'GPSAltitudeRef'];
      for (const tag of gpsTags) {
        await this.modifyExifTag(tag, '');
      }
      console.info('GPS information cleared for privacy.');
      return true;
    } catch (error) {
      console.error(`Failed to clear GPS info: ${error}`);
      return false;
    }
  }
}

3.2 专有元数据获取

// 专有元数据获取工具
class SpecificMetadataReader {
  // 获取HEIF/HEIC专有元数据
  static async getHEIFMetadata(imageSource: image.ImageSource): Promise<Record<string, string>> {
    try {
      const metadata: Record<string, string> = {};
      
      // 获取HEIF特有的属性
      const heifType = await imageSource.getImagePropertyString('HEIFType');
      if (heifType) metadata['HEIFType'] = heifType;
      
      const depthMap = await imageSource.getImagePropertyString('DepthMap');
      if (depthMap) metadata['DepthMap'] = depthMap;
      
      return metadata;
    } catch (error) {
      console.error(`Failed to get HEIF metadata: ${error}`);
      return {};
    }
  }
  
  // 获取DNG专有元数据
  static async getDNGMetadata(imageSource: image.ImageSource): Promise<Record<string, string>> {
    try {
      const metadata: Record<string, string> = {};
      
      const dngVersion = await imageSource.getImagePropertyString('DNGVersion');
      if (dngVersion) metadata['DNGVersion'] = dngVersion;
      
      const rawData = await imageSource.getImagePropertyString('RawData');
      if (rawData) metadata['RawData'] = rawData;
      
      return metadata;
    } catch (error) {
      console.error(`Failed to get DNG metadata: ${error}`);
      return {};
    }
  }
}

四、XMP元数据处理

4.1 XMP数据读取

// XMP元数据读取工具
class XMPReader {
  private imageSource: image.ImageSource;
  
  constructor(imageSource: image.ImageSource) {
    this.imageSource = imageSource;
  }
  
  // 获取完整的XMP数据
  async getXMPData(): Promise<string | undefined> {
    try {
      const xmpData = await this.imageSource.getImagePropertyString('XMP');
      console.info(`XMP data length: ${xmpData?.length || 0}`);
      return xmpData;
    } catch (error) {
      console.error(`Failed to get XMP data: ${error}`);
      return undefined;
    }
  }
  
  // 解析XMP数据中的关键字段
  async parseXMPFields(): Promise<XMPFields> {
    const xmpData = await this.getXMPData();
    const fields: XMPFields = {
      creator: '',
      rights: '',
      description: '',
      rating: '',
      label: ''
    };
    
    if (xmpData) {
      // 从XMP XML中提取字段
      // 实际项目中需要使用XML解析器
      fields.creator = this.extractXMPField(xmpData, 'dc:creator');
      fields.rights = this.extractXMPField(xmpData, 'dc:rights');
      fields.description = this.extractXMPField(xmpData, 'dc:description');
      fields.rating = this.extractXMPField(xmpData, 'xmp:Rating');
      fields.label = this.extractXMPField(xmpData, 'xmp:Label');
    }
    
    return fields;
  }
  
  // 简单的XMP字段提取
  private extractXMPField(xmp: string, fieldName: string): string {
    const regex = new RegExp(`${fieldName}\\s*=\\s*"([^"]*)"`, 'i');
    const match = xmp.match(regex);
    return match ? match[1] : '';
  }
}

interface XMPFields {
  creator: string;
  rights: string;
  description: string;
  rating: string;
  label: string;
}

4.2 XMP数据编辑

// XMP元数据编辑工具
class XMPEditor {
  private imageSource: image.ImageSource;
  
  constructor(imageSource: image.ImageSource) {
    this.imageSource = imageSource;
  }
  
  // 设置XMP元数据
  async setXMPData(xmpData: string): Promise<boolean> {
    try {
      await this.imageSource.modifyImageProperty('XMP', xmpData);
      console.info('XMP data updated successfully.');
      return true;
    } catch (error) {
      console.error(`Failed to set XMP data: ${error}`);
      return false;
    }
  }
  
  // 构建XMP数据XML
  static buildXMPXML(fields: XMPFields): string {
    return `<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description rdf:about=""
      xmlns:dc="http://purl.org/dc/elements/1.1/"
      xmlns:xmp="http://ns.adobe.com/xap/1.0/">
      <dc:creator>${fields.creator}</dc:creator>
      <dc:rights>${fields.rights}</dc:rights>
      <dc:description>${fields.description}</dc:description>
      <xmp:Rating>${fields.rating}</xmp:Rating>
      <xmp:Label>${fields.label}</xmp:Label>
    </rdf:Description>
  </rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>`;
  }
  
  // 设置作者版权信息
  async setCopyright(author: string, rights: string): Promise<boolean> {
    const xmp = XMPEditor.buildXMPXML({
      creator: author,
      rights: rights,
      description: '',
      rating: '',
      label: ''
    });
    return this.setXMPData(xmp);
  }
}

五、ImageEditor Pro元数据管理页面

5.1 元数据查看页面

// 元数据信息类型
interface MetadataInfo {
  camera: CameraInfo;
  gps: GPSInfo;
  xmp: XMPFields;
  imageFormat: string;
  imageSize: string;
  fileSize: string;
}

// 元数据查看页面
@Entry
@Component
struct MetadataPage {
  @State metadata: MetadataInfo | undefined = undefined;
  @State isLoading: boolean = false;
  @State activeTab: string = 'camera';
  private imageSource: image.ImageSource | undefined;
  
  // 加载元数据
  async loadMetadata(filePath: string): Promise<void> {
    this.isLoading = true;
    try {
      this.imageSource = image.createImageSource(filePath);
      const exifReader = new ExifReader(this.imageSource);
      const xmpReader = new XMPReader(this.imageSource);
      
      const imageInfo = await this.imageSource.getImageInfo();
      
      this.metadata = {
        camera: await exifReader.getCameraInfo(),
        gps: await exifReader.getGPSInfo(),
        xmp: await xmpReader.parseXMPFields(),
        imageFormat: imageInfo.encodedFormat || 'Unknown',
        imageSize: `${imageInfo.size.width} x ${imageInfo.size.height}`,
        fileSize: 'Unknown'
      };
    } catch (error) {
      console.error(`Failed to load metadata: ${error}`);
    } finally {
      this.isLoading = false;
    }
  }
  
  build() {
    Column() {
      Text('图片元数据')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .padding(20)
      
      if (this.isLoading) {
        LoadingProgress().width(50).height(50)
      }
      
      if (this.metadata) {
        // 标签切换
        Tabs({ index: 0 }) {
          TabContent() {
            this.buildCameraInfo()
          }.tabBar('拍摄信息')
          
          TabContent() {
            this.buildGPSInfo()
          }.tabBar('GPS信息')
          
          TabContent() {
            this.buildXMPInfo()
          }.tabBar('XMP信息')
          
          TabContent() {
            this.buildImageInfo()
          }.tabBar('图片信息')
        }
      }
    }
    .width('100%')
    .height('100%')
  }
  
  @Builder
  buildCameraInfo() {
    List() {
      ListItem() { this.buildInfoRow('制造商', this.metadata?.camera.make || '') }
      ListItem() { this.buildInfoRow('型号', this.metadata?.camera.model || '') }
      ListItem() { this.buildInfoRow('拍摄时间', this.metadata?.camera.dateTime || '') }
      ListItem() { this.buildInfoRow('光圈', this.metadata?.camera.fNumber || '') }
      ListItem() { this.buildInfoRow('曝光时间', this.metadata?.camera.exposureTime || '') }
      ListItem() { this.buildInfoRow('ISO', this.metadata?.camera.iso || '') }
      ListItem() { this.buildInfoRow('焦距', this.metadata?.camera.focalLength || '') }
    }
  }
  
  @Builder
  buildGPSInfo() {
    List() {
      ListItem() { this.buildInfoRow('纬度', `${this.metadata?.gps.latitude || ''} ${this.metadata?.gps.latitudeRef || ''}`) }
      ListItem() { this.buildInfoRow('经度', `${this.metadata?.gps.longitude || ''} ${this.metadata?.gps.longitudeRef || ''}`) }
      ListItem() { this.buildInfoRow('海拔', this.metadata?.gps.altitude || '') }
    }
  }
  
  @Builder
  buildXMPInfo() {
    List() {
      ListItem() { this.buildInfoRow('作者', this.metadata?.xmp.creator || '') }
      ListItem() { this.buildInfoRow('版权', this.metadata?.xmp.rights || '') }
      ListItem() { this.buildInfoRow('描述', this.metadata?.xmp.description || '') }
      ListItem() { this.buildInfoRow('评分', this.metadata?.xmp.rating || '') }
    }
  }
  
  @Builder
  buildImageInfo() {
    List() {
      ListItem() { this.buildInfoRow('格式', this.metadata?.imageFormat || '') }
      ListItem() { this.buildInfoRow('尺寸', this.metadata?.imageSize || '') }
      ListItem() { this.buildInfoRow('文件大小', this.metadata?.fileSize || '') }
    }
  }
  
  @Builder
  buildInfoRow(label: string, value: string) {
    Row() {
      Text(label)
        .fontSize(14)
        .fontColor('#999999')
        .width('30%')
      Text(value || '未知')
        .fontSize(14)
        .fontColor('#333333')
        .width('70%')
        .textAlign(TextAlign.End)
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 12, bottom: 12 })
  }
}

5.2 元数据编辑功能

// 元数据编辑工具
class MetadataEditor {
  // 隐私保护:清除敏感元数据
  static async sanitizeMetadata(imageSource: image.ImageSource): Promise<boolean> {
    try {
      const editor = new ExifEditor(imageSource);
      
      // 清除GPS位置信息
      await editor.clearGPSInfo();
      
      // 清除相机序列号
      await editor.modifyExifTag('BodySerialNumber', '');
      await editor.modifyExifTag('LensSerialNumber', '');
      
      console.info('Metadata sanitized for privacy.');
      return true;
    } catch (error) {
      console.error(`Metadata sanitization failed: ${error}`);
      return false;
    }
  }
  
  // 添加版权水印信息
  static async addCopyrightWatermark(
    imageSource: image.ImageSource,
    author: string,
    year: string
  ): Promise<boolean> {
    try {
      const xmpEditor = new XMPEditor(imageSource);
      const copyright = `Copyright ${year} ${author}. All rights reserved.`;
      return await xmpEditor.setCopyright(author, copyright);
    } catch (error) {
      console.error(`Copyright watermark failed: ${error}`);
      return false;
    }
  }
}

六、元数据应用场景

6.1 常见应用场景

场景 使用元数据 实现方式
图片自动旋转 Exif Orientation 读取方向标签并自动旋转
照片时间线 Exif DateTime 按拍摄时间排序照片
地图展示 Exif GPS 在MapView上标记拍摄位置
隐私保护 Exif GPS/SN 分享前清除敏感信息
版权保护 XMP dc:rights 嵌入作者和版权声明
图片评级 XMP xmp:Rating 按评分筛选图片

6.2 元数据保留策略

// 元数据保留策略
class MetadataRetentionPolicy {
  // 分享时的元数据保留策略
  static readonly SHARE_POLICY = {
    keep: ['Orientation', 'DateTime', 'Make', 'Model'],
    remove: ['GPSLatitude', 'GPSLongitude', 'GPSAltitude',
             'BodySerialNumber', 'LensSerialNumber',
             'CameraOwnerName']
  };
  
  // 存档时的元数据保留策略
  static readonly ARCHIVE_POLICY = {
    keep: [], // 空数组表示保留所有
    remove: []
  };
  
  // 应用策略
  static async applyPolicy(
    imageSource: image.ImageSource,
    policy: { keep: string[]; remove: string[] }
  ): Promise<void> {
    const editor = new ExifEditor(imageSource);
    
    for (const tag of policy.remove) {
      await editor.modifyExifTag(tag, '');
    }
    
    console.info(`Metadata retention policy applied.`);
  }
}

总结

本文详细介绍了 HarmonyOS 6.1 Image Kit 的图片元数据处理能力,包括Exif信息(拍摄参数、GPS位置、方向信息)的读取和编辑,以及XMP元数据(作者、版权、描述)的处理。通过元数据管理,ImageEditor Pro 可以实现图片自动旋转、隐私保护、版权管理等高级功能。

下一篇文章,我们将探索 Image Kit 的高级特性——图片超分辨率与AI增强,使用VideoProcessingEngine提升图片画质。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

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

更多推荐