如何使用PHP读取JPG文件中的XMP数据?

16

PHP内置了读取EXIF和IPTC元数据的支持,但我找不到任何读取XMP的方法?

10个回答

25

XMP数据实际上被嵌入到图像文件中,因此可以使用PHP的字符串函数从图像文件本身中提取它。

以下演示了这个过程(我在使用SimpleXML,但是任何其他XML API甚至简单而巧妙的字符串解析都可以给出相同的结果):

$content = file_get_contents($image);
$xmp_data_start = strpos($content, '<x:xmpmeta');
$xmp_data_end   = strpos($content, '</x:xmpmeta>');
$xmp_length     = $xmp_data_end - $xmp_data_start;
$xmp_data       = substr($content, $xmp_data_start, $xmp_length + 12);
$xmp            = simplexml_load_string($xmp_data);

仅有两点需要注意:

  • XMP大量使用XML命名空间,因此当使用一些XML工具解析XMP数据时,您需要留意这个问题。
  • 考虑到图像文件的可能大小,也许您无法使用file_get_contents()函数,因为该函数会将整个图像加载到内存中。使用fopen()打开文件流资源,并检查数据块中的关键序列<x:xmpmeta</x:xmpmeta>将极大地减少内存占用。

这就解释了为什么PHP中没有XMP特定的函数。 - Liam
这可能不再可靠。现在的jpeg文件中可能有多个XMP块。 - hippietrail
1
@hippietrail 是的。在这些情况下,需要调整逻辑以确定文字XMP包的开始和结束。例如(find <x:xmpmeta, find next </x:xmpmeta>) repeat until no more <x:xmpmeta can be found - Stefan Gehrig

13

我花了很长时间才回复这个问题,因为在谷歌搜索如何解析XMP数据时,这似乎是最好的结果。我曾多次看到几乎完全相同的代码片段被使用,但它浪费了很多内存。以下是Stefan在他的示例后提到的fopen()方法的示例。

<?php

function getXmpData($filename, $chunkSize)
{
    if (!is_int($chunkSize)) {
        throw new RuntimeException('Expected integer value for argument #2 (chunkSize)');
    }

    if ($chunkSize < 12) {
        throw new RuntimeException('Chunk size cannot be less than 12 argument #2 (chunkSize)');
    }

    if (($file_pointer = fopen($filename, 'r')) === FALSE) {
        throw new RuntimeException('Could not open file for reading');
    }

    $startTag = '<x:xmpmeta';
    $endTag = '</x:xmpmeta>';
    $buffer = NULL;
    $hasXmp = FALSE;

    while (($chunk = fread($file_pointer, $chunkSize)) !== FALSE) {

        if ($chunk === "") {
            break;
        }

        $buffer .= $chunk;
        $startPosition = strpos($buffer, $startTag);
        $endPosition = strpos($buffer, $endTag);

        if ($startPosition !== FALSE && $endPosition !== FALSE) {
            $buffer = substr($buffer, $startPosition, $endPosition - $startPosition + 12);
            $hasXmp = TRUE;
            break;
        } elseif ($startPosition !== FALSE) {
            $buffer = substr($buffer, $startPosition);
            $hasXmp = TRUE;
        } elseif (strlen($buffer) > (strlen($startTag) * 2)) {
            $buffer = substr($buffer, strlen($startTag));
        }
    }

    fclose($file_pointer);
    return ($hasXmp) ? $buffer : NULL;
}

值得注意的是,当图像不包含XMP数据时,这个程序会挂起,尽管我相信对于知道如何解决的人来说,这可以很容易地解决。 - Nico Burns
2
我在 while 循环中添加了一个 else\break 条件,如果文件中不存在 XMP 元素,则终止循环。 - Bryan Geraghty
我重构了这个函数,先复制块,然后对缓冲区执行检测/修改,而不是试图对块执行检测/修改。 - Bryan Geraghty
1
难道不可能出现一个块包含“<x:xmp”,而下一个块是“meta....”,导致php脚本错过xmp片段吗? - Niksac
你是绝对正确的。这个函数在chunkSize < 12时无法正常工作。这很容易修复,但感谢你指出来! - Bryan Geraghty
我已经添加了一个检查,以确保chunkSize参数小于12会导致异常。 - Bryan Geraghty

4
在Linux上一个简单的方法是调用exiv2程序,该程序在debian的同名包中可用。
$ exiv2 -e X extract image.jpg

将产生包含嵌入式XMP的image.xmp文件,现在可以解析该文件。


3

我知道...这个帖子有点老了,但它在我寻找解决方案时对我很有帮助,所以我觉得这可能对其他人也有帮助。

我采用了这个基本解决方案,并对它进行了修改,使它能够处理标签跨越多个块的情况。这允许你将块大小设置得更大或更小。

<?php
function getXmpData($filename, $chunk_size = 1024)
{
 if (!is_int($chunkSize)) {
  throw new RuntimeException('Expected integer value for argument #2 (chunkSize)');
 }

 if ($chunkSize < 12) {
  throw new RuntimeException('Chunk size cannot be less than 12 argument #2 (chunkSize)');
 }

 if (($file_pointer = fopen($filename, 'rb')) === FALSE) {
  throw new RuntimeException('Could not open file for reading');
 }

 $tag = '<x:xmpmeta';
 $buffer = false;

 // find open tag
 while ($buffer === false && ($chunk = fread($file_pointer, $chunk_size)) !== false) {
  if(strlen($chunk) <= 10) {
   break;
  }
  if(($position = strpos($chunk, $tag)) === false) {
   // if open tag not found, back up just in case the open tag is on the split.
   fseek($file_pointer, -10, SEEK_CUR);
  } else {
   $buffer = substr($chunk, $position);
  }
 }

 if($buffer === false) {
  fclose($file_pointer);
  return false;
 }

 $tag = '</x:xmpmeta>';
 $offset = 0;
 while (($position = strpos($buffer, $tag, $offset)) === false && ($chunk = fread($file_pointer, $chunk_size)) !== FALSE && !empty($chunk)) {
  $offset = strlen($buffer) - 12; // subtract the tag size just in case it's split between chunks.
  $buffer .= $chunk;
 }

 fclose($file_pointer);

 if($position === false) {
  // this would mean the open tag was found, but the close tag was not.  Maybe file corruption?
  throw new RuntimeException('No close tag found.  Possibly corrupted file.');
 } else {
  $buffer = substr($buffer, 0, $position + 12);
 }

 return $buffer;
}
?>


2

Bryan的解决方案到目前为止是最好的,但它有一些问题,所以我对其进行了修改,简化了它,并删除了一些功能。

我发现他的解决方案有三个问题:

A)如果提取的块恰好落在我们正在搜索的字符串中间,它将找不到。较小的块大小更容易引起此问题。

B)如果块同时包含开始和结束,它将找不到。可以通过额外的if语句重新检查找到开始的块来解决这个问题,以查看是否还找到结束。

C)在else语句中添加的终止while循环的语句如果没有找到xmp数据会产生副作用,即如果第一次无法找到开始元素,则不会再检查任何块。这很容易修复,但由于第一个问题,这不值得。

我下面的解决方案不够强大,但更加稳健。它只会检查一个块,并从中提取数据。它只适用于开始和结束在该块中的情况,因此块大小需要足够大以确保始终捕获该数据。根据我使用Adobe Photoshop / Lightroom导出文件的经验,xmp数据通常从约20kB开始,到约45kB结束。对于我的图像,我的50k块大小似乎很好用,如果你剥离了一些导出的数据,例如具有许多开发设置的CRS块,则会少得多。

function getXmpData($filename)
{
    $chunk_size = 50000;
    $buffer = NULL;

    if (($file_pointer = fopen($filename, 'r')) === FALSE) {
        throw new RuntimeException('Could not open file for reading');
    }

    $chunk = fread($file_pointer, $chunk_size);
    if (($posStart = strpos($chunk, '<x:xmpmeta')) !== FALSE) {
        $buffer = substr($chunk, $posStart);
        $posEnd = strpos($buffer, '</x:xmpmeta>');
        $buffer = substr($buffer, 0, $posEnd + 12);
    }
    fclose($file_pointer);
    return $buffer;
}

我更新了我的函数,修复了它存在的逻辑问题 :) - Bryan Geraghty
啊,谢谢Bryan!我刚刚才注意到你回复了。我会检查一下你修改后的代码,看看它是否适用于我(我还不是一个程序员,所以我还没有完全理解它...)。 - Sebastien B.
哦,我现在明白了...你是一次构建一个缓冲区,并始终检查缓冲区。这可以避免我列出的所有问题。聪明!谢谢。 - Sebastien B.
我已经审查了代码,最后的elseif语句,如果我理解正确的话,是为了清除缓冲区(除了最后一部分,以防起始标记挂在那里)...但从我对substr函数的理解来看...难道不应该是$buffer = substr($buffer, -strlen($startTag));(注意减号,从字符串末尾开始)。现在,没有减号,新的$buffer值将大部分与之前相同,而没有被清除。它可以工作,但效率不如预期。如果我错了,请纠正我(对于这些评论,抱歉)。 - Sebastien B.

2
感谢Sebastien B提供的简化版本:)。如果您想避免块大小对某些文件太小的问题,只需添加递归即可。
function getXmpData($filename, $chunk_size = 50000){      
  $buffer = NULL;
  if (($file_pointer = fopen($filename, 'r')) === FALSE) {
    throw new RuntimeException('Could not open file for reading');
  }

  $chunk = fread($file_pointer, $chunk_size);
  if (($posStart = strpos($chunk, '<x:xmpmeta')) !== FALSE) {
      $buffer = substr($chunk, $posStart);
      $posEnd = strpos($buffer, '</x:xmpmeta>');
      $buffer = substr($buffer, 0, $posEnd + 12);
  }

  fclose($file_pointer);

// recursion here
  if(!strpos($buffer, '</x:xmpmeta>')){
    $buffer = getXmpData($filename, $chunk_size*2);
  }

  return $buffer;
}

2
如果您有可用的ExifTool(非常有用的工具)并且可以运行外部命令,您可以使用其选项提取XMP数据(-xmp:all)并以JSON格式输出(-json),然后您可以轻松地将其转换为PHP对象:
$command = 'exiftool -g -json -struct -xmp:all "'.$image_path.'"';
exec($command, $output, $return_var);
$metadata = implode('', $output);
$metadata = json_decode($metadata);

1
我开发了Xmp Php Tookit扩展:这是一个基于Adobe XMP工具包的php5扩展,提供了从jpeg、psd、pdf、视频、音频中读取/写入/解析xmp元数据的主要类和方法。该扩展受GPL许可证保护。即将推出新版本,支持php 5.3(目前仅兼容php 5.2.x),并应在Windows和macOSX上提供(现在仅适用于freebsd和Linux系统)。 http://xmpphptoolkit.sourceforge.net/

1
我尝试了你的工具包,但是我无法编译它 :( 抱怨缺少printf。 "xmp_toolkit/common/XMP_LibUtils.hpp:179:62: error: ‘printf’ was not declared in this scope" - haggi

0

如果您能够在您的环境中安装exiv2

sudo apt install exiv2

然后,在fluxine的答案基础上,可以使用exiv2将所有图像元数据(EXIF、IPTC和XMP)提取到一个关联数组中。
function image_meta_data($image_path) {
    $meta_data = [];

    // execute exiv2 via the command line
    exec('exiv2 -Pkt ' . $image_path, $output = null, $retval = null);

    // process output into associative array
    foreach ($output as $line) {
        $key = trim(substr($line, 0, 46));
        $value = str_replace('lang="x-default" ', '', trim(substr($line, 46))); // remove in-line language tag
        $meta_data[$key] = $value;
    }

    return $meta_data;
}

使用方法:

$meta = image_meta_data($image_path);
print_r($meta);
// Examples:
echo $meta['Xmp.dc.title'] ?? '';
echo $meta['Iptc.Application2.DateCreated'] ?? '';
echo $meta['Exif.Image.ImageDescription'] ?? '';

0

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