4

I have an external SVG file which contains some embedded image tags in pattern. Whenever I convert this SVG into PNG using toDataURL(), the generated PNG images does not contain the image I have applied as pattern to some SVG paths. Is there any way to solve this problem?

Adam Azad
  • 11,171
  • 5
  • 29
  • 70
nilesh_ramteke
  • 135
  • 3
  • 8
  • I assume you are using this [`HTMLCanvasElement.toDataURL()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL), but in order to help you debug your problem, you should paste the code you are using for the canvas and the conversion. It will be difficult to help you otherwise... – Armfoot Dec 02 '15 at 12:41

1 Answers1

10

Yes there are : append the svg into your document and encode all the included images to dataURIs.

I am writing a script that does this and also some other stuff like including external style-sheets and some other fix of where toDataURL will fail (e.g external elements referenced through xlink:href attribute or <funciri>).

Here is the function I wrote for parsing the images content :

function parseImages(){
    var xlinkNS = "http://www.w3.org/1999/xlink";
    var total, encoded;
    // convert an external bitmap image to a dataURL
    var toDataURL = function (image) {

        var img = new Image();
        // CORS workaround, this won't work in IE<11
        // If you are sure you don't need it, remove the next line and the double onerror handler
        // First try with crossorigin set, it should fire an error if not needed
        img.crossOrigin = 'Anonymous';

        img.onload = function () {
            // we should now be able to draw it without tainting the canvas
            var canvas = document.createElement('canvas');
            canvas.width = this.width;
            canvas.height = this.height;
            // draw the loaded image
            canvas.getContext('2d').drawImage(this, 0, 0);
            // set our <image>'s href attribute to the dataURL of our canvas
            image.setAttributeNS(xlinkNS, 'href', canvas.toDataURL());
            // that was the last one
            if (++encoded === total) exportDoc();
        };

        // No CORS set in the response      
        img.onerror = function () {
            // save the src
            var oldSrc = this.src;
            // there is an other problem
            this.onerror = function () {
                console.warn('failed to load an image at : ', this.src);
                if (--total === encoded && encoded > 0) exportDoc();
            };
            // remove the crossorigin attribute
            this.removeAttribute('crossorigin');
            // retry
            this.src = '';
            this.src = oldSrc;
        };
        // load our external image into our img
        img.src = image.getAttributeNS(xlinkNS, 'href');
    };

    // get an external svg doc to data String
    var parseFromUrl = function(url, element){
        var xhr = new XMLHttpRequest();
        xhr.onload = function(){
            if(this.status === 200){
                var response = this.responseText || this.response;
                var dataUrl = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(response);
                element.setAttributeNS(xlinkNS, 'href', dataUrl);
                if(++encoded === total) exportDoc();
                }
            // request failed with xhr, try as an <img>
            else{
                toDataURL(element);
                }
            };
        xhr.onerror = function(){toDataURL(element);};
        xhr.open('GET', url);
        xhr.send();
        };

    var images = svg.querySelectorAll('image');
    total = images.length;
    encoded = 0;

    // loop through all our <images> elements
    for (var i = 0; i < images.length; i++) {
        var href = images[i].getAttributeNS(xlinkNS, 'href');
        // check if the image is external
        if (href.indexOf('data:image') < 0){
            // if it points to another svg element
            if(href.indexOf('.svg') > 0){
                parseFromUrl(href, images[i]);
                }
            else // a pixel image
                toDataURL(images[i]);
            }
        // else increment our counter
        else if (++encoded === total) exportDoc();
    }
    // if there were no <image> element
    if (total === 0) exportDoc();
}

Here the svgDoc is called svg,
and the exportDoc() function could just be written as :

var exportDoc = function() {
    // check if our svgNode has width and height properties set to absolute values
    // otherwise, canvas won't be able to draw it
    var bbox = svg.getBoundingClientRect();

    if (svg.width.baseVal.unitType !== 1) svg.setAttribute('width', bbox.width);
    if (svg.height.baseVal.unitType !== 1) svg.setAttribute('height', bbox.height);

    // serialize our node
    var svgData = (new XMLSerializer()).serializeToString(svg);
    // remember to encode special chars
    var svgURL = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(svgData);

    var svgImg = new Image();

    svgImg.onload = function () {
        var canvas =  document.createElement('canvas');
        // IE11 doesn't set a width on svg images...
        canvas.width = this.width || bbox.width;
        canvas.height = this.height || bbox.height;

        canvas.getContext('2d').drawImage(svgImg, 0, 0, canvas.width, canvas.height);
        doSomethingWith(canvas)
    };

    svgImg.src = svgURL;
};

But once again, you will have to append your svg into the document first (either through xhr or into an <iframe> or an <object> element, and you will have to be sure all your resources are CORS compliant (or from same domain) in order to get these rendered.

var svg = document.querySelector('svg');
var doSomethingWith = function(canvas) {
  document.body.appendChild(canvas)
};

function parseImages() {
  var xlinkNS = "http://www.w3.org/1999/xlink";
  var total, encoded;
  // convert an external bitmap image to a dataURL
  var toDataURL = function(image) {

    var img = new Image();
    // CORS workaround, this won't work in IE<11
    // If you are sure you don't need it, remove the next line and the double onerror handler
    // First try with crossorigin set, it should fire an error if not needed
    img.crossOrigin = 'anonymous';

    img.onload = function() {
      // we should now be able to draw it without tainting the canvas
      var canvas = document.createElement('canvas');
      canvas.width = this.width;
      canvas.height = this.height;
      // draw the loaded image
      canvas.getContext('2d').drawImage(this, 0, 0);
      // set our <image>'s href attribute to the dataURL of our canvas
      image.setAttributeNS(xlinkNS, 'href', canvas.toDataURL());
      // that was the last one
      if (++encoded === total) exportDoc();
    };

    // No CORS set in the response  
    img.onerror = function() {
      // save the src
      var oldSrc = this.src;
      // there is an other problem
      this.onerror = function() {
        console.warn('failed to load an image at : ', this.src);
        if (--total === encoded && encoded > 0) exportDoc();
      };
      // remove the crossorigin attribute
      this.removeAttribute('crossorigin');
      // retry
      this.src = '';
      this.src = oldSrc;
    };
    // load our external image into our img
    var href = image.getAttributeNS(xlinkNS, 'href');
    // really weird bug that appeared since this answer was first posted
    // we need to force a no-cached request for the crossOrigin be applied
    img.src = href + (href.indexOf('?') > -1 ? + '&1': '?1');
  };

  // get an external svg doc to data String
  var parseFromUrl = function(url, element) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      if (this.status === 200) {
        var response = this.responseText || this.response;
        var dataUrl = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(response);
        element.setAttributeNS(xlinkNS, 'href', dataUrl);
        if (++encoded === total) exportDoc();
      }
      // request failed with xhr, try as an <img>
      else {
        toDataURL(element);
      }
    };
    xhr.onerror = function() {
      toDataURL(element);
    };
    xhr.open('GET', url);
    xhr.send();
  };

  var images = svg.querySelectorAll('image');
  total = images.length;
  encoded = 0;

  // loop through all our <images> elements
  for (var i = 0; i < images.length; i++) {
    var href = images[i].getAttributeNS(xlinkNS, 'href');
    // check if the image is external
    if (href.indexOf('data:image') < 0) {
      // if it points to another svg element
      if (href.indexOf('.svg') > 0) {
        parseFromUrl(href, images[i]);
      } else // a pixel image
        toDataURL(images[i]);
    }
    // else increment our counter
    else if (++encoded === total) exportDoc();
  }
  // if there were no <image> element
  if (total === 0) exportDoc();
}

var exportDoc = function() {
  // check if our svgNode has width and height properties set to absolute values
  // otherwise, canvas won't be able to draw it
  var bbox = svg.getBoundingClientRect();

  if (svg.width.baseVal.unitType !== 1) svg.setAttribute('width', bbox.width);
  if (svg.height.baseVal.unitType !== 1) svg.setAttribute('height', bbox.height);

  // serialize our node
  var svgData = (new XMLSerializer()).serializeToString(svg);
  // remember to encode special chars
  var svgURL = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(svgData);

  var svgImg = new Image();

  svgImg.onload = function() {
    var canvas = document.createElement('canvas');
    // IE11 doesn't set a width on svg images...
    canvas.width = this.width || bbox.width;
    canvas.height = this.height || bbox.height;

    canvas.getContext('2d').drawImage(svgImg, 0, 0, canvas.width, canvas.height);
    doSomethingWith(canvas)
  };

  svgImg.src = svgURL;
};
window.onload = parseImages;
canvas {
  border: 1px solid green !important;
}
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg" version="1.1">
  <defs>

    <pattern id="Pattern" x="0" y="0" width=".25" height=".25">
      <image xlink:href="https://dl.dropboxusercontent.com/s/1alt1303g9zpemd/UFBxY.png" width="100" height="100"/>
    </pattern>

  </defs>

  <rect fill="url(#Pattern)" x="0" y="0" width="200" height="200"/>
</svg>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Excellent solution. +1 Though I would say it may be best to include {} for single statements after `if`, `else` , etc as that is one of the top overlooked sources of bugs for newbies. – Blindman67 Dec 02 '15 at 14:16
  • @Blindman67 you are right, bad habit of mine and quickly copy-pasted from the whole script which still needs a rough cleanup... – Kaiido Dec 02 '15 at 14:48
  • @Kaiido I'm having a similar problem [link](http://stackoverflow.com/questions/40721670/class-update-not-load-background-into-svg) After alter the background-image of my content through different classes it doesn't show the image, just a blank space :) – Rafael de Castro Nov 21 '16 at 13:56
  • @Kaiido, your code is great, but I think it would benefit from some async methods, right? – Vali Munteanu Aug 30 '17 at 12:59