Home > Software engineering >  Canvas.toDataURL fails when rendering SVG with clip-path
Canvas.toDataURL fails when rendering SVG with clip-path

Time:11-18

I am attempting to convert a generated SVG into a PNG/JPG via HTML5 Canvas. So far this method works with everything I have thrown at it, barring one exception. When an element in the source SVG has a clip-path attribute, canvas.toDataURL() returns an empty url (i.e. data;).

These are the relevant functions I am using to perform the conversion.

// expects svg dataurl
// returns a dataurl
async function _svgToRasterImage(url, options) {
  const image = await _svgToImageElement(url)

  const context = document.createElement('canvas').getContext('2d', {
    antialias: options.rasterAA
  })
  context.canvas.width  = options.rasterW || image.width
  context.canvas.height = options.rasterH || image.height
  context.drawImage(image,
    0, 0,          image.width,          image.height,
    0, 0, context.canvas.width, context.canvas.height
  )

  return context.canvas.toDataURL(
    options.rasterTarget,
    options.rasterQuality
  )
}

// expects svg dataurl
// returns an SVGImageElement
function _svgToImageElement(url) {
  return new Promise(async (resolve) => {
    const image = new Image()
    image.addEventListener(
      'load',
      resolve(image),
      { once: true }
    )
    image.src = url
  })
}

Here is an example of a problematic SVG (I have abbreviated some of the embedded elements e.g. href="data:image/png;base64,...").

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="180" height="252">
  <svg width="180" height="252" x="0" y="0">
    <g>
      <rect width="162" height="226.8" x="9" y="12.600000000000001" fill-opacity="0" rx="7.2" ry="7.2" stroke-width="7.2" stroke="#00ff00"/>
    </g>
    <g>
      <ellipse rx="81" ry="113.4" cx="90" cy="126" fill-opacity="0" stroke-width="7.2" stroke="#0000ff"/>
    </g>
    <g clip-path="url(&quot;#SvgjsClipPath1009&quot;)">
      <image width="256" height="256" xlink:href="data:image/png;base64,..." transform="matrix(0.6328125,0,0,0.6328125,9,45.00000000000001)"/>
    </g>
  </svg>
  <defs>
    <clipPath id="SvgjsClipPath1009">
      <rect width="162" height="226.8" x="9" y="12.600000000000001"/>
    </clipPath>
  </defs>
</svg>

While I am not certain, I am confident that the above SVG is valid because it renders correctly in the DOM in both Chrome and Firefox; however, there is a distinct possibility that I am missing something obvious that the browser is ignoring (or fixing for me in this case)!

CodePudding user response:

Thanks to @Kaiido for the help!

Your image loader has a bug, it's not waiting for the image has loaded before calling resolve(img), it sets the result of this call as the handler to the event and thus calls it directly.

The solution is to use an arrow function in the callback of the event listener because calling resolve(image) directly will immediately execute, resolving the promise before the image actually loads.

function _svgToImageElement(url) {
  return new Promise(async (resolve) => {
    const image = new Image()
    image.addEventListener(
      'load',
      e => resolve(image), // <-- DO
      //   resolve(image),    <-- DON'T
      { once: true }
    )
    image.src = url
  })
}

It is amazing how much pain 3 missing characters can cause.

  • Related