目录

读取和编辑图片XMP元数据

XMP(Extensible Metadata Platform,可扩展元数据平台)是一种用于描述数字资源(如图片、文档、音频等)元数据的标准格式。以XML为基础,允许为资源添加丰富的描述性信息,支持自定义命名空间和扩展。XMP元数据通常内嵌在JPEG、PNG、GIF、DNG、TIFF等格式图片中。

从API版本26.0.0开始,Image Kit支持对JPEG、PNG、GIF、DNG、TIFF格式图片的XMP元数据进行读取,同时也支持对JPEG、PNG、GIF格式图片的XMP元数据进行编辑。

开发步骤

获取图片,创建ImageSource(可选),再获取XMPMetadata对象,使用XMP路径语法读写标签。XMP元数据的操作方法请参考XMPMetadata。

  1. 导入相关模块。

    // 导入相关模块。
    import { image } from '@kit.ImageKit';
    import { BusinessError } from '@kit.BasicServicesKit';
  2. 获取XMPMetadata对象。

    在使用XMP元数据相关接口前,需要先获取一个XMPMetadata实例。当前支持以下方式:

    • 方式一:手动创建新的XMPMetadata对象。适用于需要构造全新XMP元数据的场景。

      async createXMPMetadata(): Promise<image.XMPMetadata> {
        let xmpMetadata = new image.XMPMetadata();
        console.info('Succeeded in creating XMPMetadata.');
        return xmpMetadata;
      }
    • 方式二:通过ImageSource读取图片中的XMP元数据。适用于需要读取或修改已有图片中XMP元数据的场景。

      推荐使用readImageMetadataByType接口获取XMP元数据,可以直接传入MetadataType.XMP_METADATA,精准获取XMP类型的元数据,避免读取其他无关元数据。

      async readXMPMetadataFromImage(imageSource: image.ImageSource): Promise<image.XMPMetadata | undefined> {
        let types: image.MetadataType[] = [image.MetadataType.XMP_METADATA];
        try {
          let metaData: image.ImageMetadata = await imageSource.readImageMetadataByType(types);
          if (metaData != undefined && metaData.xmpMetadata != undefined) {
            // 成功获取XMPMetadata对象。
            let xmpMetadata = metaData.xmpMetadata;
            console.info('Succeeded in reading XMP metadata from image.');
            return xmpMetadata;
          }
          console.info('Failed to read XMP metadata from image.');
          return undefined;
        } catch (error) {
          console.error(`Failed to read XMP metadata. code is ${(error as BusinessError).code}, message is ${(error as BusinessError).message}`);
          return undefined;
        }
      }
  3. 编辑XMP元数据。

    使用XMP路径定位到目标标签后,调用对应的方法进行编辑。例如:通过setValue设置标签值,通过getTag获取标签值,通过removeTag删除标签。

    async operateXMPMetadata(xmpMetadata: image.XMPMetadata): Promise<void> {
      try {
        // 设置xmp:title标签的值。
        await xmpMetadata.setValue(`${image.XMP_BASIC.prefix}:title`, image.XMPTagType.STRING, 'My Title');
    
        // 获取xmp:title路径的标签。
        let tag: image.XMPTag | null = await xmpMetadata.getTag(`${image.XMP_BASIC.prefix}:title`);
        console.info(`${image.XMP_BASIC.prefix}:title: ${tag?.value}`);
    
        // 删除xmp:title标签。
        await xmpMetadata.removeTag(`${image.XMP_BASIC.prefix}:title`);
      } catch (error) {
        console.error(`Failed to operate XMP tags. code is ${(error as BusinessError).code}, message is ${(error as BusinessError).message}`);
      }
    }
  4. 将编辑后的XMP元数据写回图片。

    编辑后的XMPMetadata对象可通过writeImageMetadata写回图片文件。

    DNG和TIFF格式的图片不支持将XMP元数据写回图片文件。

    async writeXMPMetadataToImage(imageSource: image.ImageSource, xmpMetadata: image.XMPMetadata): Promise<void> {
      try {
        // 将修改后的ImageMetadata(包含XMPMetadata)写回图片。
        let metaData: image.ImageMetadata = { xmpMetadata: xmpMetadata } as image.ImageMetadata;
        await imageSource.writeImageMetadata(metaData);
        console.info('Succeeded in writing XMP metadata to image.');
      } catch (error) {
        console.error(`Failed to write XMP metadata. code is ${(error as BusinessError).code}, message is ${(error as BusinessError).message}`);
      }
    }

XMP路径语法详解

XMP路径由以下语法构件组合而成。在使用getTag、setValue、removeTag等接口时,均需通过XMP路径指定目标标签。

[h2]基本属性路径

使用":"将命名空间前缀和标签名分隔开,用于定位指定命名空间下的某个具体标签。

  • 使用标准命名空间(如xmp、dc、exif等)时,无需额外注册。Image Kit支持但不限于常量中定义的标准命名空间。
  • 使用自定义命名空间时,需要先通过registerXMPNamespace完成注册。

示例:

async setAndGetBasicTag(xmpMetadata: image.XMPMetadata): Promise<void> {
  try {
    // 设置xmp:title标签的值,类型为字符串,值为'My Title'。
    await xmpMetadata.setValue(`${image.XMP_BASIC.prefix}:title`, image.XMPTagType.STRING, 'My Title');

    // 获取xmp:title路径的标签。
    let tag: image.XMPTag | null = await xmpMetadata.getTag(`${image.XMP_BASIC.prefix}:title`);
    console.info(`${image.XMP_BASIC.prefix}:title: ${tag?.value}`);
  } catch (error) {
    console.error(`Failed to set or get basic tag. code is ${(error as BusinessError).code}, message is ${(error as BusinessError).message}`);
  }
}

[h2]数组索引

使用"[]"配合索引值定位数组中的元素,索引从1开始计数(1代表第一个元素),或使用"last()"访问最后一个元素。

在使用数组索引添加数组元素前,必须先确保已创建数组容器节点。使用setValue为数组路径设置UNORDERED_ARRAY、ORDERED_ARRAY、ALTERNATE_ARRAY或ALTERNATE_TEXT类型,即可创建数组容器。

示例1:创建数组容器并添加元素

以下示例假设已注册自定义命名空间book。如果尚未注册,请先调用registerXMPNamespace完成注册。

async createArrayAndAddElements(xmpMetadata: image.XMPMetadata): Promise<void> {
  try {
    // 创建无序数组容器。
    await xmpMetadata.setValue('book:fruits', image.XMPTagType.UNORDERED_ARRAY, undefined);

    // 向数组中添加多个元素,数组索引起始值为1。
    let fruits: string[] = ['apple', 'banana'];
    for (let i = 0; i < fruits.length; i++) {
      await xmpMetadata.setValue(`book:fruits[${i + 1}]`, image.XMPTagType.STRING, fruits[i]);
    }
    console.info('Succeeded in creating array and adding elements.');
  } catch (error) {
    console.error(`Failed to create array or add elements. code is ${(error as BusinessError).code}, message is ${(error as BusinessError).message}`);
  }
}

示例2:通过索引获取数组元素

以下示例假设已注册自定义命名空间book。如果尚未注册,请先调用registerXMPNamespace完成注册。

async getArrayElements(xmpMetadata: image.XMPMetadata): Promise<void> {
  try {
    // 获取第二个元素。
    let tag2: image.XMPTag | null = await xmpMetadata.getTag('book:fruits[2]');
    console.info(`book:fruits[2]: ${tag2?.value}`);

    // 获取最后一个元素。
    let lastTag: image.XMPTag | null = await xmpMetadata.getTag('book:fruits[last()]');
    console.info(`book:fruits[last()]: ${lastTag?.value}`);
  } catch (error) {
    console.error(`Failed to get array elements. code is ${(error as BusinessError).code}, message is ${(error as BusinessError).message}`);
  }
}

[h2]结构体成员

使用"/"指定结构体成员。当某个标签的类型是STRUCTURE时,可通过该语法访问其内部的子成员。

在使用结构体成员路径添加子成员前,必须先创建结构体父节点。使用setValue为结构体路径设置STRUCTURE类型,即可创建结构体容器。

示例:创建结构体父节点并操作成员

以下示例假设已注册自定义命名空间book。如果尚未注册,请先调用registerXMPNamespace完成注册。

async createStructAndOperateMembers(xmpMetadata: image.XMPMetadata): Promise<void> {
  try {
    // 创建结构体父节点。
    await xmpMetadata.setValue('book:parentNode', image.XMPTagType.STRUCTURE, undefined);

    // 添加结构体成员。
    let members: Record<string, string> = {
      'child1': 'child1 value',
      'child2': 'child2 value'
    };
    for (const entry of Object.entries(members)) {
      await xmpMetadata.setValue(`book:parentNode/book:${entry[0]}`, image.XMPTagType.STRING, entry[1]);
    }

    // 获取结构体成员的值。
    let childTag: image.XMPTag | null = await xmpMetadata.getTag('book:parentNode/book:child1');
    console.info(`book:parentNode/book:child1: ${childTag?.value}`);
  } catch (error) {
    console.error(`Failed to operate structure members. code is ${(error as BusinessError).code}, message is ${(error as BusinessError).message}`);
  }
}

[h2]限定符访问

使用"/?"指定限定符。限定符用于对主标签提供额外的修饰信息,也常用于访问多语言文本数组中元素的语言属性。

在使用限定符路径添加限定符前,必须先创建主标签。使用setValue为主标签路径设置适当类型,即可创建主标签。

示例1:设置并获取限定符的值

以下示例假设已注册自定义命名空间book。如果尚未注册,请先调用registerXMPNamespace完成注册。

async setAndGetQualifierValue(xmpMetadata: image.XMPMetadata): Promise<void> {
  try {
    // 设置主标签 book:bookTitle
    await xmpMetadata.setValue('book:bookTitle', image.XMPTagType.STRING, 'My Book Title');
    // 为主标签添加限定符 book:lastUpdated
    await xmpMetadata.setValue('book:bookTitle/?book:lastUpdated', image.XMPTagType.STRING, '2024-01-15');
    console.info('Succeeded in setting tag and qualifier.');

    // 获取限定符的值
    let qualifierTag: image.XMPTag | null = await xmpMetadata.getTag('book:bookTitle/?book:lastUpdated');
    console.info(`book:bookTitle/?book:lastUpdated: ${qualifierTag?.value}`);
  } catch (error) {
    console.error(`Failed to operate qualifier. code is ${(error as BusinessError).code}, message is ${(error as BusinessError).message}`);
  }
}

示例2:为多语言文本设置语言限定符

多语言文本(语言替代数组)本质上也是一种数组,类型为ALTERNATE_TEXT。在添加元素前,必须先创建数组容器;且每个元素必须通过"xml:lang"限定符指定语言,否则在序列化时会失败,导致无法写回文件。

async setLanguageQualifier(xmpMetadata: image.XMPMetadata): Promise<void> {
  try {
    // 创建语言替代数组容器。
    await xmpMetadata.setValue(`${image.DUBLIN_CORE.prefix}:title`, image.XMPTagType.ALTERNATE_TEXT, undefined);

    // 添加第一个多语言文本元素,并设置其语言限定符。
    await xmpMetadata.setValue(`${image.DUBLIN_CORE.prefix}:title[1]`, image.XMPTagType.STRING, 'Hello');
    await xmpMetadata.setValue(`${image.DUBLIN_CORE.prefix}:title[1]/?xml:lang`, image.XMPTagType.STRING, 'en-US');

    // 添加第二个多语言文本元素及语言限定符。
    await xmpMetadata.setValue(`${image.DUBLIN_CORE.prefix}:title[2]`, image.XMPTagType.STRING, '你好');
    await xmpMetadata.setValue(`${image.DUBLIN_CORE.prefix}:title[2]/?xml:lang`, image.XMPTagType.STRING, 'zh-CN');

    // 获取第一个元素的语言限定符。
    let langTag: image.XMPTag | null = await xmpMetadata.getTag(`${image.DUBLIN_CORE.prefix}:title[1]/?xml:lang`);
    console.info(`dc:title[1]/?xml:lang: ${langTag?.value}`);
  } catch (error) {
    console.error(`Failed to set language qualifier. code is ${(error as BusinessError).code}, message is ${(error as BusinessError).message}`);
  }
}

[h2]命名空间前缀(仅rootPath支持)

仅指定命名空间前缀,遍历该命名空间下所有标签。

该语法仅适用于enumerateTags和getTags接口的rootPath参数。

示例:

async enumerateNamespaceTags(xmpMetadata: image.XMPMetadata): Promise<void> {
  try {
    // 遍历Dublin Core命名空间下的全部字段。
    xmpMetadata.enumerateTags((path: string, tag: image.XMPTag): boolean => {
      console.info(`path: ${path}, name: ${tag.name}, value: ${tag.value}`);
      return true;  // 返回true继续遍历。
    }, `${image.DUBLIN_CORE.prefix}`, { isRecursive: true, onlyQualifier: false });
    console.info('Succeeded in enumerating namespace tags.');
  } catch (error) {
    console.error(`Failed to enumerate namespace tags. code is ${(error as BusinessError).code}, message is ${(error as BusinessError).message}`);
  }
}