HarmonyOS 7 图片 XMP 写回成功却读不到?xml:lang、格式边界和回读校验一次讲清

适用范围:HarmonyOS 7、API 26、Image Kit ArkTS 接口。XMPMetadata 从 API 26.0.0 开始提供。本文只讨论官方文档已说明的格式和路径语法,接口能力以当前 API 26 SDK 为准。

给图片写标题、版权、关键词,看起来只是给几个字段赋值。真正上线后,最容易出现的却是三类问题:

  • 内存里的 XMPMetadata 能读到值,写回文件时却失败;
  • JPEG 测试正常,换成 DNG 或 TIFF 就报错;
  • writeImageMetadata 没有抛出预期外的错误,但重新打开文件后读不到目标标签。

这三类问题分别对应:XMP 结构是否完整、图片格式是否支持写入,以及有没有做文件级回读验证。

HarmonyOS 7 Image Kit XMP 写回失败与回读验证流程

先把官方能力边界列清楚

华为 Image Kit 文档说明,从 API 26.0.0 开始:

图片格式读取 XMP编辑后写回
JPEG / JPG支持支持
PNG支持支持
GIF支持支持
DNG支持不支持
TIFF支持不支持

因此,“能读取”不能推导出“能写回”。DNG 和 TIFF 应在进入编辑链路前就被格式门禁拦住,而不是等到最后调用 writeImageMetadata 才处理失败。

案例一:多语言标题已经 setValue,为什么序列化仍失败

XMP 的多语言文本不是普通字符串数组。官方文档给出的类型是 ALTERNATE_TEXT,数组中每个元素都需要通过 xml:lang 限定符声明语言。

错误思路通常是:

  1. 创建 dc:title 数组;
  2. 写入 dc:title[1] 和 dc:title[2];
  3. 忘记给数组元素设置 /?xml:lang;
  4. 内存里似乎有两个值,序列化时才失败。

正确创建多语言标题

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

interface LocalizedValue {
  lang: string
  value: string
}

async function setLocalizedTitle(
  metadata: image.XMPMetadata,
  values: LocalizedValue[]
): Promise<void> {
  const root = `${image.DUBLIN_CORE.prefix}:title`

  await metadata.setValue(
    root,
    image.XMPTagType.ALTERNATE_TEXT,
    undefined
  )

  for (let index = 0; index < values.length; index++) {
    const item = values[index]
    const itemPath = `${root}[${index + 1}]`

    await metadata.setValue(
      itemPath,
      image.XMPTagType.STRING,
      item.value
    )
    await metadata.setValue(
      `${itemPath}/?xml:lang`,
      image.XMPTagType.STRING,
      item.lang
    )
  }
}

这里有三个细节:

  • XMP 数组下标从 1 开始,不是从 0 开始;
  • 要先创建 ALTERNATE_TEXT 容器,再创建元素;
  • 每个元素都要有 xml:lang,不能只给整个数组写一次语言。

写入前先做应用侧校验

function validateLocalizedValues(values: LocalizedValue[]): string[] {
  const errors: string[] = []
  const languages = new Set<string>()

  values.forEach((item: LocalizedValue, index: number) => {
    const lang = item.lang.trim()
    const value = item.value.trim()

    if (!lang) {
      errors.push(`第 ${index + 1} 项缺少 xml:lang`)
    }
    if (!value) {
      errors.push(`第 ${index + 1} 项内容为空`)
    }
    if (lang && languages.has(lang)) {
      errors.push(`语言重复:${lang}`)
    }
    if (lang) {
      languages.add(lang)
    }
  })

  return errors
}

应用侧提前拒绝空语言、空内容和重复语言,错误信息会比底层序列化失败更容易定位。

案例二:DNG 能读出 XMP,为什么不能保存修改

DNG 和 TIFF 属于“可读取、不可写回”的范围。常见错误是把读取成功当作写入能力探测:

const metadata = await imageSource.readImageMetadataByType([
  image.MetadataType.XMP_METADATA
])

// 错误推断:既然 metadata.xmpMetadata 存在,就一定能写回。

读取成功只能证明文件内存在可解析 XMP,不代表编码器允许把修改后的元数据重新写入该格式。

在业务入口做格式门禁

interface XmpCapability {
  canRead: boolean
  canWrite: boolean
}

function normalizeFormat(format: string): string {
  const value = format.trim().toLowerCase()
  return value === 'jpg' ? 'jpeg' : value
}

function getXmpCapability(format: string): XmpCapability {
  const normalized = normalizeFormat(format)
  return {
    canRead: ['jpeg', 'png', 'gif', 'dng', 'tiff'].includes(normalized),
    canWrite: ['jpeg', 'png', 'gif'].includes(normalized)
  }
}

处理流程应当是:

function assertWritableFormat(format: string): void {
  const capability = getXmpCapability(format)
  if (!capability.canRead) {
    throw new Error(`不支持读取 XMP:${format}`)
  }
  if (!capability.canWrite) {
    throw new Error(`该格式只支持读取 XMP,不能写回:${format}`)
  }
}

如果业务必须修改 DNG/TIFF 的描述信息,需要在产品层明确替代方案,例如另存为支持写回的格式或把业务元数据放进独立数据库;不能悄悄改扩展名,也不能把“内存修改成功”展示成“文件保存成功”。

一条完整的写入链路

1. 精准读取 XMP

官方推荐通过 readImageMetadataByType 指定 XMP_METADATA,避免读取无关元数据。

async function readXmp(
  source: image.ImageSource
): Promise<image.XMPMetadata | undefined> {
  const metadata = await source.readImageMetadataByType([
    image.MetadataType.XMP_METADATA
  ])
  return metadata.xmpMetadata
}

如果图片原本没有 XMP,可以创建新的实例:

const xmp = new image.XMPMetadata()

2. 修改标签

async function updateMetadata(
  xmp: image.XMPMetadata
): Promise<void> {
  await setLocalizedTitle(xmp, [
    { lang: 'en-US', value: 'Cloud over the lake' },
    { lang: 'zh-CN', value: '湖面上的云' }
  ])

  await xmp.setValue(
    `${image.XMP_BASIC.prefix}:CreatorTool`,
    image.XMPTagType.STRING,
    'HarmonyOS Image Kit'
  )
}

3. 写回文件

async function writeXmp(
  source: image.ImageSource,
  xmp: image.XMPMetadata
): Promise<void> {
  const metadata = {
    xmpMetadata: xmp
  } as image.ImageMetadata

  await source.writeImageMetadata(metadata)
}

这里的成功只代表写入调用完成。要证明文件真的包含目标数据,还需要关闭旧对象、重新创建 ImageSource 并回读。

不要省略“重新打开文件再读”的验收

如果直接对内存中的同一个 XMPMetadata 调用 getTag,读到的只是刚才设置的对象,不是磁盘证据。

正确的验证步骤:

  1. 完成 writeImageMetadata;
  2. 释放或停止使用旧 ImageSource;
  3. 从目标文件重新创建 ImageSource;
  4. 再次调用 readImageMetadataByType;
  5. 逐项读取标签与 xml:lang;
  6. 将实际值与预期值比较。
async function verifyLocalizedTitle(
  xmp: image.XMPMetadata,
  expected: LocalizedValue[]
): Promise<boolean> {
  const root = `${image.DUBLIN_CORE.prefix}:title`

  for (let index = 0; index < expected.length; index++) {
    const itemPath = `${root}[${index + 1}]`
    const valueTag = await xmp.getTag(itemPath)
    const langTag = await xmp.getTag(
      `${itemPath}/?xml:lang`
    )

    if (valueTag?.value !== expected[index].value) {
      return false
    }
    if (langTag?.value !== expected[index].lang) {
      return false
    }
  }
  return true
}

自定义命名空间为什么会“路径找不到”

基本路径使用 前缀:标签名。如果使用自定义前缀,必须先注册命名空间,再通过注册后的前缀访问标签。不要把完整 URI 直接拼在每一个路径里,也不要假定任意前缀都已存在。

结构体成员使用 /,限定符使用 /?。例如:

  • book:parent/book:child:访问结构体成员;
  • book:title/?book:lastUpdated:访问限定符;
  • dc:title[2]/?xml:lang:访问数组第二项的语言限定符。

如果父结构体或数组容器还没创建,直接写子路径也会失败。路径错误时应打印“操作路径 + 预期类型 + 错误码”,不要只记录一条“写入失败”。

我实际验证了什么

在不依赖设备文件系统的策略层,我运行了以下断言:

  • JPG 会规范化为 jpeg,可读也可写;
  • PNG 可写;
  • DNG 与 TIFF 可读但不可写;
  • en-US、zh-CN 两个完整本地化值通过校验;
  • 缺少 xml:lang 会被拦截;
  • 同一种语言重复出现会被拦截。

上述策略测试全部通过。它验证的是格式门禁与多语言数据结构,不代替 API 26 设备上的实际图片写回。最终验收仍要使用真实 JPEG、PNG、GIF 文件执行“写入、关闭、重新打开、回读、比对”。

错误处理不要只看 message

async function safeWrite(
  source: image.ImageSource,
  xmp: image.XMPMetadata
): Promise<boolean> {
  try {
    await writeXmp(source, xmp)
    return true
  } catch (error) {
    const businessError = error as BusinessError
    console.error(
      `write XMP failed, code=${businessError.code}, ` +
      `message=${businessError.message}`
    )
    return false
  }
}

日志至少包含:

  • 图片真实格式,而不是只看文件后缀;
  • 目标 XMP 路径;
  • 标签类型;
  • 是否创建过父容器;
  • 错误码和错误信息;
  • 写入后回读的实际值。

方案对比

方案能否证明写回成功主要问题
只检查 setValue 不抛错不能只证明内存对象可修改
writeImageMetadata 返回后提示成功不充分没验证目标文件内容
写入后读取同一对象不能仍然是内存数据
重新打开文件并逐标签比较可以成本稍高,但证据完整

对用户来说,“保存成功”意味着以后重新打开仍能看到数据。因此,文件级回读不是额外测试,而是保存功能的一部分。

上线前检查清单

  • targetSDKVersion 与使用的 API 26 能力匹配
  • 图片真实格式经过门禁
  • DNG/TIFF 不进入写回流程
  • 多语言文本先创建 ALTERNATE_TEXT 容器
  • 数组下标从 1 开始
  • 每个本地化元素都有 xml:lang
  • 自定义前缀先注册命名空间
  • 结构体和数组先创建父节点
  • 写入失败记录 BusinessError 代码
  • 写入后重新打开文件并回读比对
  • JPEG、PNG、GIF 各准备至少一个真实样本

官方资料

XMP 排障最有效的顺序是:先看格式能不能写,再看路径结构是否完整,最后用重新打开文件的方式回读。把这三步固定下来,就不会再把“内存里改过了”误判成“图片文件已经保存”。

Logo

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

更多推荐