13

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.

gordyr
  • 6,078
  • 14
  • 65
  • 123
  • From [your other question](http://stackoverflow.com/questions/10400897/how-to-convert-an-svg-string-into-a-jpg-with-inkscape) I suspect you've now switched to Inkscape (which can scale an image using options on the command line). If that's the case, would you add a comment below the question? `:)`. – halfer May 01 '12 at 19:44
  • Indeed... i'll edit my question accordingly. :-) – gordyr May 01 '12 at 20:18
  • Would love to see more details on your Inkscape solution! – supertrue Sep 02 '12 at 07:23
  • For anyone still having problems [this](http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute) might work. I manually replaced the width and height of – patrics Apr 06 '13 at 20:26
  • With the library [contao/imagine-svg](https://github.com/contao/imagine-svg) you can resize the SVG images before sending them to ImageMagick. – ausi Feb 27 '18 at 07:32

6 Answers6

16

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
psycho brm
  • 7,494
  • 1
  • 43
  • 42
  • Width and height must be in "px". – psycho brm Nov 21 '12 at 00:25
  • Another bug is that images dont scale well, they are blurred. The workaround is to use bigger width and height (it wont affect relative size if you use svg viewBox="0 0 width height" without pixels, and svg width height with pixels). – psycho brm Nov 21 '12 at 12:33
4

I was looking for a solution, and i found this just after reading this post, and work like a charm:

$im = new Imagick();
$im->readImage("/path/to/image.svg");
$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->readImage("/path/to/image.svg");
// Now you can do anything with the image, such as convert to a raster image and output it to the browser:
$im->setImageFormat("png");
header("Content-Type: image/png");
echo $im;

Credits go to the author of that comment in php manuals page.

Strae
  • 18,807
  • 29
  • 92
  • 131
  • 1
    This is by far the easiest way to upscale an SVG before converting to PNG. Much easier than messing with DOMDocument and trying to read the viewbox. I wanted the resulting PNG to have a width of 1600px but maintain aspect ratio, but was dealing with SVGs with lots of different viewbox attributes. This worked like a charm. – Mat May 19 '21 at 07:48
  • This is the best and easiest way, and should be the accepted answer. Resolutions have to be multiplied by 2.54. – BogisW Mar 04 '23 at 22:32
1

You don't need imagick to complete this task. For example you wont to resize your svg (w: 60px, h: 70px) => (w: 36px, h: 36px) to get an icon for a button.

$svg = file_get_contents($your_svg_file);

// I prefer to use DOM, because it's safer and easier as to use preg_match
$svg_dom = new DOMDocument();

libxml_use_internal_errors(true);
$svg_dom->loadXML($svg);
libxml_use_internal_errors(false);

//get width and height values from your svg
$tmp_obj = $svg_dom->getElementsByTagName('svg')->item(0);
$svg_width = floatval($tmp_obj->getAttribute('width'));
$svg_height = floatval($tmp_obj->getAttribute('height'));

// set width and height of your svg to preferred dimensions
$tmp_obj->setAttribute('width', 36);
$tmp_obj->setAttribute('height', 36);

// check if width and height of your svg is smaller than the width and 
// height you set above => no down scaling is needed
if ($svg_width < 36 && $svg_height < 36) {
    //center your svg content in new box
    $x = abs($svg_width - 36) / 2;
    $y = abs($svg_height - 36) / 2;
    $tmp_obj->getElementsByTagName('g')->item(0)->setAttribute('transform', "translate($x,$y)");
} else {
    // scale down your svg content and center it in new box
    $scale = 1;

    // set padding to 0 if no gaps are desired
    $padding = 2;

    // get scale factor
    if ($svg_width > $svg_height) {
        $scale = (36 - $padding) / $svg_width;
    } else {
        $scale = (36 - $padding) / $svg_height;
    }

    $x = abs(($scale * $svg_width) - 36) / 2;
    $y = abs(($scale * $svg_height) - 36) / 2;
    $tmp_obj->getElementsByTagName('g')->item(0)->setAttribute('transform', "translate($x,$y) scale($scale,$scale)");

    file_put_contents('your_new_svg.svg', $svg_dom->saveXML());
}

Be careful by setting translate(x,y), because it may happen that your svg content can be set outside of the box and you will see nothing except the background.

My script above works proper only if your initial translate is set to (0,0). You can use this

$svg_path = $svg_dom->getElementsByTagName('path')->item(0);
$svg_g = $svg_dom->getElementsByTagName('g')->item(0);
$transform = $svg_g->getAttribute('transform');

// get x and y of translate
$transform = substr($transform, strlen('translate('));
$transform = substr($transform, 0, strlen($transform)-1);
$transform_data = explode(',', $transform);

// get path data
$d = $svg_path->getAttribute('d');
$d_data = explode(' ', $d);
$tmp = explode(',', $d_data[1]);
$d_data[1] = ($tmp[0] + $transform_data[0]).','.($tmp[1]+$transform_data[1]);
$svg_path->setAttribute('d', implode(' ', $d_data));
$svg_g->setAttribute('transform','translate(0,0)');
file_put_contents('your_new_svg.svg',$svg_dom->saveXML());

to set translate to (0,0) and adapt path data to new settings because path data depends on translate and vice versa.

I use this two scripts to generate png icons by resizing my svg icons to dimension I need and converting them to png without loss of quality.

I hope it's clear what I mean.

San Droid
  • 332
  • 1
  • 7
0

Here is an example on how to take an image that is already in a string (say, from a database), and resize it, add a border, and print it out. I use this for showing reseller logos

  // Decode image from base64
  $image=base64_decode($imagedata);

  // Create Imagick object
  $im = new Imagick();

  // Convert image into Imagick
  $im->readimageblob($image);

  // Create thumbnail max of 200x82
  $im->thumbnailImage(200,82,true);

  // Add a subtle border
  $color=new ImagickPixel();
  $color->setColor("rgb(220,220,220)");
  $im->borderImage($color,1,1);

  // Output the image
  $output = $im->getimageblob();
  $outputtype = $im->getFormat();

  header("Content-type: $outputtype");
  echo $output;
Chintan
  • 1,204
  • 1
  • 8
  • 22
  • 1
    Thanks chintu, unfortunately this is of no help to me as I wish to resize the SVG prior to it being rasterized to ensure full quality. I can of course simply resize the final jpeg without any problem, but the resulting image would lose quality. Resizing the SVG first (since SVG is a vector format) and 'then' finally rastering the image would keep the quality during the upsize. I'm sorry if I wasn't clear enough in my initial post. Thanks for trying though. :-) – gordyr Apr 30 '12 at 12:31
0

In order to scale width, height, x, y attributes found in a SVG file.

function scale($svg, $k) {
    $rxvbox = '/viewBox="(\d+) (\d+) (\d+) (\d+)"/si';
    $rxw = '/width="(\d+)"/si';
    $rxh = '/height="(\d+)"/si';
    $rxx =  '/x="(\d+)"/si';
    $rxy =  '/y="(\d+)"/si';
    $svg = preg_replace_callback($rxvbox, function($m) use ($k) {return 'viewBox="'.$m[1]*$k.' '.$m[2]*$k.' '.$m[3]*$k.' '.$m[4]*$k.'"';}, $svg);
    $svg = preg_replace_callback($rxw, function($m) use ($k) {return 'width="'.$m[1]*$k.'"';}, $svg);
    $svg = preg_replace_callback($rxh, function($m) use ($k) {return 'height="'.$m[1]*$k.'"';}, $svg);
    $svg = preg_replace_callback($rxx, function($m) use ($k) {return 'x="'.$m[1]*$k.'"';}, $svg);
    $svg = preg_replace_callback($rxy, function($m) use ($k) {return 'y="'.$m[1]*$k.'"';}, $svg);
    return $svg;
}
0

This solution work for .svg with attribute viewBox

function svgToPng($url, $savepath, $minWidth, $minHeight): bool {
    $data = file_get_contents($url);
    $dom = new DOMDocument();
    $dom->loadXML($data);
    $svg = $dom->getElementsByTagName('svg')->item(0);

    if (!$svg) return false;
    if (!$svg->hasAttribute('viewBox')) return false;

    $attrViewBox = $svg->getAttribute('viewBox');
    $pattern = '/(\d*(?:\.\d+)?) (\d*(?:\.\d+)?) (\d*(?:\.\d+)?) (\d*(?:\.\d+)?)/i';
    preg_match($pattern, $attrViewBox, $viewBox);

    $width = floatval($viewBox[3]);
    $height = floatval($viewBox[4]);
    if (!$width || !$height) return false;
    $scale = 1;
    if ($width < $minWidth) $scale = $minWidth / $width;
    if ($height < $minHeight) $scale = max($scale, ($minHeight / $height));
    $width *= $scale;
    $height *= $scale;
    $width = round($width);
    $height = round($height);
    
    $svg->setAttribute('width', $width . 'px');
    $svg->setAttribute('height', $height . 'px');
    $svg->setAttribute('x', $viewBox[1]);
    $svg->setAttribute('y', $viewBox[2]);
    
    $im = new Imagick();
    $im->setBackgroundColor(new ImagickPixel('transparent'));
    $im->readImageBlob($dom->saveXML());
    $im->setImageFormat('png32');
    $im->resizeImage($width, $height, imagick::FILTER_LANCZOS, 1);
    $im->writeImage($savepath);
    $im->clear();
    $im->destroy();
    
    return true;
}
  • Thank you for this code snippet, which might provide some limited, immediate help. A [proper explanation](https://meta.stackexchange.com/q/114762/349538) would greatly improve its long-term value by showing why this is a good solution to the problem and would make it more useful to future readers with other, similar questions. Please [edit] your answer to add some explanation, including the assumptions you’ve made. – helvete Jun 15 '22 at 11:08