0

Chrome: v96 Firefox: v95

I'm trying to download an SVG image as a PNG image from the browser. This seems to be working fine for Chrome, but I'm downloading a blank image with firefox. Any idea why?

export function downloadSvgImage(svgElement: HTMLElement, name: string) {

    const xml = new XMLSerializer().serializeToString(svgElement);
    const svg64 = window.btoa(xml);
    const b64Start = 'data:image/svg+xml;base64,';

    const viewBox = svgElement.getAttribute('viewBox');
    const dimensionArr = viewBox.split(' ');
    const width = parseInt(dimensionArr[2]);
    const height = parseInt(dimensionArr[3]);

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = width;
    canvas.height = height;

    const image = new Image();

    image.onload = () => {

        canvas.getContext('2d').drawImage(image, 0, 0, width, height);

            canvas.toBlob((blob: any) => {
                const anchor = document.createElement('a');
                anchor.download = `${name}.png`;
                anchor.href = URL.createObjectURL(blob);
                anchor.click();
                URL.revokeObjectURL(blob);
            }, 'image/png');

    };
    image.src = b64Start + svg64;
}
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#syntax Docs say blob can be null if an error occurs. Is that what's happening? – nlta Jan 04 '22 at 20:29
  • The blob is not null when I console.log() it in the .toBlob function. The Size of the blob in Firefox, is much smaller than the blob in Chrome. – FiddleRacoon99 Jan 04 '22 at 20:30
  • 1
    I'm curious if you get a different result if you don't call revokeObjectURL - I'm wondering if there's a race condition here. – Todd Jan 04 '22 at 20:31
  • That is a good idea, but when I don't call revokeObjectURL, it still does not work. – FiddleRacoon99 Jan 04 '22 at 20:39

1 Answers1

1

I updated the code that captures the dimension to the following, and it downloads the SVG image through Firefox as intended:

...
  let dimensionX = svgElement.viewBox.baseVal.width;
  let dimensionY = svgElement.viewBox.baseVal.height;
  if (dimensionX == 0 || dimensionY == 0) {
    dimensionX = svgElement.getBBox().width;
    dimensionY = svgElement.getBBox().height;
  }
  const width = dimensionX;
  const height = dimensionY;
...

function downloadSvgImage(svgElement, name) {
  const xml = new XMLSerializer().serializeToString(svgElement);
  const svg64 = window.btoa(xml);
  const b64Start = "data:image/svg+xml;base64,";

  let dimensionX = svgElement.viewBox.baseVal.width;
  let dimensionY = svgElement.viewBox.baseVal.height;
  if (dimensionX == 0 || dimensionY == 0) {
    dimensionX = svgElement.getBBox().width;
    dimensionY = svgElement.getBBox().height;
  }

  const width = svgElement.clientWidth * 0.5; // dimensionX;
  const height = svgElement.clientHeight * 0.5; // dimensionY;

  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;

  const image = new Image();

  image.onload = () => {
    canvas.getContext("2d").drawImage(image, 0, 0, width, height);
    const url = canvas.toDataURL("image/png", 1);
    const anchor = document.createElement("a");
    anchor.download = `${name}.png`;
    anchor.href = url;
    anchor.click();

    setTimeout(() => URL.revokeObjectURL(url), 0);
  };
  image.src = b64Start + svg64;
}

window.onload = function(){
  const svg = document.querySelector("#cartman");
  svg.setAttribute("width", svg.clientWidth);
  svg.setAttribute("height", svg.clientHeight);
  console.log("Download starting in 3 seconds...");
  setTimeout(() => downloadSvgImage(svg, "cartman-sp"), 3000);
}
<svg xmlns="http://www.w3.org/2000/svg" id="cartman" viewBox="0 0 104 97">
  <path d="M14,85l3,9h72c0,0,5-9,4-10c-2-2-79,0-79,1" fill="#7C4E32"/>
  <path d="M19,47c0,0-9,7-13,14c-5,6,3,7,3,7l1,14c0,0,10,8,23,8c14,0,26,1,28,0c2-1,9-2,9-4c1-1,27,1,27-9c0-10,7-20-11-29c-17-9-67-1-67-1" fill="#E30000"/>
  <path d="M17,32c-3,48,80,43,71-3 l-35-15" fill="#FFE1C4"/>
  <path d="M17,32c9-36,61-32,71-3c-20-9-40-9-71,3" fill="#8ED8F8"/>
  <path d="M54,35a10 8 60 1 1 0,0.1zM37,38a10 8 -60 1 1 0,0.1z" fill="#FFF"/>
  <path d="M41,6c1-1,4-3,8-3c3-0,9-1,14,3l-1,2h-2h-2c0,0-3,1-5,0c-2-1-1-1-1-1l-3,1l-2-1h-1c0,0-1,2-3,2c0,0-2-1-2-3M17,34l0-2c0,0,35-20,71-3v2c0,0-35-17-71,3M5,62c3-2,5-2,8,0c3,2,13,6,8,11c-2,2-6,0-8,0c-1,1-4,2-6,1c-4-3-6-8-2-12M99,59c0,0-9-2-11,4l-3,5c0,1-2,3,3,3c5,0,5,2,7,2c3,0,7-1,7-4c0-4-1-11-3-10" fill="#FFF200"/>
  <path d="M56,78v1M55,69v1M55,87v1" stroke="#000" stroke-linecap="round"/>
  <path d="M60,36a1 1 0 1 1 0-0.1M49,36a1 1 0 1 1 0-0.1M57,55a2 3 0 1 1 0-0.1M12,94c0,0,20-4,42,0c0,0,27-4,39,0z"/>
  <path d="M50,59c0,0,4,3,10,0M56,66l2,12l-2,12M25,50c0,0,10,12,23,12c13,0,24,0,35-15" fill="none" stroke="#000" stroke-width="0.5"/>
</svg>

I hope it solves your issue.

NOTE: You will have to test this code inside your local environment as downloads from frames are intentionally blocked by the browser.


Update

So I realized that my initial "fix" wasn't actually working as it was still downloading a small empty canvas. I eventually discovered that Firefox has a long-standing bug with regards to rendering SVGs inside the canvas element unless the width and height attributes are specified on the <SVG> root element with non-percentage based values. So I fixed this by manually setting them to their <SVG>'s client dimensions:

svg.setAttribute("width", svg.clientWidth);
svg.setAttribute("height", svg.clientHeight);

You can define the dimensions of the image/png as a percentage of the <SVG>'s width & height.

  const width = svgElement.clientWidth * 0.5; // half the width of the original svg
  const height = svgElement.clientHeight * 0.5; // half the height of the original svg
  ...
  canvas.width = width;
  canvas.height = height;

I also updated the code for the data URL generation to use the .toDataURL method of the canvas element, and delayed the revokeObjectURL until moments after the download initializes:

    canvas.getContext("2d").drawImage(image, 0, 0, width, height);
    const url = canvas.toDataURL("image/png", 1);
    const anchor = document.createElement("a");
    anchor.download = `${name}.png`;
    anchor.href = url;
    anchor.click();
    setTimeout(() => URL.revokeObjectURL(url), 0);

I hope this update solves the issue officially this time around.

Vektor
  • 697
  • 5
  • 14