【问题标题】:Reading JPG file's XMP metadata读取 JPG 文件的 XMP 元数据
【发布时间】:2014-04-23 19:17:06
【问题描述】:

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

基本上谷歌已经描述了使用的元数据here

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

我已经尝试过 Commons-Imaging、元数据提取器和最近的 Adob​​es 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();
                }

【问题讨论】:

    标签: java android xmp


    【解决方案1】:

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

    以下引用来自 Adob​​e XMP 规范第 3 部分,用于将 ExtendedXMP 块作为 JPEG APP1:

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

    • 以 null 结尾的签名字符串“http://ns.adobe.com/xmp/extension/”。
    • 存储为 32 字节 ASCII 十六进制字符串的 128 位 GUID,大写 A-F,无空终止。 GUID 是完整的 128 位 MD5 摘要 扩展 XMP 序列化。
    • 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 格式。

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

    原图来自here

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

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

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

    【讨论】:

      【解决方案2】:

      我已经能够使用 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
      }
      

      【讨论】:

        【解决方案3】:

        我遇到了同样的问题,我认为问题在于扩展数据存储在第二个 xmpmeta 部分中,例如元数据提取器会跳过该部分。所以我能做的是在字节流中搜索每个部分,看看它是否具有我期望的属性。我还发现,至少对于深度图数据,base 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);
                }
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-07-14
          • 1970-01-01
          • 2017-07-26
          • 1970-01-01
          • 2010-10-02
          • 2011-02-04
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多