34

I'm working on a client-side/javascript function to save or convert an existing D3-SVG graph into a file. I've searched a lot and found some recommendations, mainly using canvas.toDataURL().

I have no <canvas> in my page, and instead using:d3.select("body").append("svg").... I've also tried to append the SVG to the <canvas> but nothing happens.

Could you please help me to resolve this exception:

Uncaught TypeError: Object #<SVGSVGElement> has no method 'toDataURL' 

Thank you

Lars Kotthoff
  • 107,425
  • 16
  • 204
  • 204
Moein
  • 739
  • 2
  • 9
  • 17
  • For in-browser conversion to png, check out http://stackoverflow.com/questions/3975499/convert-svg-to-image-jpeg-png-etc-in-the-browser – widged Apr 17 '13 at 02:14
  • If it doesn't need to be at runtime, tools like casperjs let you take a screenshot of any element in the page http://casperjs.org/api.html#casper.captureSelector – widged Apr 17 '13 at 02:18
  • For pdf export, see http://stackoverflow.com/questions/3360641/how-to-insert-a-svg-file-in-a-pdf-document. – widged Apr 17 '13 at 02:31

5 Answers5

20

To display your svg within a canvas, you first have to convert it using a parser/renderer utility such as http://code.google.com/p/canvg/

(code adapted from: Convert SVG to image (JPEG, PNG, etc.) in the browser, not tested)

// the canvg call that takes the svg xml and converts it to a canvas
canvg('canvas', $("#my-svg").html());

// the canvas calls to output a png
var canvas = document.getElementById("canvas");
var img = canvas.toDataURL("image/png");
Community
  • 1
  • 1
widged
  • 2,749
  • 21
  • 25
  • 1
    The first line will not work because 1. canvg awaits for the URL, not the actual XML data and 2. $('svg').html() would not work either - you'll need innerSVG JS library to get access to inner SVG XML. – WASD42 Aug 13 '13 at 11:10
  • `canvg(document.getElementById('drawingArea'), '...')` is given as usage https://github.com/gabelerner/canvg. Changed `$('svg').html() ` to `$("#my-svg").html()`. – widged Sep 08 '15 at 00:51
  • 1
    Once you have img, how do you trigger a download of it? – rjurney Jul 05 '16 at 14:56
  • $("#my-svg").html() won't return the opening and closing `` tags; to get this to work, I had to call `canvg('canvas', "" + $("my-svg").html() + "")`. – Randoms May 01 '17 at 18:30
  • this does not seem to work in the browser when I use the npm build. – vijayst Jun 23 '18 at 12:59
6

As pointed out by @Premasagar in this comment on this question Convert SVG to image (JPEG, PNG, etc.) in the browser

If the borwser supports both SVG and canvas you can use this technique https://svgopen.org/2010/papers/62-From_SVG_to_Canvas_and_Back/index.html

function importSVG(sourceSVG, targetCanvas) {
    // https://developer.mozilla.org/en/XMLSerializer
    svg_xml = (new XMLSerializer()).serializeToString(sourceSVG);
    var ctx = targetCanvas.getContext('2d');

    // this is just a JavaScript (HTML) image
    var img = new Image();
    // http://en.wikipedia.org/wiki/SVG#Native_support
    // https://developer.mozilla.org/en/DOM/window.btoa
    img.src = "data:image/svg+xml;base64," + btoa(svg_xml);

    img.onload = function() {
        // after this, Canvas’ origin-clean is DIRTY
        ctx.drawImage(img, 0, 0);
    }
}
Community
  • 1
  • 1
Pavel Nikolov
  • 9,401
  • 5
  • 43
  • 55
6

Just a heads up that I turned this concept into a small JavaScript library: https://github.com/krunkosaurus/simg

It simply converts any SVG to an image to swap out or trigger a download. Idea taken from here: http://techslides.com/save-svg-as-an-image/

Mauvis Ledford
  • 40,827
  • 17
  • 81
  • 86
  • im trying you js library but is not working. The code from techslides does work, although not in Firefox 33.0 One thing i notice is that your code references the a.href to the img src, instead of the canvas data URL. Any ideas? – Sebastian Oct 28 '14 at 17:04
  • Also, im using a lineChart from DC.js and the png file looks like css styles were not applied, even though i pasted all dc.css in the html. – Sebastian Oct 28 '14 at 17:12
  • @sebastian: Just a heads up that after receiving a few pull request I went ahead and updated the codebase with documentation and more features. – Mauvis Ledford Sep 18 '15 at 05:56
  • This throws an error in Chrome as it refuses to deal with MIME – SumNeuron May 06 '17 at 09:52
  • Working good for me except that `d3.js` produced chart is black and white, colors got lost. Any ideas? – Nikita Vlasenko Mar 16 '18 at 23:52
3

The Simg library created and suggest by Mauvis Ledford above worked great for allowing my svg charts created with Dimple to be downloaded.

I did however need to change one aspect of the code to make it work. Inside of the toString() prototype, inside the forEach loop (line 37), if you change "svg.setAttribute(..)" to "svg[0].setAttribute" it will alleviate the "setAttribute(..) is not a function" error. Similarly the same needs to be done right below in the return statement, appending "[0]" after svg (line 39).

I also had to manually edit the "canvas.width" and "canvas.height" (lines 48 & 49) assignments in the toCanvas() prototype, in order to make the downloaded image a more correct size (it was previously just downloading a static 300x150 square in the top left corner of the chart).

Psilocybic
  • 41
  • 3
0

This is an old question, in 2022 we have ES6 and we don't need 3rd party libraries.

Here is a very basic way to convert svg images into other formats.

The trick is to load the svg element as an img element, then use a canvas element to convert into a desired format. So four steps are necessary:

  1. Extract svg as xml data string.
  2. Load the xml data string into a img element
  3. Convert the img element to a dataURL using a canvas element
  4. Load the converted dataURL into a new img element

Step 1

Extracting a svg as xml data string is simple:

// Select the element:
const $svg = document.getElementById('svg-container').querySelector('svg')

// Serialize it as xml string:
const svgAsXML = (new XMLSerializer()).serializeToString($svg)

// Encode it as a data string:
const svgData = `data:image/svg+xml,${encodeURIComponent(svgAsXML)}`

Step 2

Loading the xml data string into a img element:

// This function returns a Promise whenever the $img is loaded
const loadImage = async url => {
    const $img = document.createElement('img')
    $img.src = url
    return new Promise((resolve, reject) => {
        $img.onload = () => resolve($img)
        $img.onerror = reject
        $img.src = url
    })
}

Step 3

Converting the img element to a dataURL using a canvas element:

const $canvas = document.createElement('canvas')
$canvas.width = $svg.clientWidth
$canvas.height = $svg.clientHeight
$canvas.getContext('2d').drawImage(img, 0, 0, $svg.clientWidth, $svg.clientHeight)
return $canvas.toDataURL(`image/${format}`, 1.0)

Step 4

Loading the converted dataURL into a new img element:

const $img = document.createElement('img')
$img.src = dataURL
$holder.appendChild($img)

Here you have a working snippet:

const $svg = document.getElementById('svg-container').querySelector('svg')
const $holder = document.getElementById('img-container')
const $label = document.getElementById('img-format')

const destroyChildren = $element => {
  while ($element.firstChild) {
    const $lastChild = $element.lastChild ?? false
    if ($lastChild) $element.removeChild($lastChild)
  }
}

const loadImage = async url => {
  const $img = document.createElement('img')
  $img.src = url
  return new Promise((resolve, reject) => {
    $img.onload = () => resolve($img)
    $img.onerror = reject
    $img.src = url
  })
}

const convertSVGtoImg = e => {
  const $btn = e.target
  const format = $btn.dataset.format ?? 'png'
  $label.textContent = format

  destroyChildren($holder)

  const svgAsXML = (new XMLSerializer()).serializeToString($svg)
  const svgData = `data:image/svg+xml,${encodeURIComponent(svgAsXML)}`

  loadImage(svgData).then(img => {
    const $canvas = document.createElement('canvas')
    $canvas.width = $svg.clientWidth
    $canvas.height = $svg.clientHeight
    $canvas.getContext('2d').drawImage(img, 0, 0, $svg.clientWidth, $svg.clientHeight)
    return $canvas.toDataURL(`image/${format}`, 1.0)
  }).then(dataURL => {
    console.log(dataURL)
    const $img = document.createElement('img')
    $img.src = dataURL
    $holder.appendChild($img)
  }).catch(console.error)
}

const buttons = [...document.querySelectorAll('[data-format]')]
for (const $btn of buttons) {
  $btn.onclick = convertSVGtoImg
}
<div style="float: left; width: 70%">
  <div style="float: left; width: 50%">
    <div id="svg-container">
      <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="200" height="200" viewBox="0 0 248 204">
  <path fill="#1d9bf0" d="M221.95 51.29c.15 2.17.15 4.34.15 6.53 0 66.73-50.8 143.69-143.69 143.69v-.04c-27.44.04-54.31-7.82-77.41-22.64 3.99.48 8 .72 12.02.73 22.74.02 44.83-7.61 62.72-21.66-21.61-.41-40.56-14.5-47.18-35.07 7.57 1.46 15.37 1.16 22.8-.87-23.56-4.76-40.51-25.46-40.51-49.5v-.64c7.02 3.91 14.88 6.08 22.92 6.32C11.58 63.31 4.74 33.79 18.14 10.71c25.64 31.55 63.47 50.73 104.08 52.76-4.07-17.54 1.49-35.92 14.61-48.25 20.34-19.12 52.33-18.14 71.45 2.19 11.31-2.23 22.15-6.38 32.07-12.26-3.77 11.69-11.66 21.62-22.2 27.93 10.01-1.18 19.79-3.86 29-7.95-6.78 10.16-15.32 19.01-25.2 26.16z"/>
</svg>
    </div>
    <div>svg</div>
  </div>

  <div style="float: right; width: 50%">
    <div id="img-container"></div>
    <div id="img-format"></div>
  </div>

</div>

<div>
  <button id="btn-png" data-format="png">PNG</button>
  <button id="btn-jpg" data-format="jpeg">JPG</button>
  <button id="btn-webp" data-format="webp">WEBP</button>
</div>
Teocci
  • 7,189
  • 1
  • 50
  • 48