ImageMagick Workaround for SVG Support

I have installed the PECL package IMagick in my computer. Its current version 7:6.6.2.6 -1 does a great job converting SVG files into raster images. In my hosting account the Imagemagick package is not the most recent: I have an SVG file that contained elemants repeated using the Inkscape’s spray can, but when I convert it using a test program in my host it only print one of the repeated stars.

How can I resolve this?

Well, as you probably know, SVG is an XML format, so I can read it using a text editor. Doing this I found that there’s an element named ‘use’ with an attribute named ‘href’. “href” we know from HTML, and the element’s named “use” makes sense when using an existing element. So I commented out all the “use” elements, and viewing the SVG file as an image I saw the star at the exact location as in the  erroneous raster image, but with other elements not printed there. Probably the SAX engine stopped after the first unknown element. Commented out all elements from the first “use” elements until the end of file, and BINGO! This reproduced the problem. Well, the “use” elements should be replaced by the original ones with changes to their attributes. The following code does it:

/**
* I found that the "use" tags are not supported, so I'm replacing them
* by something else.
*
* The function returns the content of the file, the returned value can be the 1st argument of 
* Imagick::readImageBlob
*/
function svg_workaround($filename){
  $mimeType = mime_content_type($filename);

  if ($mimeType != 'text/xml' && mimeType!='image/svg+xml'){
  // At home the mime type is "Image/svg+xml" in the hosting account "text/xml"
    return file_get_contents($filename);
  }

  $contents = file_get_contents($filename);
  $docRoot = new DOMDocument(); // For random access and fast location of referenced objects
                                                     // you would use 'DOM'.

  if (!$docRoot->loadXML($contents))
    return FALSE;

  $xlinkNs = getXLinkNs($docRoot); // For the "href" attribute.

  $svgElems = $docRoot->getElementsByTagName('svg');
  if (!$svgElems->length)
    return $contents;

  $nodeList = $docRoot->getElementsByTagName('use');

  // Because the node lists changes when elements are replaces, we keep the elements in an array.
  $nodes = array();
  for ($i=0; $ilength; $i++)
    $nodes[]=$nodeList->item($i);

  foreach ($nodes as $node)
    if (!replace_use_tag($node, $docRoot, $xlinkNs))
      return false;

  return $docRoot->saveXML();
}

// The function that replaces the "use" elements
function replace_use_tag($node, $doc, $xlinkNs){
  $href=$node->getAttribute($xlinkNs . ':href');
  if (!$href)
    $href=$node->getAttribute('href');

  if (!$href)
    return FALSE;

  $id=substr($href, 1); // Removing the '#' from the href.
  if (!$id)
    return FALSE;
  // We use 'xpath' to locate a node with a given id because, unlike HTML, the use of getElementById
  // in XML DOM is not trivial.
  $xpath=new DOMXPath($doc);
  $elems=$xpath->query('//*[@id=\'' . $id . '\']');
  $orig=$elems->item(0);

  if (!$orig)
    return FALSE;

  $newNode=$orig->cloneNode(TRUE); // TRUE for deep cloning; When cloning a composite such as "g",
                                                             // its child elements should be copied as well.

  // Copy attributes from the 'use' node to the new node, not including the 'href'
  $attributes=$node->attributes;

  $length=$attributes->length;

  for ($i=0; $iitem($i); $i++){
    $attrName=$item->name;
    $attrValue=$item->value;
    if ($attrName=='href' || $attrName==$xlinkNs . ':href')
      continue;
    $newNode->setAttribute($attrName, $attrValue);
  }

  $parent=$node->parentNode;
  $parent->replaceChild($newNode, $node);
  return TRUE;
}

// Get the namespace for XLink.
function getXLinkNs($doc){
  return $doc->lookupPrefix('http://www.w3.org/1999/xlink');
}

Advertisements