Search code examples
javascriptsvgd3.js

Issue: SVG Image Not Visible After Downloading SVG as Image in d3.js


Problem Description: I'm trying to create an SVG containing an image fetched from a URL (in this case, from Unsplash) and provide a download button to download the SVG as an image (PNG). However, after downloading the SVG as an image, the embedded image is not visible in the downloaded PNG file.

Code Example: Here is a simplified version of the code I'm using:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Random Image SVG with Download Button</title>
  <script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
  <svg id="svg-container" width="400" height="200"></svg>
  <br>
  <button onclick="downloadSvg()">Download as Image</button>

  <script>
    async function getRandomImageUrl() {
      const response = await fetch('https://source.unsplash.com/random');
      const imageUrl = response.url;
      return imageUrl;
    }

    async function createRandomImageSVG() {
      const imageUrl = await getRandomImageUrl();

      const svg = d3.select('#svg-container');

      // Append the image directly to the SVG
      svg.append('image')
        .attr('x', 0)
        .attr('y', 0)
        .attr('width', 400)
        .attr('height', 200)
        .attr('href', imageUrl); // Use href instead of xlink:href for newer browsers
    }

    createRandomImageSVG();

    function downloadSvg() {
      const svgElement = document.getElementById('svg-container');
      const serializer = new XMLSerializer();
      const svgString = serializer.serializeToString(svgElement);

      // Create a temporary canvas
      const canvas = document.createElement('canvas');
      canvas.width = 400;
      canvas.height = 200;

      const context = canvas.getContext('2d');

      // Create a new image
      const img = new Image();
      img.onload = function() {
        // Draw SVG onto the canvas
        context.drawImage(img, 0, 0);

        // Convert the canvas to data URL
        const dataUrl = canvas.toDataURL('image/png');

        // Create a temporary link element
        const a = document.createElement('a');
        a.href = dataUrl;
        a.download = 'random_image.png';
        a.click();
      };

      // Encode the SVG string to Base64 and set as source of image element
      img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgString)));
    }
  </script>
</body>
</html>

Expected Behavior: The SVG should be converted to a PNG image when the "Download as Image" button is clicked, and the downloaded image should include the random image fetched from Unsplash.

Actual Behavior: After downloading the SVG as an image, the PNG file does not display the embedded image from Unsplash. It appears without the image.

Additional Notes: This issue occurs because the SVG image is referenced from an external URL (Unsplash), and browsers prevent cross-origin requests for security reasons when converting SVG to a data URL. I have tried different methods, including embedding the image directly into the SVG and drawing the SVG onto a canvas before converting it to a data URL, but the issue persists. Question: How can I ensure that the downloaded PNG file includes the embedded image from Unsplash? Is there a way to handle the cross-origin issue effectively when converting SVG to a data URL?


Solution

  • I've found one solution and it's work for me

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Random Image SVG with Download Button</title>
        <script src="https://d3js.org/d3.v7.min.js"></script>
    </head>
    
    <body>
        <svg id="svg-container" width="400" height="300" xmlns="http://www.w3.org/2000/svg"></svg>
        <br>
        <button onclick="downloadSvg()">Download as Image</button>
    
        <script>
            async function getRandomImageUrl() {
                const response = await fetch('https://source.unsplash.com/random/400x300');
                const blob = await response.blob();
                return new Promise((resolve) => {
                    const reader = new FileReader();
                    reader.onloadend = () => resolve(reader.result);
                    reader.readAsDataURL(blob);
                });
            }
    
            async function createRandomImageSVG() {
                const imageUrl = await getRandomImageUrl();
    
                const svg = d3.select('#svg-container');
                svg.append('image')
                    .attr('x', 0)
                    .attr('y', 0)
                    .attr('width', 400)
                    .attr('height', 300)
                    .attr('href', imageUrl); // Embed Base64-encoded image data directly
       
            }
    
            createRandomImageSVG();
    
            function downloadSvg() {
                const svgElement = document.getElementById('svg-container');
                const serializer = new XMLSerializer();
                const svgString = serializer.serializeToString(svgElement);
    
                const canvas = document.createElement('canvas');
                canvas.width = 400;
                canvas.height = 300;
                const context = canvas.getContext('2d');
    
                const img = new Image();
                img.onload = function () {
                    context.drawImage(img, 0, 0);
    
                    const dataUrl = canvas.toDataURL('image/png');
    
                    const a = document.createElement('a');
                    a.href = dataUrl;
                    a.download = 'random_image.png';
                    a.click();
                };
    
                img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgString)));
            }
        </script>
    </body>
    
    </html>