如何使用Imagick/ImageMagick调整SVG大小

13

我正在将SVG文件的字符串表示发送到服务器,并使用Imagick以以下方式将其转换为JPEG:

$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();

然而我希望在栅格化之前调整SVG的大小,以便生成的图像比SVG文件中指定的尺寸更大。

我将我的代码修改为以下内容:

$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();

这段代码应该能够确定分辨率并相应地调整SVG大小。如果SVG画布和其元素具有基于百分比的宽度,则可以完美运行,但是对于以'px'定义的元素似乎无法正常工作。而这恰恰是必需的。

将发送到服务器的典型SVG字符串如下所示:

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

正如您所看到的,构成此SVG的元素具有像素定义的宽度和高度(对于此应用程序不幸的是使用百分比不是一个选项)。

有什么解决方法吗?或者有没有其他将SVG转换为PNG并在给定大小下呈现而不损失质量的方法。

谢谢。

编辑:虽然我从未真正找到完美的解决方案。相反,我最终将SVG数据作为JSON发送,通过服务器端循环遍历并将像素缩放到预期高度。

然后,在经过多次尝试之后,我意识到imagemagick存在问题,无法处理标准SVG变换/旋转命令,从而使任何操作的元素失调。我最终切换到使用“inkscape”将生成的SVG渲染为光栅图像。一切都很好。我仍在深入研究一个潜在的公式解决方案,以抵消imagemagick所做出的差异。如果我有任何成功,我会再次更新这个问题。


从您的另一个问题中(https://dev59.com/5WPVa4cB1Zd3GeqP4lW9),我怀疑您现在已经转向使用Inkscape(可以使用命令行选项缩放图像)。如果是这种情况,您能否在下面的问题中添加一条评论? :) - halfer
确实...我会相应地编辑我的问题。 :-) - gordyr
很想看到您的Inkscape解决方案的更多细节! - supertrue
对于仍然遇到问题的人,这个链接可能会有用。我手动替换了<svg>标签的宽度和高度,并添加了一个transform=scale(x)属性... - patrics
使用库contao/imagine-svg,您可以在将SVG图像发送到ImageMagick之前调整其大小。 - ausi
6个回答

16

为了解决php_imagick的错误,您可以缩放SVG的宽度和高度:

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;
}

然后您可以轻松创建漂亮的透明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();
}

注意: 我一直在寻找解决方案,如何将SVG从其初始小尺寸重新缩放。但似乎imagick :: setResolution有问题。但是,ImageMagick库本身正在工作,所以您可以使用exec('convert ...')(由托管提供商出于安全原因可能已被禁用)。

因此,要从较小的SVG创建50x50缩略图,您需要执行以下操作:

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

宽度和高度必须以“px”为单位。 - psycho brm
另一个问题是图像缩放效果不佳,会出现模糊。解决方法是使用更大的宽度和高度(如果您使用没有像素的SVG viewBox="0 0 width height"和带有像素的SVG宽度高度,则不会影响相对大小)。 - psycho brm

4

我正在寻找一个解决方案,然后在阅读这篇文章后发现了这个方法,它非常好用:

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

感谢PHP手册页面中那条评论的作者。

1
这绝对是将SVG放大后转换为PNG的最简单方法。比起搞乱DOMDocument并尝试读取viewbox要容易得多。我想让生成的PNG宽度为1600像素但保持纵横比,但是我处理的SVG具有许多不同的viewbox属性。这个方法非常好用。 - Mat
这是最好、最简单的方法,应该被接受为答案。分辨率必须乘以2.54。 - BogisW

1

完成此任务不需要使用imagick。例如,您想要调整大小的svg(w:60px,h:70px)=>(w:36px,h:36px)以获得按钮图标。

$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());
}

请注意设置translate(x,y),因为这可能会导致您的svg内容被设置在框之外,除了背景什么都看不到。

如果您的初始translate设置为(0,0),则我的上面的脚本才能正常工作。您可以使用此方法。

$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());

将翻译设置为(0,0),并根据新设置调整路径数据,因为路径数据取决于翻译,反之亦然。
我使用这两个脚本将我的SVG图标调整大小到所需的尺寸,并将它们转换为PNG格式而不会失去质量。
我希望我的意思很清楚。

0

这里有一个示例,演示如何对已经存储在字符串中的图像进行调整大小、添加边框并打印出来。我通常用它来展示转售商的标志。

  // 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;

1
谢谢Chintu,不幸的是这对我没有帮助,因为我希望在将SVG转换为光栅图之前调整其大小以确保完全质量。当然,我可以简单地调整最终的JPEG大小而不会有任何问题,但是结果图像会失去质量。首先调整SVG的大小(因为SVG是矢量格式),然后最终再将其转换为光栅图,可以在放大时保持质量。如果我在最初的帖子中表达得不够清楚,我很抱歉。还是谢谢你的尝试。 :-) - gordyr

0
为了扩展SVG文件中发现的宽度、高度、X、Y属性。
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

这个解决方案适用于带有 viewBox 属性的 .svg 文件

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;
}

感谢您提供这段代码片段,它可能会提供一些有限的、即时的帮助。适当的解释将极大地提高其长期价值,因为它会展示出为什么这是一个好的问题解决方案,并使其对未来的读者在解决类似问题时更有用。请[编辑]您的答案并添加一些解释,包括您所做的假设。 - helvete

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接