Search code examples
javascripthtmltypescriptsvghtml5-canvas

canvas.ToBlob working in Chrome, but not working with firefox


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;
}

Solution

  • 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.