1

I've read a range of Questions and web articles but seem to be up against a brick wall. I have reduced my problem to a very simple SVG file, containing a single image. When converted from PHP via imagick, the image is rendered blank, but from the command line using the same rsvg engine it renders fine.

This project has worked like a charm on my local Fedora dev machine, only when moving to production on Centos have I had this trouble.

Environment: Centos 6.2 server, PHP 5.6.36 using php-fpm, Imagick module version 3.4.3 using ImageMagick library version 6.7.8-9 2016-06-16

Note: my apache server does not jail or chroot the file system at all, so all normal path references should work fine, e.g. it writes to a log file in the web root using an absolute path name without problem.

$ 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"

So I believe the format 'SVG' indicates it will use RSVG for the conversion from PHP.

My SVG (saved from 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>

My PHP code:

<?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";

The same attempt from command line:

convert rsvg:src.svg cli.jpg

The same using rsvg-convert (as indicated in delegates output above):

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

Result: Both command line attempts, cli.jpg and rsvg-convert.jpg, show the image just fine. The attempt from PHP shows a blank output i.e. the <image> element did not render.

There is no exception thrown, or error output that I can find.

I have tried:

  • Make the xlink:href be a file:// URL with absolute path, i.e. xlink:href="file:///home/myuser/public_html/test/svg-problem/image.jpg"
  • Make the xlink:href be a file:// URL with relative to path current directory i.e. xlink:href="file://./image.jpg"
  • Make the xlink:href be a file:// URL with no path i.e. xlink:href="file://image.jpg"
  • Force 'RSVG' usage with $im->setFormat('RSVG') .. it throws an exception with Unable to set format. I cannot see why.

Since the conversion via PHP completes without calling setFormat, and the formats list shows above says it will use RSVG by default, I think it's using RSVG, even though my attempt to force RSVG format failed.

I have read that you can recompile the rsvg library to show more error output when it fails to load a file, but on my production cPanel managed server I am very loath to try recompiling anything.

Any ideas what to try next? Pulling my hair out here :)

fmw42
  • 46,825
  • 10
  • 62
  • 80
Neek
  • 7,181
  • 3
  • 37
  • 47
  • 1
    You write the output to a `out.jpg` but set `$im->setImageFormat('png24')`? In addition, are you sure the file produced from rsvg-convert directly is really a jpg? The command line tool does not offer that output format. From my attempt, I got a PNG even if I set a name out.jpg. – ccprog May 21 '18 at 13:15
  • 1
    And I came across [this](https://github.com/lovell/sharp/issues/973) thread. Maybe try loading the image with `$im->readImage()`. – ccprog May 21 '18 at 13:17
  • Thank you @ccprog, well spotted, yes I mis-typed it as jpg in my sample program, I have edited my Question accordingly. However, fixing this to `.png` still results in a blank 500x500 px image. Interestingly, the `.jpg` output by my original does seem to be a JFIF image, according to linux's `file` command, and by inspecting its header bytes, not sure why that's different to your experience. This is a red herring anyway :) – Neek May 22 '18 at 04:44
  • @ccprog it seems you are correct in your `$im->readImage()` suggestion. Adding the `readImage('src.svg')` instead of using `file_get_contents`, does seem to render the out.png correctly. I cannot see why, can you write anything more conclusing in an Answer that I could accept? In my case, I can live with outputting the SVG to a file on disk instead of loading it from an in-memory string, so this is a workable solution for me. – Neek May 22 '18 at 04:50

2 Answers2

2

The problem here is that I don't know how ImageMagick uses the librsvg interface in detail. But a few things stand out in the latest librsvg doc. While the paragraphs were only added in the v2.42 doc, I think they apply also to older versions, according to what I have observed in Gnome systems.

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.

Depending on the ImageMagick implementation, that leads to two possible solutions:

  1. Load the SVG directly from a file with

     $im->readImage('src.svg');
    
  2. Use the string, but set the second file argument in

     $im->readImageBlob('<?xml version="1.0"?>' . $svg, 'src.svg')
    

Only as an aside, $im->setFormat('RSVG') doesn't make any sense, since "RSVG" is only the name of the library and not a file format.

Community
  • 1
  • 1
ccprog
  • 20,308
  • 4
  • 27
  • 44
  • I see your point.. maybe, once I'm reading the SVG in from a string, it has no 'current directory' concept. I did think that absolute pathnames for the images would work, but apparently not. Writing the SVG to file and using relative links, so that rsvg sees the SVG and image files on the file system, seems to work fine. Haven't time to try your solution 2 yet. This project has been put on the back burner for a little while due to other constraints but we'll be back on it soon, your advice helped us get proof of concept working so it's got legs :) – Neek May 26 '18 at 09:09
0

I could not make it work through IMagick PHP library, but using proc_open leads to exactly same result as when calling from command line:

$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
Meglio
  • 1,646
  • 2
  • 17
  • 33
  • I also gave up on imagick and am calling `rsvg-convert` command line via `exec()`. Note that with PHP you may have to edit your php.ini (or php-fpm ini file, wherever that is for you) to allow use of `exec` or `proc_open` via the `disallow_functions` directive. For me, `exec` was disallowed and returned no data, but did log an error in the error_log. – Neek Jan 30 '20 at 09:09