读取JPG文件的XMP元数据

7

我正在开发一款Android应用程序,该应用程序应该利用Google相机的新深度图生成功能。

基本上,Google已经描述了在此处使用的元数据here

我可以访问大部分元数据,但不幸的是,最重要的数据被编码为extendedXmp,我无法获得任何XMP解析库来正确解析它!

我尝试过Commons-Imaging、metadata-extractor和最近的Adobes XMPCore

XMPCore可能能够处理扩展版本,但没有文档说明如何从JPG文件中获取它来解析数据,它假定原始XMP数据被传递

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

这是我的尝试:

使用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-extractor
                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个回答

8
最初,Adobe并没有预料到XMP数据长度会超过一个JPEG段(约64K)的限制,他们的XMP规范规定XMP数据必须适合一个段。后来当他们发现单个JPEG APP1段不足以容纳XMP数据时,他们改变了规范,允许多个APP1段用于整个XMP数据。数据被分成两部分:标准XMP和ExtendedXMP。标准XMP部分是一个带有包装器的“正常”XMP结构,而ExtendedXMP部分没有包装器。ExtendedXMP数据可以进一步分割以适应多个APP1。

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

每个块都会被写入JPEG文件中的一个单独的APP1标记段中。每个ExtendedXMP标记段包含:
  • 一个以"http://ns.adobe.com/xmp/extension/"结尾的签名字符串。
  • 作为32字节ASCII十六进制字符串(大写A-F,无空终止符)存储的128位GUID。该GUID是完整ExtendedXMP序列的128位MD5摘要。
  • ExtendedXMP序列化的完整长度,作为32位无符号整数
  • 此部分的偏移量,作为32位无符号整数。
  • ExtendedXMP的部分内容
除了以空字符结尾的字符串作为ExtendedXMP数据的ID之外,我们还可以看到有一个GUID,这个GUID应该与标准XMP部分中找到的值相同。偏移量用于连接ExtendedXMP的不同部分,因此ExtendedXMP APP1的序列可能甚至不是按顺序排列的。然后是实际的数据部分,这就是为什么@Matt的答案需要一些修复字符串的方法。还有另一个值-ExtendedXMP序列化的完整长度,它具有两个目的:检查数据的完整性以及提供用于连接数据的缓冲区大小。
当我们发现一个ExtendedXMP段时,我们需要将当前数据与其他ExtendedXMP段连接起来,最终得到整个ExtendedXMP数据。然后我们将两个XML树连接在一起(也删除标准XMP部分中的GUID),以检索整个XMP数据。
我用Java制作了一个名为icafe的库,可以提取和插入XMP以及ExtendedXMP。其中ExtendedXMP的一个用例是用于谷歌的深度地图数据,实际上是隐藏在实际图像中的灰度图像,作为元数据,在JPEG的情况下,作为XMP数据。深度地图图像可以用于模糊原始图像等。深度地图数据通常很大,必须分成标准和扩展的XMP部分。整个数据被Base64编码,并且可以是PNG格式。
以下是示例图像和提取的深度地图:

enter image description here

这张图片来源于这里

注意:最近我发现另一个网站讨论了Google Cardboard Camera应用程序,该应用程序可以利用嵌入在JPEG XMP数据中的图像和音频。ICAFE现在支持从这些图像中提取图像和音频。示例用法可以在此处找到此处,并使用以下调用JPEGTweaker.extractDepthMap()

这是ICAFE从讨论Google Cardboard Camera应用程序的网站上提取的原始图像:

enter image description here

很遗憾,我找不到在此处插入MP4音频的方法。

3

我使用metadata-extractor库,通过迭代XMP属性,成功读取了存储在XMP中的Picasa人脸数据。

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
}

1
我遇到了同样的问题,我认为问题是扩展数据存储在第二个xmpmeta部分中,例如metadata-extractor会跳过该部分。所以我能做的就是在字节流中搜索每个部分,并查看它是否具有我期望的属性。我还发现,至少对于深度图数据,base64编码字符串显然被分块成大约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);
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接