0

I'm trying to convert a bunch of siblings SVG images to base64 PNG strings, using just the browser and plain Javascript, but for some reason beyond my knowledge I get the base64 PNG string of only the last SVG. Here is the HTML snippet:

<html>
<head><title>Browser SVG to PNG Converter</title></head>
<body bgcolor="#DDDDEE">
    <h1>Browser SVG to PNG Converter</h1>
    <div id="div_svg">
        <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150">
            <rect width="100%" height="100%" style="stroke-width:0;fill:rgb(35,235,235);" />
            <rect x="30" y="15" width="90" height="120" style="stroke:#000000;stroke-width:1;fill:none;" /> </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="130" height="150">
            <rect width="100%" height="100%" style="stroke-width:0;fill:rgb(35,200,35);" />
            <rect x="30" y="30" width="70" height="90" style="stroke:#000000;stroke-width:2;fill:none;" /> </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150">
            <rect width="100%" height="100%" style="stroke-width:0;fill:rgb(35,235,235);" />
            <rect x="30" y="15" width="90" height="120" style="stroke:#000000;stroke-width:3;fill:none;" /> </svg>
    </div>
    <br />
    <button id="btn_convert">Convert SVG to PNG and display it below</button>
    <br />
    <br />
    <canvas id="aux_canvas" style="display:none;"></canvas>
    <textarea id="output_png" style="width:90vw;height:40vh;display:block;">base64 PNG source will be displayed here:</textarea>
    <script>
    document.getElementById('btn_convert').addEventListener('click', function() {
        var svg_all = document.getElementById('div_svg').querySelectorAll('svg');
        var canvas = document.getElementById('aux_canvas');
        var win = window.URL || window.webkitURL || window;
        var img = new Image();
        img.addEventListener("load", function() {
            canvas.getContext('2d').drawImage(img, 0, 0);
            win.revokeObjectURL(url);
            document.getElementById('output_png').value += "\n\n" + canvas.toDataURL("image/png");
        });
        for(let i = 0; i < svg_all.length; i++) {
            let svg = svg_all[i];
            canvas.width = svg.getBoundingClientRect().width;
            canvas.height = svg.getBoundingClientRect().height;
            var data = new XMLSerializer().serializeToString(svg);
            var blob = new Blob([data], {
                type: 'image/svg+xml'
            });
            var url = win.createObjectURL(blob);
            document.getElementById('output_png').value += "\nGoing to load image..."
            img.src = url;
            document.getElementById('output_png').value += "\nEnded loading image..."
        }
    });
    </script>
</body>
</html>

Input as hardcoded SVG and output to textarea are only for testing purposes, my ultimate goal is a Javascript function getting an array of SVG sources and returning the corresponding PNG images as an array of base64 strings.

mmj
  • 5,514
  • 2
  • 44
  • 51

1 Answers1

3

Image loading is asynchronous, you have to wait for each image to load before moving to the next, use Promise with async and await before each loading.‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Here is a working example

const svgString = `
        <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150">
            <rect width="100%" height="100%" style="stroke-width:0;fill:rgb(35,235,235);" />
            <rect x="30" y="15" width="90" height="120" style="stroke:#000000;stroke-width:1;fill:none;" /> </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="130" height="150">
            <rect width="100%" height="100%" style="stroke-width:0;fill:rgb(35,200,35);" />
            <rect x="30" y="30" width="70" height="90" style="stroke:#000000;stroke-width:2;fill:none;" /> </svg>
        <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150">
            <rect width="100%" height="100%" style="stroke-width:0;fill:rgb(35,235,235);" />
            <rect x="30" y="15" width="90" height="120" style="stroke:#000000;stroke-width:3;fill:none;" /> </svg>`;
            
const div = document.createElement("div");
div.innerHTML = svgString;
const svgs = div.querySelectorAll("svg");


const convertButton = document.querySelector("#btn_convert");
const output = document.querySelector("#output_png");
const img = document.createElement("img");
const canvas = document.createElement("canvas");
const ctx = canvas.getContext('2d');
const URL = window.URL || window.webkitURL || window;

convertButton.onclick = async () => {        
  for(let i = 0; i < svgs.length; i++) {
    output.value += "\n---- Loading image... \n\n";
    output.value += await svgToBase64(svgs[i]);
    output.value += "\n---- Image Loaded ... \n\n";
  }
};
    
async function svgToBase64(svg) {
  const width = svg.getAttribute("width");
  const height = svg.getAttribute("height");
  
  canvas.width = width;
  canvas.height = height;

  const data = new XMLSerializer().serializeToString(svg);
  console.log(data)
  const blob = new Blob([data], {
      type: 'image/svg+xml'
  });
  console.log(blob)
  var url = URL.createObjectURL(blob);

  return new Promise((resolve) => {
    img.onload = () => {
      ctx.drawImage(img, 0, 0);
      URL.revokeObjectURL(url);
      resolve(canvas.toDataURL("image/png"));
    }
    img.src = url;
  });
}
body {
  background: #DDDDEE;
}
<h1>Browser SVG to PNG Converter</h1>
<br />
<button id="btn_convert">Convert SVG to PNG and display it below</button>
<br />
<br />
<textarea id="output_png" style="width:90vw;height:40vh;display:block;">base64 PNG source will be displayed here:</textarea>
Fennec
  • 1,535
  • 11
  • 26
  • I tried to hardcode SVG graphics in a string instead of HTML, defining `svgs` through `var d = document.createElement('div'); d.innerHTML = ' ... ... ... '; const svgs = d.querySelectorAll("svg");` but it doesn't work, returning empty base64 images strings, any idea why? Maybe `querySelectorAll` cannot be applied to dinamically created elements? – mmj Jul 02 '22 at 16:39
  • I don't think creating `SVG` with `innerHTML` works, you need to use `document.createElementNS("http://www.w3.org/2000/svg", "svg")` and the same for each child element `document.createElementNS("http://www.w3.org/2000/svg", "rect")` ... This thread could be what you're looking for https://stackoverflow.com/a/20539306/12594730 – Fennec Jul 02 '22 at 22:15
  • The browser is able to interpret the SVG coming from a string, in fact if I set the `innerHTML` of a dinamically created `div` to such string and append the `div` to DOM, SVGs are correctly displayed. So there must be a way to obtain what I look for without rebuilding the SVGs programmatically, which is almost unfeasible starting from a string. – mmj Jul 02 '22 at 23:10
  • I edited the example the SVGs are implemented from a string with `innerHTML` and it works ... I can't tell what's wrong with your implementation without a concrete code example ... Careful though, `innerHTML` will clear any event listeners you may have on elements and there's also DOM XSS attacks, never trust user input. – Fennec Jul 02 '22 at 23:58
  • The difference is that in your code the container `div` is already in DOM, while I was creating it dinamically and for some reason that way does not work. Of course I can keep in my DOM an invisible `div` just for convertion purposes, nevertheless creating it dinamically without inserting it in the DOM would be cleaner. – mmj Jul 03 '22 at 00:53
  • 1
    It was a silly one, the problem come from `getBoundingClientRect` it does not work if the element is not rendered and silently return `0`, I edited the example replaced `getBoundingClientRect` with `getAttribute("width" /*"height"*/)` ... This will not work if the `svg` does not have a `width` and `height` attributes or only a `viewBox`, you need a way to get or guess the `svg` size – Fennec Jul 03 '22 at 16:51