阅读JPG文件的XMP元数据

时间:2014-04-23 19:17:06

标签: java android xmp

我正在开发Android应用程序,该应用程序应该使用Google Camera的新深度图生成功能。

Google基本上描述了使用的元数据here

我可以访问大多数元数据,但遗憾的是最重要的数据被编码为extendedXmp,我无法让任何XMP解析库正确解析它!

我尝试过Commons-Imaging,元数据提取器和最近的Adobes XMPCore

XMPCore可能能够处理扩展版本,但是没有文档如何让它解析JPG文件中的数据,假设要传递原始XMP数据

是否有正确的XMP解析实现,包括JPG文件的扩展部分,或者我只是做错了什么?

这是我的尝试:

使用Commons-Imaging:

                try {
                    String imageParser = new JpegImageParser().getXmpXml(new ByteSourceInputStream(imageStream, "img.jpg"), new HashMap<String, Object>());

                    Log.v(TAG, imageParser);

                } catch (ImageReadException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }

使用元数据提取器

                Metadata metadata = ImageMetadataReader.readMetadata(
                        new BufferedInputStream(imageStream), false);


                XmpDirectory xmp = metadata
                        .getDirectory(XmpDirectory.class);
                XMPMeta xmpMeta = xmp.getXMPMeta();



                String uri = "http://ns.google.com/photos/1.0/depthmap/";

                Log.v(TAG, xmpMeta.doesPropertyExist(uri, "GDepth:Format") + " " );

                try {
                    XMPProperty hasExtendedXMP = xmpMeta.getProperty("http://ns.adobe.com/xmp/note/", "xmpNote:HasExtendedXMP");

                    Log.v(TAG, hasExtendedXMP.getValue().toString() + " " + new String(Base64.decode(hasExtendedXMP.getValue().toString(), Base64.DEFAULT)));

                } catch (XMPException e) {
                    e.printStackTrace();
                }

3 个答案:

答案 0 :(得分:6)

最初,Adobe没想到XMP数据长度会超过一个JPEG段(约64K)的限制,而他们的XMP规范声明XMP数据必须合二为一。后来,当他们发现单个JPEG APP1段不足以容纳XMP数据时,他们改变了他们的规范,以允许整个XMP数据的多个APP1段。数据分为两部分:标准XMP和ExtendedXMP。标准XMP部件是带有包装器的“普通”XMP结构,而ExtendedXMP部件没有包装器。 ExtendedXMP数据可以进一步划分为多个APP1。

以下引用来自Adobe XMP规范第3部分,适用于ExtendedXMP块作为JPEG APP1:

  

每个块都在单独的APP1标记内写入JPEG文件   分割。每个ExtendedXMP标记段包含:

     
      
  • 以空值终止的签名字符串“http://ns.adobe.com/xmp/extension/”。
  •   
  • 存储为32字节ASCII十六进制字符串的128位GUID,大写字母A-F,无空终止。 GUID是完整的128位MD5摘要   ExtendedXMP序列化。
  •   
  • ExtendedXMP序列化的全长,为32位无符号整数
  •   
  • 此部分的偏移量为32位无符号整数。
  •   
  • ExtendedXMP的一部分
  •   

我们可以看到除了以null结尾的字符串作为ExtendedXMP数据的id之外,还有一个GUID,其值应与标准XMP部分中的值相同。偏移量用于连接ExtendedXMP的不同部分 - 因此ExtendedXMP APP1的顺序可能甚至不是有序的。然后是实际的数据部分,这就是为什么@Matt的答案需要一些方法来修复字符串。还有另一个值 - ExtendedXMP序列化的全长,它有两个用途:检查数据的完整性以及提供加入数据的缓冲区大小。

当我们找到一个ExtendedXMP段时,我们需要将当前数据与其他ExtendedXMP段连接起来,最后得到整个ExtendedXMP数据。然后,我们将两个XML树连接在一起(同样从标准XMP部件中删除GUID)以检索整个XMP数据。

我在Java中创建了一个库icafe,它可以提取和插入XMP以及ExtendedXMP。 ExtendedXMP的用例之一是Google的深度图数据,实际上是作为元数据隐藏在实际图像中的灰度图像,而在JPEG的情况下,作为XMP数据。深度图图像可以用于例如模糊原始图像。深度图数据通常很大,必须分成标准和扩展的XMP部分。整个数据是Base64编码的,可以是PNG格式。

以下是示例图像和提取的深度图:

enter image description here

原始图片来自here

注意:最近我发现另一个website谈论Google Cardboard Camera应用程序,它可以利用JPEG XMP数据中嵌入的图像和音频。 ICAFE现在支持从这些图像中提取图像和音频。使用以下调用JPEGTweaker.extractDepthMap()

可以找到here示例用法

答案 1 :(得分:3)

我已经能够使用metadata-extractor库读取存储在XMP中的Picasa面部数据,并通过XMP属性读取迭代器:

try {
    Metadata metadata = ImageMetadataReader.readMetadata(imageFile);
    XmpDirectory xmpDirectory = metadata.getDirectory(XmpDirectory.class);
    XMPMeta xmpMeta = xmpDirectory.getXMPMeta();
    XMPIterator itr = xmpMeta.iterator();
    while (itr.hasNext()) {
        XMPPropertyInfo pi = (XMPPropertyInfo) itr.next();
        if (pi != null && pi.getPath() != null) {
            if ((pi.getPath().endsWith("stArea:w")) || (pi.getPath().endsWith("mwg-rs:Name")) || (pi.getPath().endsWith("stArea:h")))
                System.out.println(pi.getValue().toString());
        }
    }
} catch (final NullPointerException npe) {
  // ignore
}

答案 2 :(得分:1)

我遇到了同样的问题,我认为问题在于扩展数据存储在第二个xmpmeta部分中,例如,元数据提取器会跳过该部分。所以我能够做的是在字节流中搜索每个部分,看看它是否具有我期望的属性。我还发现,至少对于深度图数据,基本64编码的字符串显然被分成大约64 KB的部分,并且包括一些需要被移除的标头以便正确地解码字符串。下面的fixString函数很可能被知道分块信息的人所取代。这取决于https://www.adobe.com/devnet/xmp.html提供的xmpcore库。

import java.io.*;
import java.util.*;
import com.adobe.xmp.*;
import com.adobe.xmp.impl.*;

public class XMP
{
    // An encoding should really be specified here, and for other uses of getBytes!
    private static final byte[] OPEN_ARR = "<x:xmpmeta".getBytes();
    private static final byte[] CLOSE_ARR = "</x:xmpmeta>".getBytes();

    private static void copy(InputStream in, OutputStream out) throws IOException
    {
        int len = -1;
        byte[] buf = new byte[1024];
        while((len = in.read(buf)) >= 0)
        {
            out.write(buf, 0, len);
        }

        in.close();
        out.close();
    }

    private static int indexOf(byte[] arr, byte[] sub, int start)
    {
        int subIdx = 0;

        for(int x = start;x < arr.length;x++)
        {
            if(arr[x] == sub[subIdx])
            {
                if(subIdx == sub.length - 1)
                {
                    return x - subIdx;
                }
                subIdx++;
            }
            else
            {
                subIdx = 0;
            }
        }

        return -1;
    }

    private static String fixString(String str)
    {
        int idx = 0;
        StringBuilder buf = new StringBuilder(str);
        while((idx = buf.indexOf("http")) >= 0)
        {
            buf.delete(idx - 4, idx + 75);
        }

        return buf.toString();
    }

    private static String findDepthData(File file) throws IOException, XMPException
    {
        FileInputStream in = new FileInputStream(file);
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        copy(in, out);
        byte[] fileData = out.toByteArray();

        int openIdx = indexOf(fileData, OPEN_ARR, 0);
        while(openIdx >= 0)
        {
            int closeIdx = indexOf(fileData, CLOSE_ARR, openIdx + 1) + CLOSE_ARR.length;

            byte[] segArr = Arrays.copyOfRange(fileData, openIdx, closeIdx);
            XMPMeta meta = XMPMetaFactory.parseFromBuffer(segArr);

            String str = meta.getPropertyString("http://ns.google.com/photos/1.0/depthmap/", "Data");

            if(str != null)
            {
                return fixString(str);
            }

            openIdx = indexOf(fileData, OPEN_ARR, closeIdx + 1);
        }

        return null;
    }

    public static void main(String[] args) throws Exception
    {
        String data = findDepthData(new File(args[0]));
        if(data != null)
        {
            byte[] imgData = Base64.decode(data.getBytes());
            ByteArrayInputStream in = new ByteArrayInputStream(imgData);
            FileOutputStream out = new FileOutputStream(new File("out.png"));
            copy(in, out);
        }
    }
}