尚不支持交叉引用流

时间:2012-02-22 17:50:39

标签: php zend-framework pdf zend-pdf

我是Zend Framework的新手,所以如果我遗漏了一些简单的话,我会道歉。但是,我原以为直接从documentation获取的代码会起作用。相反,我得到了一个未被捕获的例外。

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

我一直在四处寻找可能的解决方案,但运气不佳。 This是最相似的,它无法解决我的问题。从我在那里阅读,以及从其他来源,PDF版本1.4及更早版本应该工作正常,但这不是这里的情况,它已经岁月。我的PDF版本都是1.4,所以我甚至不确定该帖子的准确程度。该代码适用于演示中包含的PDF,但不适用于我正在尝试使用的任何现有代码。我会上传PDF,但它们都是保密的。

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

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

4 个答案:

答案 0 :(得分: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版本,&#34;%PDF-1.4&#34;在这种情况下,然后很多令人困惑的看起来字符。忽略这一点,但请注意PDF版本。请注意&#34; xpacket&#34;上面示例中的标记,表示每次要查找元数据时需要查找的内容。只需按Ctrl + F查找&#34; xmpmeta&#34;,第一次出现应该是您的元数据。 提醒:请勿尝试使用受密码保护的文档。一切都是混淆的,包括meta,这也意味着PHP也无法读取它。我相信有一个选项允许在受密码保护的PDF中读取元数据,但我无法记住,也不知道它是否真的适用于PHP。

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

在您获得字符串中的元数据后,您将要稍微清理它。您要做的第一件事是确保您的元数据在一个根节点中很好地包装,以便XML解析器可以读取它。有几个例子他们不是。解决此问题的最佳/最简单方法是添加一个通用包装器。我建议使用最常用的一个。对我来说,这就是&#34; xmpmeta&#34;标签内部&#34; rdf&#34;包装。确保每个元数据启动相同对于导航文档非常重要。可能有一种更好的方法可以做到这一点,但这样做效果并不太低效(至少现在,在我删除了两个循环之后)。

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对您来说可能略有不同。这些只是我遇到的所有命名空间。 注意:我遇到的问题是删除节点的顺序非常重要。我不知道为什么,但它会删除&#34; xmp&#34;来自&#34; xmpMM&#34;我会被一个&#34; MM&#34;命名空间。上面的代码似乎没有这个问题,所以我不确定它是否仍然是一个问题,但为了以防万一,要小心。无论哪种方式,它都难以修复,只需让PHP对其进行排序然后将其反转即可。 REGEX删除默认名称空间声明。我尝试了许多不同的方法来解决这个问题,但这是我能找到的唯一能够持续发挥作用的方法。可能有一种方法可以将这两个REGEX功能结合起来,但是当涉及到REGEX时我完全迷失了,而我的尝试只是让它破了。我不确定为什么然后再用XML删除名称空间。这似乎是我最近尝试清理这一点的尝试之一,但这是来自一个有效的解决方案,所以它不会受到伤害(至少不是功能)。除了REGEX之外,第一位可能会被删除并替换为XML解决方案,但我还没有对此进行验证。在将字符串加载到XML之前,仍然需要删除默认名称空间,因为XML解析器不考虑&#34; xmlns&#34;属性是一个实际属性。命名空间版本的唯一原因&#34; xmlns:$prefix&#34;作品是因为它们不被认为是&#34; xmlns&#34;但是&#34; xmlns:$prefix&#34;属性。微妙之处。

不要跟我一样。不要尝试实现有史以来创建的每个版本的PDF。它无法完成。嗯......它可能会,但它比它的价值更麻烦。幸运的是,这些都是内部文件,所以当我达到极限并且厌倦了调整它只是为了打破别的东西,或者失去我以前的兼容性时,我只是将最后几个文件转换成了。找到最常见的版本并处理它们,然后是下一个最常见的版本和设置条件,依此类推。一旦你达到了只有少数人离开的地方,让他们更新,或者只是宣布你不支持这个版本。特别是如果他们年纪大了。添加功能只是为了只用于少量文档而没有任何意义。我能记住的最大的一个问题是&#34; xpacket&#34;并不总是在自己的路线上。有时它与一些元数据标签共享空间。这导致&#34;缺失&#34;数据,因为直到&#34; xpacket&#34;之后才开始录制元数据。被找到。这似乎是一个简单的修复,但它发现了很多问题,所以我最终完全废弃了这个版本并让它们更新。幸运的是那些是最后3-4个文件。

清理完元数据后,就可以将其解析为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仍然不是那么好,所以也许有更好的方法来做到这一点,我不知道。

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

答案 1 :(得分:2)

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

答案 2 :(得分:0)

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

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

现在zend可以打开文件,它运行正常。

我不太确定哪些选项在预设中有所不同,但我认为它是某种流式/分区式网络版本,而zend无法处理。

答案 3 :(得分:0)

以我为例,当我将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