how to resize an SVG with Imagick/ImageMagick

gordyr picture gordyr · Apr 30, 2012 · Viewed 13.1k times · Source

I am sending a string representation of an SVG file to the server and using Imagick to turn this into a jpeg in the following manner:

$image = stripslashes($_POST['json']);
$filename = $_POST['filename'];
$unique = time();

$im = new Imagick();
$im->readImageBlob($image);
$im->setImageFormat("jpeg");
$im->writeImage('../photos/' . $type . '/humourised_' . $unique . $filename);
$im->clear();
$im->destroy();

However I wish to resize the SVG prior to rasterizing it so the the resulting image is larger than the dimensions specified within the SVG file.

I modified my code to the following:

$image = stripslashes($_POST['json']);
$filename = $_POST['filename'];
$unique = time();

$im = new Imagick();
$im->readImageBlob($image);
$res = $im->getImageResolution();
$x_ratio = $res['x'] / $im->getImageWidth();
$y_ratio = $res['y'] / $im->getImageHeight();
$im->removeImage();
$im->setResolution($width_in_pixels * $x_ratio, $height_in_pixels * $y_ratio);

$im->readImageBlob($image);
$im->setImageFormat("jpeg");
$im->writeImage('../photos/' . $type . '/humourised_' . $unique . $filename);
$im->clear();
$im->destroy();

This code should work out the resolution and resize the SVG accordingly. It works perfectly if the SVG canvas and it's elements have 'percentage' based widths, however it doesn't appear to work with elements defined in 'px'. Which is unfortunately a requirement.

A typical SVG string that will be sent to the server looks like this:

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/SVG/DTD/svg10.dtd">
<svg id="tempsvg" style="overflow: hidden; position: relative;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="333" version="1.1" height="444">
   <image transform="matrix(1,0,0,1,0,0)" preserveAspectRatio="none" x="0" y="0" width="333" height="444" xlink:href="http://www.songbanc.com/assets/embed/photos/full/133578615720079914224f9e7aad9ac871.jpg"></image>
   <image transform="matrix(1,0,0,1,0,0)" preserveAspectRatio="none" x="85.5" y="114" width="50" height="38" xlink:href="http://www.songbanc.com/assets/embed/humourise/elements/thumb/thumb_lips4.png"></image>
   <path transform="matrix(1,0,0,1,0,0)" fill="none" stroke="#000" d="M110.5,133L140.5,133" stroke-dasharray="- " opacity="0.5"></path>
   <circle transform="matrix(1,0,0,1,0,0)" cx="140.5" cy="133" r="5" fill="#000" stroke="#000"></circle>
   <path transform="matrix(1,0,0,1,0,0)" fill="none" stroke="#000" d="M110.5,133L110.5,155.8" stroke-dasharray="- " opacity="0.5"></path>
   <circle transform="matrix(1,0,0,1,0,0)" cx="110.5" cy="155.8" r="5" fill="#000" stroke="#000"></circle>
   <circle transform="matrix(1,0,0,1,0,0)" cx="110.5" cy="133" r="5" fill="#000" stroke="#000"></circle>
</svg>

As you can see the elements that make up this SVG has pixel definition widths and heights (using percentages is unfortunately not an option for this application)

Is there any way around this? Or any other method of converting an SVG to png and rendering it at a given size without loss of quality.

Thanks.

EDIT: Although I never actually managed to find a perfect solution. Instead I ended sending the SVG data as json, looping through it server side and scaling the pixels to the intended height.

Then, after much trial and error I realised that imagemagick had issues wih the standard SVG transform/rotate commands, throwing any manipulated elements out of whack. I ended up switching too 'inkscape' to render the resulting SVG as a rasterised image. And all is well. I'm still digging into a potential formulatic solution to offset the differences that imagemagick makes. If I have any success I will update this question again.

Answer

psycho brm picture psycho brm · Nov 21, 2012

As a workaround of php_imagick's bug, you can scale svg's width=".." and height="..":

function svgScaleHack($svg, $minWidth, $minHeight)
{
    $reW = '/(.*<svg[^>]* width=")([\d.]+px)(.*)/si';
    $reH = '/(.*<svg[^>]* height=")([\d.]+px)(.*)/si';
    preg_match($reW, $svg, $mw);
    preg_match($reH, $svg, $mh);
    $width = floatval($mw[2]);
    $height = floatval($mh[2]);
    if (!$width || !$height) return false;

    // scale to make width and height big enough
    $scale = 1;
    if ($width < $minWidth)
        $scale = $minWidth/$width;
    if ($height < $minHeight)
        $scale = max($scale, ($minHeight/$height));

    $width *= $scale*2;
    $height *= $scale*2;

    $svg = preg_replace($reW, "\${1}{$width}px\${3}", $svg);
    $svg = preg_replace($reH, "\${1}{$height}px\${3}", $svg);

    return $svg;
}

Then you can easily create nice transparent PNG!

createThumbnail('a.svg', 'a.png');

function createThumbnail($filename, $thname, $size=50)
{
    $im = new Imagick();
    $svgdata = file_get_contents($filename);
    $svgdata = svgScaleHack($svgdata, $size, $size);

    $im->setBackgroundColor(new ImagickPixel('transparent'));
    $im->readImageBlob($svgdata);

    $im->setImageFormat("png32");
    $im->resizeImage($size, $size, imagick::FILTER_LANCZOS, 1);

    file_put_contents($thname, $im->getImageBlob());
    $im->clear();
    $im->destroy();
}

Note: I've been searching for a solution how to rescale SVG from its initial small size. However it seems that imagick::setResolution is broken. However, ImageMagick library itself is working, so you can use exec('convert...') (might be disabled for security reasons by hosting provider).

So to create thumbnail 50x50 from smaller svg you would do:

convert -density 500 -resize 50 50 -background transparent a.svg PNG32:a.png