39

I'm generating HTML from a Docbook source while using SVG for images (converted from MathML). This works fine for some browsers that can interpret SVG, but fails for others.

What I would really like is to add a post-processing step that will convert SVG to PNG "in-place" (within the HTML).

So something like this:

<body>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
        <circle cx="50" cy="50" r="30" />
    </svg>
</body>

Would get seamlessly converted to this:

<body>
    <img src="img0001.png" />
</body>

And I would get a converted PNG alongside.

Is there something that will do this?

jjkparker
  • 27,597
  • 6
  • 28
  • 29
  • possible duplicate of [Convert SVG to image (JPEG, PNG, etc.) in the browser](http://stackoverflow.com/questions/3975499/convert-svg-to-image-jpeg-png-etc-in-the-browser) – Phrogz Nov 17 '11 at 02:03
  • 1
    https://mybyways.com/blog/convert-svg-to-png-using-your-browser – natenho Apr 17 '21 at 04:49

6 Answers6

52

Demo: http://phrogz.net/SVG/svg_to_png.xhtml

  1. Create an img and set its src to your SVG.
  2. Create an HTML5 canvas and use drawImage() to draw that image to your canvas.
  3. Use toDataURL() on the canvas to get a base64 encoded PNG.
  4. Create an img and set it's src to that data URL.
var mySVG    = document.querySelector('…'),      // Inline SVG element
    tgtImage = document.querySelector('…'),      // Where to draw the result
    can      = document.createElement('canvas'), // Not shown on page
    ctx      = can.getContext('2d'),
    loader   = new Image;                        // Not shown on page

loader.width  = can.width  = tgtImage.width;
loader.height = can.height = tgtImage.height;
loader.onload = function(){
  ctx.drawImage( loader, 0, 0, loader.width, loader.height );
  tgtImage.src = can.toDataURL();
};
var svgAsXML = (new XMLSerializer).serializeToString( mySVG );
loader.src = 'data:image/svg+xml,' + encodeURIComponent( svgAsXML );

However, this answer (and all client-side only solutions) require the browser to support SVG, which may make it useless for your specific needs.

Edit: This answer assumes that the SVG is available as a separate URL. Due to the problems described in this question I cannot get the above to work with an SVG document embedded in the same document performing the work.

Edit 2: The problems described in that other question have been overcome by improvements to Chrome and Firefox. There is still the limitation that the <svg> element must have width="…" height="…" attributes for Firefox to allow it to be drawn to a canvas. And Safari currently taints the entire canvas whenever you draw any SVG to it (regardless of source) but that should change soon.

Community
  • 1
  • 1
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • 3
    This is excellent. Mozilla wrote up a nice whitepaper on how to do this fully, here: https://developer.mozilla.org/en-US/docs/HTML/Canvas/Drawing_DOM_objects_into_a_canvas. Wouldn't have found it without your suggested course of action. – ZachB Sep 14 '12 at 01:10
  • I just posted some example code of how to do this with d3.js http://stackoverflow.com/a/23667012/439699 – ace May 14 '14 at 23:49
  • Note that as of this writing the current version of iOS (iOS 7) seems to be unable to fetch a valid data URL from the canvas, per several related questions on this site. – Phrogz May 21 '14 at 15:47
  • @confile is correct: Safari 5.1.7 (Windows) still taints the canvas when drawing SVG content to it, preventing toDataURL() from working. (I don't have access to OS X at the moment to report if 7.0.3 behaves similarly.) – Phrogz May 21 '14 at 18:35
  • @confile Note that WebKit patches for [SVG tainting Canvas bug](https://bugs.webkit.org/show_bug.cgi?id=119492) landed in Main in [August 2013](http://trac.webkit.org/changeset/153876). So maybe they're still percolating to release branches. – Phrogz May 21 '14 at 18:39
  • in case you are dealing with angular and svg XMLSerializer might fail. I found that this still works: var svgAsXML = $('#preview_svg')[0].outerHTML; $scope.data.current_shot = 'data:image/svg+xml,' + encodeURIComponent( svgAsXML ); – RedRoosterMobile Nov 14 '14 at 21:08
  • 2
    As of November 29th, 2015, Safari STILL AS YET will not permit conversion of SVG to PNG via `toDataURL()`. Effectively rendering any solution I've found so far on SO only partial :( Come ON Apple. – cbmtrx Nov 29 '15 at 19:10
  • @user3229542 shows how to apply css inline so that the img comes out the same. css can be applied to the svg in a separate stylesheet and drawing the img to the canvas causes the styles to be lost in the process – Gerald Mar 19 '16 at 14:21
  • @amergin Ask this as its own question, not as a comment. Add I think you see, it has nothing to do with the Canvas-to-PNG conversion, and likely not even related to svg-to-canvas. – Phrogz Dec 16 '16 at 13:49
  • SVG sources loaded as images cannot reference external resources such as scripts, images (including other SVG files), fonts, meaning that loading an SVG file as source for an IMG element gets you reduced functionality for that SVG file. Since external resource loading is one of the backbones for reusing assets, I'd say this is a big impediment. – Armen Michaeli Nov 04 '17 at 17:52
5

I ran into this problem over the past weekend, and ended up writing a simple library to do more or less what Phrogz describes. It also hard codes the target SVG's style in order to avoid rendering issues. Hopefully the next person looking for a way to do this can simply use my code and move on to the more interesting challenges!

P.S. I only tested this in Chrome.

// Takes an SVG element as target
function svg_to_png_data(target) {
  var ctx, mycanvas, svg_data, img, child;

  // Flatten CSS styles into the SVG
  for (i = 0; i < target.childNodes.length; i++) {
    child = target.childNodes[i];
    var cssStyle = window.getComputedStyle(child);
    if(cssStyle){
       child.style.cssText = cssStyle.cssText;
    }
  }

  // Construct an SVG image
  svg_data = '<svg xmlns="http://www.w3.org/2000/svg" width="' + target.offsetWidth +
             '" height="' + target.offsetHeight + '">' + target.innerHTML + '</svg>';
  img = new Image();
  img.src = "data:image/svg+xml," + encodeURIComponent(svg_data);

  // Draw the SVG image to a canvas
  mycanvas = document.createElement('canvas');
  mycanvas.width = target.offsetWidth;
  mycanvas.height = target.offsetHeight;
  ctx = mycanvas.getContext("2d");
  ctx.drawImage(img, 0, 0);

  // Return the canvas's data
  return mycanvas.toDataURL("image/png");
}

// Takes an SVG element as target
function svg_to_png_replace(target) {
  var data, img;
  data = svg_to_png_data(target);
  img = new Image();
  img.src = data;
  target.parentNode.replaceChild(img, target);
}
user2428118
  • 7,935
  • 4
  • 45
  • 72
user3229542
  • 51
  • 1
  • 1
4

I took @Phrogz code above and made a working snippet. Not sure mySVG.clientWidth works in FF though. It also available here - https://jsfiddle.net/LLjLpo05/

var mySVG = document.querySelector('#svblock'),        // Inline SVG element
    tgtImage = document.querySelector('#diagram_png'), // Where to draw the result
    can = document.createElement('canvas'), // Not shown on page
    ctx = can.getContext('2d'),
    loader = new Image; // Not shown on page

//loader.width  = can.width  = tgtImage.width = mySVG.getBBox().width;
//loader.height = can.height = tgtImage.height = mySVG.getBBox().height;

loader.width = can.width = tgtImage.width = mySVG.clientWidth;
loader.height = can.height = tgtImage.height = mySVG.clientHeight;

loader.onload = function() {
  ctx.drawImage(loader, 0, 0, loader.width, loader.height);
  tgtImage.src = can.toDataURL();
};
var svgAsXML = (new XMLSerializer).serializeToString(mySVG);
loader.src = 'data:image/svg+xml,' + encodeURIComponent(svgAsXML);
<div id="diagram_image">
  <svg id="svblock" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 120">
    <defs id="defs_block">
      <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
        <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" />
      </filter>
    </defs>
    <title>blockdiag</title>
    <desc/>
    <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="46" />
    <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="259" y="46" />
    <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="451" y="46" />
    <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="40" />
    <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="128" y="66">discovery</text>
    <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="256" y="40" />
    <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="320" y="66">execution</text>
    <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="448" y="40" />
    <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="512" y="66">reporting</text>
    <path d="M 192 60 L 248 60" fill="none" stroke="rgb(0,0,0)" />
    <polygon fill="rgb(0,0,0)" points="255,60 248,56 248,64 255,60" stroke="rgb(0,0,0)" />
    <path d="M 384 60 L 440 60" fill="none" stroke="rgb(0,0,0)" />
    <polygon fill="rgb(0,0,0)" points="447,60 440,56 440,64 447,60" stroke="rgb(0,0,0)" />
  </svg>
</div>

<img id="diagram_png" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4
//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" />

UPDATE: Refactored a bit - https://jsfiddle.net/e4r8sk18/1/

UPDATE2: Refactored into converter class - https://jsfiddle.net/07a93Lt6/5/

anatoly techtonik
  • 19,847
  • 9
  • 124
  • 140
3

FOP and Batik

http://xmlgraphics.apache.org/fop/

http://xmlgraphics.apache.org/batik/

FOP, from Apache, incorporates Batik, also from Apache. Batik has an SVG rendering tool which will generate your PNGs. FOP also is a document generating tool.

Josh Pearce
  • 3,399
  • 1
  • 23
  • 24
  • best you go with the latest fop, which also has the latest batik. With batik standalone i had trouble fixing dependencies to the lastest fop. Only got it to work with 0.9sth and pdf rendering was shitty(no patterns). – RedRoosterMobile Nov 14 '14 at 21:12
2

this is rather old, but I found a simpler snippet that does the job correctly in modern browsers: https://gist.github.com/Caged/4649511

Ramiro Araujo
  • 465
  • 3
  • 11
1

if you want to do it purely on the client-side, you would need two steps:

  1. convert SVG to Canvas (http://code.google.com/p/canvas-svg/ or some other tools)
  2. convert Canvas to PNG (http://www.nihilogic.dk/labs/canvas2image/ or some other tools)

this obviously will work only in HTML5-capable browsers.

wildcard
  • 7,353
  • 3
  • 27
  • 25