通过 rsvg 转换 SVG 的 Imagick 从 PHP 呈现图像空白,但从命令行可以

Imagick converting SVG via rsvg renders image blank from PHP, but ok from command line

我已经阅读了一系列问题和网络文章,但似乎遇到了困难。我已将我的问题简化为一个非常简单的 SVG 文件,其中包含一个图像。当通过 imagick 从 PHP 转换时,图像呈现为空白,但从命令行使用相同的 rsvg 引擎它呈现正常。

这个项目在我本地的 Fedora 开发机器上运行得很好,只有在 Centos 上进行生产时我才遇到这个问题。

环境:Centos 6.2 服务器,PHP 5.6.36 使用 php-fpm,Imagick 模块版本 3.4.3 使用 ImageMagick 库版本 6.7.8-9 2016-06-16

注意:我的 apache 服务器根本没有对文件系统进行 jail 或 chroot,因此所有正常的路径引用都应该可以正常工作,例如它使用绝对路径名毫无问题地写入 Web 根目录中的日志文件。

$ identify -list format | grep -i svg
     MSVG  SVG       rw+   ImageMagick's own SVG internal renderer
      SVG  SVG       rw+   Scalable Vector Graphics (RSVG 2.39.0)
     SVGZ  SVG       rw+   Compressed Scalable Vector Graphics (RSVG 2.39.0)


$ identify -list delegate | grep -i svg
        cdr =>          "uniconvertor" "%i" "%o.svg"; mv "%o.svg" "%o"
        dot =>          "dot" -Tsvg "%i" -o "%o"
        svg =>          "rsvg-convert" -o "%o" "%i"

所以我相信格式 'SVG' 表示它将使用 RSVG 进行从 PHP 的转换。

我的 SVG(从 Inkscape 保存):

<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="620mm" height="640mm" viewBox="0 0 620 640" version="1.1" id="svg28" inkscape:version="0.92.3 (2405546, 2018-03-11)" sodipodi:docname="radcover-620x640b.svg">
  <defs id="defs22"/>
  <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.24748737" inkscape:cx="1386.1321" inkscape:cy="1147.0763" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="1920" inkscape:window-height="1016" inkscape:window-x="2048" inkscape:window-y="27" inkscape:window-maximized="1" inkscape:snap-global="false"/>
  <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,343)">
  <image fill-opacity="0" stroke="none" stroke-opacity="0" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" x="54.336063385009766" y="-280.25213623046875" width="280.0984802246094" height="175.3288116455078" preserveAspectRatio="none" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="image.jpg" data-number="1" transform="matrix(0.98887052,-0.14877865,0.14877865,0.98887052,0.00000000,0.00000000)"/>
  </g>
</svg>

我的PHP代码:

<?php
$widthPx = 500;
$heightPx = 500;
$outFilename = 'out.png';

$svg = file_get_contents('src.svg');

$im = new Imagick();
$im->readImageBlob('<?xml version="1.0"?>' . $svg);
$im->setImageFormat('png24');

$im->resizeImage($widthPx, $heightPx, imagick::FILTER_LANCZOS, 1);

$im->writeImage($outFilename);

echo "Done.\n";

来自命令行的相同尝试:

convert rsvg:src.svg cli.jpg

同样使用 rsvg-convert(如上面的委托输出所示):

rsvg-convert src.svg -o rsvg-convert.jpg

结果:cli.jpgrsvg-convert.jpg 两种命令行尝试都可以很好地显示图像。 PHP 的尝试显示空白输出,即 <image> 元素未呈现。

我没有发现任何异常抛出或错误输出。

我试过:

由于通过 PHP 完成的转换没有调用 setFormat,并且上面显示的格式列表说它将默认使用 RSVG,我认为它正在使用 RSVG,即使我试图强制使用 RSVG格式化失败。

我了解到您可以重新编译 rsvg 库以在加载文件失败时显示更多错误输出,但在我的生产 cPanel 托管服务器上,我非常不愿意尝试重新编译任何东西。

知道接下来要尝试什么吗?把我的头发拉出来:)

这里的问题是我不知道ImageMagick是如何详细使用librsvg接口的。但在最近的 librsvg doc 中有几件事很突出。虽然这些段落仅添加到 v2.42 文档中,但根据我在 Gnome 系统中观察到的情况,我认为它们也适用于旧版本。

When processing an SVG, librsvg will only load referenced files if they are in the same directory as the base file, or in a subdirectory of it...This is so that malicious SVG files cannot include files that are in a directory above.

If you already have SVG data in memory, you can create a memory input stream...Note that in this case, it is important that you specify the base_file for the in-memory SVG data. Librsvg uses the base_file to resolve links to external content, like raster images.

根据 ImageMagick 的实现,有两种可能的解决方案:

  1. 直接从文件加载 SVG

     $im->readImage('src.svg');
    
  2. 使用字符串,但在

    中设置第二个文件参数
     $im->readImageBlob('<?xml version="1.0"?>' . $svg, 'src.svg')
    

顺便说一句,$im->setFormat('RSVG') 没有任何意义,因为“RSVG”只是库的名称,而不是文件格式。

我无法通过 IMagick PHP 库使其工作,但使用 proc_open 会导致与从命令行调用时完全相同的结果:

$imgPath = '/path/to/image.svg';
$svg = file_get_contents($imgPath);

// ... manipulate svg content here if needed

$descriptorSpec = array(
  0 => array("pipe", "r"), // STDIN
  1 => array("pipe", "w"), // STDOUT
  2 => array("pipe", "w"), // STDERR
);

// fd:0 - STDIN, fd:1: STDOUT,
// svg: and png: tell ImageMagick input/output formats
$cmd = "convert svg:fd:0 png:fd:1";

$proc = proc_open($cmd, $descriptorSpec, $pipes, null, null);

if (!is_resource($proc)) {
  throw new RuntimeException("Failed to proc_open: $cmd");
}

fwrite($pipes[0], $svg);
fclose($pipes[0]);

$png = stream_get_contents($pipes[1]);
fclose($pipes[1]);

if (!$png) {
  throw new RuntimeException("Command returned no output: $cmd");
}

$stdErr = stream_get_contents($pipes[2]);
fclose($pipes[2]);
if ($stdErr) {
  throw new RuntimeException("Command $cmd returned error: $stdErr");
}

$exitStatus = proc_close($proc);

if ($exitStatus) {
  throw new RuntimeException("Command returned $exitStatus: $cmd");
}

// Render PNG with appropriate headers, or save to file, or return etc