4

我是 Zend 框架的新手,所以如果我遗漏了一些简单的东西,我深表歉意。但是,我原以为直接从文档中获取的代码会起作用。相反,我得到了一个未捕获的异常。

Fatal error:  Uncaught exception 'Zend_Pdf_Exception' with message 'Cross-reference streams are not supported yet.' in C:\xampp\php\zend\library\Zend\Pdf\Parser.php:318
Stack trace:
#0 C:\xampp\php\zend\library\Zend\Pdf\Parser.php(460): Zend_Pdf_Parser->_loadXRefTable('116')
#1 C:\xampp\php\zend\library\Zend\Pdf.php(318): Zend_Pdf_Parser->__construct('PDF/Current...', Object(Zend_Pdf_ElementFactory_Proxy), true)
#2 C:\xampp\php\zend\library\Zend\Pdf.php(267): Zend_Pdf->__construct('PDF/Current...', NULL, true)
#3 C:\xampp\htdocs\test\test.php(7): Zend_Pdf::load('PDF/Current...')
#4 {main}
  thrown in C:\xampp\php\zend\library\Zend\Pdf\Parser.php on line 318

我一直在四处寻找可能的解决方案,但运气不佳。是最相似的,它不能解决我的问题。从我在那里读到的以及从其他来源来看,PDF 版本 1.4 和更早的版本应该可以正常工作,但这里不是这种情况,而且它已经存在多年了。我的 PDF 版本都是 1.4,所以我什至不确定该帖子的准确性。该代码适用于演示中包含的 PDF,但不适用于我尝试使用的任何现有 PDF。我会上传 PDF,但它们都是机密的。

我只是想获取元数据,但我什至无法加载文档。我开始使用框架,因此我不必创建自己的解析器。如果有更简单的方法可以做到这一点,或者如果有人可以对此有所了解,我将非常感激。

编辑:为澄清起见,我已经尝试了链接文档页面中的两种方法。两者都不起作用。

4

4 回答 4

4

我最终不得不为此创建自己的解析器。如果有人发现这一点并对我的做法有任何进一步的建议或问题,请添加评论。

解决方案

我不会上传整个代码,因为它真的很长、很乱而且效率低下。自从最初的帖子以来,我作为一名开发人员已经成长了一些,并且一直打算回去再试一次。所以我会用这篇文章来解释我有什么,指出我发现的一些问题和解决方案,并就如何提高效率发表一些评论。希望这会让你更容易,并希望这会激励我做出一些改变。免责声明:自从我上次查看此代码以来已经有几个月了,所以不要指望我会记住所有内容。但是,我非常擅长(一次)记录我的代码和发现,所以我不记得的大部分都是次要的。

我可以告诉您的最重要的事情是查看原始 XML、做笔记并比较您的一些文件。Adobe 在创建元数据语法时显然无法下定决心,因此您最终将不得不为所有不同的修订添加多个检查(稍后我将给出一个示例)。实际上在文档中查找元数据非常容易。Adobe 为您提供了一组很好的开始/结束标签,因此您只需遍历文档直到找到它们。这是我正在解析的一个 PDF 中的一个经过清理和概括的示例。

<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 4.2.1-c043 52.372728, 2009/01/18-15:08:04        ">
    <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
        <rdf:Description rdf:about=""
            xmlns:dc="http://purl.org/dc/elements/1.1/">
            <dc:format>application/pdf</dc:format>
            <dc:title>
                <rdf:Alt>
                    <rdf:li xml:lang="x-default">Title of Document</rdf:li>
                </rdf:Alt>
            </dc:title>
            <dc:creator>
                <rdf:Seq>
                    <rdf:li>Creator of Document (Not author)</rdf:li>
                </rdf:Seq>
            </dc:creator>
            <dc:description>
                <rdf:Alt>
                    <rdf:li xml:lang="x-default">Short description</rdf:li>
                </rdf:Alt>
            </dc:description>
        </rdf:Description>
        <rdf:Description rdf:about=""
            xmlns:xmp="http://ns.adobe.com/xap/1.0/">
            <xmp:CreateDate>2004-01-27T16:36:09Z</xmp:CreateDate>
            <xmp:CreatorTool>FrameMaker 7.0</xmp:CreatorTool>
            <xmp:ModifyDate>2012-02-20T15:55:19Z</xmp:ModifyDate>
        </rdf:Description>
        <rdf:Description rdf:about=""
            xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
            <pdf:Producer>Acrobat Distiller 9.4.5 (Windows)</pdf:Producer>
        </rdf:Description>
        <rdf:Description rdf:about=""
            xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/">
            <xmpMM:DocumentID>uuid:4eae0fcf-f493-4773-9473-f81c7491e8aa</xmpMM:DocumentID>
            <xmpMM:InstanceID>uuid:98209926-ba98-4ac7-a5f7-050050048f5d</xmpMM:InstanceID>
        </rdf:Description>
    </rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>

查看原始 XML 数据的最佳方法是下载 notepad++(尽管您可以使用任何类似记事本的程序)并在其中打开 PDF。您将首先看到的是 PDF 版本,在这种情况下为“%PDF-1.4”,然后是许多看起来令人困惑的字符。忽略这一点,但请注意 PDF 版本。请注意上面示例中的“xpacket”标签,这是您每次想要查找元数据时都需要查找的标签。只需 Ctrl+F 即可找到“xmpmeta”,第一次出现的应该是您的元数据。警告:不要尝试使用受密码保护的文档。一切都被混淆了,包括元数据,这也意味着 PHP 也无法读取它。我相信有一个选项可以允许读取受密码保护的 PDF 中的元数据,但我无法确定,也不知道它是否真的适用于 PHP。

就像您可以 Ctrl+F 在 notepad++ 中查找元数据一样,您也可以在 PHP 中使用fgets()和一个while循环。我没有做但可能是一个好主意的事情是确定从文档的哪一端开始。这在所有 PDF 版本之间并不通用,但相同版本的位置似乎相似。例如,在 PDF 1.4 中,它们似乎都更靠近文档的底部,而在 PDF 1.6 中,它们都更靠近顶部。同样,您可以从第一行检查 PDF 版本。用 PHP 阅读文档应该很容易设置,所以我将跳过这段代码。不过,我会指出,一旦找到整个元数据就退出循环是个好主意,因为这是一个处理非常密集的操作,因此您需要尽可能节省时间。我还建议一次只在 10-20 个文件组上运行它,如果文件较大,则更少。

在字符串中获取元数据后,您需要对其进行一些清理。您要做的第一件事是确保将元数据很好地包装在单个根节点中,以便 XML 解析器可以读取它。有几个例子他们不是。解决此问题的最佳/最简单方法是添加一个通用包装器。我建议您使用最常见的一种。对我来说,那是带有内部“rdf”包装器的“xmpmeta”标签。确保每个元数据开始相同对于导航文档很重要。可能有更好的方法来做到这一点,但这很有效,而且效率并不低(至少现在,在我删除了两个循环之后)。

if(strpos($xmlstr, 'xmpmeta') === FALSE) {
    if(strpos($xmlstr, 'rdf:rdf') === FALSE) { $xmlstr = "<rdf>$xmlstr</rdf>"; }
    $xmlstr = "<xmpmeta>$xmlstr</xmpmeta>";
}

之后,您将要删除命名空间。我尝试使用它们,但是当 URL 在每个实现中不断变化并且您不确定自己拥有哪些 URL 时,这样做有点困难。此外,它已经开始运行缓慢,添加所有额外的 XML 解析只会让情况变得更糟。删除它们要简单得多。

$nodesToRemove = array('rdf', 'pdf', 'xap', 'xapMM', 'xmp', 'xmpMM', 'dc', 'x');
foreach($nodesToRemove as $remove) { $xmlstr = str_replace("$remove:", '', $xmlstr); }
$xmlstr = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $xmlstr);
$xmlstr = preg_replace("/xmlns[^=]*='[^']*'/i", '', $xmlstr);

$dom = new DOMDocument();
$dom->loadXML($xmlstr);
$sxe = simplexml_import_dom($dom);
$root = $dom->documentElement;
$namespaces = $sxe->getDocNamespaces(TRUE);

foreach($namespaces as $prefix => $uri) {
    $root->removeAttributeNS($uri, $prefix);
    $root->removeAttribute("xmlns:$prefix");
}

if($root->hasChildNodes()) {
    foreach($root->childNodes as $element) {
        if ($element->nodeType != XML_TEXT_NODE) {
            $this->_removeNS($element, $namespaces);
        }
    }
}

$nodesToRemove你来说可能有点不同。这些只是我遇到的所有命名空间。笔记:我遇到了删除节点的顺序很重要的问题。我不知道为什么,但它会从“xmpMM”中删除“xmp”,我会被困在“MM”命名空间中。上面的代码似乎没有这个问题,所以我不确定它是否仍然是一个问题,但以防万一,要小心。无论哪种方式,它都不太难修复,只需让 PHP 对其进行排序然后反转它。REGEX 删除默认命名空间声明。我尝试了许多不同的方法来解决这个问题,但这是我能找到的唯一一种始终有效的方法。可能有一种方法可以结合这两个 REGEX 函数,但是当谈到 REGEX 时我完全迷失了,我的尝试只是让它坏了。我不确定为什么我要使用 XML 再次删除命名空间。这似乎是我最近尝试清理一下的尝试之一,但是这是来自一个可行的解决方案,所以它不会受到伤害(至少不是功能)。除了 REGEX 之外,第一个位可能会被删除并替换为 XML 解决方案,尽管我尚未对此进行验证。在将字符串加载到 XML 之前,仍然需要删除默认名称空间,因为 XML 解析器不认为“xmlns”属性是实际属性。命名空间版本的唯一原因“ 在将字符串加载到 XML 之前,仍然需要删除默认名称空间,因为 XML 解析器不认为“xmlns”属性是实际属性。命名空间版本的唯一原因“ 在将字符串加载到 XML 之前,仍然需要删除默认名称空间,因为 XML 解析器不认为“xmlns”属性是实际属性。命名空间版本的唯一原因“xmlns:$prefix" 起作用是因为它们不被视为 "xmlns" 属性,而是 " xmlns:$prefix" 属性。微妙之处。

不要像我一样。不要尝试实现曾经创建的每个版本的 PDF。这是做不到的。嗯......它可能可以,但它比它的价值更麻烦。对我来说幸运的是,这些都是内部文档,所以当我达到我的极限并且厌倦了调整它只是为了破坏其他东西,或者失去我以前拥有的兼容性时,我只是转换了最后几个文档。找到最常见的版本并处理它们,然后找到下一个最常见的版本并为它们设置条件,依此类推。一旦你到了只剩下几个的地步,更新它们,或者只是宣布你不支持这个版本。特别是如果他们年纪大了。为只用于少数文档的东西添加功能是没有意义的。我能记得的一件大事是“

清理元数据后,您就可以将其解析为 XML。例如,这是我获取描述的方式。

function getDescription($xml) {
    $return = 'Error: Metadata could not be retrieved';//Return value if metadata can not be parsed

    $sxe = new SimpleXMLElement($xml);

    $xpath = array(
        '//description/Alt/li',
        '//Description/Alt/li',
        '//xmpmeta/RDF/*[last()]',
        //'//Description/description',
    );
    foreach($xpath as $pattern) {
        $temp = $sxe->xpath($pattern);

        if( ! empty($temp)) {
            $return = isset($temp[0]->description) ? $temp[0]->description : $temp[0];
            break;
        }
    }

    //Return value if description was not found in metadata
    return empty($return) ? 'Error: Metadata "description" could not be retrieved' : strval($return);
}

有几点需要注意。第一个是 XPATH 的数组。这些是我之前谈到的多重条件。您可能还注意到注释掉了 XPATH。那是我仍在为兼容性工作或已经放弃的一个。我不记得了,自从我不得不看这个以来已经有一段时间了,而且没有人抱怨错误。所以我假设这不是问题。需要注意的另一件事是仅此 ONE 字段的偏差量。元数据发生了很大变化,有时还会恢复。因此,您必须检查每种情况,确保没有其他偏差,然后添加可能发生的任何其他情况。需要研究的是根据版本保存单独的解析器,然后加载正确的解析器,这可能会降低效率。现在回想起来,也许更简单的方法是查找每个修订版的标准化文档,但我最终主要是通过反复试验来完成这项工作。因此,虽然这对我有用,但我可能错过了一些事情,因为这在我的任何文档中都不是问题。需要注意的另一件事是修订之间的标签有多相似。我不是,而且对于高级 XPATH 仍然不是那么好,所以也许有更好的方法来做到这一点,我不知道。需要注意的另一件事是修订之间的标签有多相似。我不是,而且对于高级 XPATH 仍然不是那么好,所以也许有更好的方法来做到这一点,我不知道。需要注意的另一件事是修订之间的标签有多相似。我不是,而且对于高级 XPATH 仍然不是那么好,所以也许有更好的方法来做到这一点,我不知道。

我希望这会有所帮助。我知道它给了我一些想法。如果您有任何其他具体问题,请告诉我。

于 2012-05-08T14:01:19.420 回答
4

就我而言,当我将 PDF 转换为 1.4 版(从 1.6 版)时,它起作用了。我使用了这里的命令:https ://superuser.com/questions/25598/linux-pdf-version-converter

gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/screen -dNOPAUSE -dQUIET -dBATCH -sOutputFile=output.pdf input.pdf
于 2019-03-13T15:37:48.010 回答
2

我在使用 OpenOffice Writer 的导出到 PDF 功能生成的 PDF 时遇到了同样的问题。在 Acrobat 或其他 PDF 阅读器中,它们可以毫无问题地打开,但 ZF 无法处理它们。我将 OpenOffice 文件保存为 .docs 并使用 MS Word 将它们导出为 .pdf。现在它们显示...

于 2012-06-06T10:56:26.270 回答
0

我在使用 adobe 创建的 pdf 文档时遇到了同样的问题。

这次我再次重新保存了文档,而不是使用 adobe 的标准保存选项。这次我使用“优化的 PDF”(另存为下的另一个 adobe 预设)保存为文档。

现在zend可以打开文件并且工作正常。

我不太确定预设中的哪些选项不同,但我认为这是 zend 无法处理的某种流式传输/分割的网络版本。

于 2016-08-28T12:25:25.253 回答