SVG to image for display and download — and truly wishing IE R.I.P.

Background and objective

I have been working on a website that need to

  1. Add some display element on the SVG at the frontend (the backend generated SVG is missing some elements)
  2. And finally to support displaying and downloading of the resulting SVG at frontend.

My (maybe not so well) solution direction

With quite some exploration of what might and might not work, the idea would be leveraging canvas as a middle ground for rendering the SVG (as background image) as well as using the toBlob to prepare for download.

What works (for most browser)

A simplified version as follow:

// 0. The API return is expected to be base64 encoded SVG string
// 1. parse the SVG base64 text back to SVG with atob()
let svgTxt = atob(base64Res);
// 2. make a div and put the svg text as inner html
const svgContainerElem = document.createElement("div");
svgContainerElem.innerHTML = svgTxt;
// 3. Do some manipulation using DOM manipulation on the svg node
// ...
// 4. get back the modified SVG text
const svgTextUpdated = svgContainerElem.innerHTML;
// 5. convert the SVG text into base64 str and prepare to put to canvas
const svgdataUriStr = `data:image/svg+xml;utf8,${encodeURIComponent(svgTextUpdated)}`;
// 6. create an Image object and prepare to load the data
const img = new Image();
let blob2Download;
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// 6.1 make the blob ready for download
canvas.toBlob((blob)=>{
blob2Download = blob;
},"image/png");
}
img.src=svgdataUriStr;
// 7. set the bsae64 dataUri as background image of the target Div
const bgStr = `url('${svgdataUriStr}')`;
const divToDisplay = document.getElementById("DivToDisplay");
divToDisplay.style.backgroundImage = bgStr;

What does not work for IE

IE not support toBlob()

Checking with can I use (caniuse.com), it say it support with prefix ms

// calling toBlob()
canvas.toBlob((blob)=>{
blob2Download = blob;
},"image/png");
// calling IE msToBlob()
blob2Download = canvas.msToBlob("image/png");

The Image onload() failed

The reason is IE seems does not support loading the data Uri of SVG, it seemed to me the issue is about the format of the input I pass to the Image object as source

// Approach 0: utf8 with encoded svg string like <svg>...</svg>
svgdataUriStr = `data:image/svg+xml;utf8,${encodeURIComponent(svgTextUpdated)}`;
// Approach 1: using blob => security error (anything from SVG is cross origin...)
const svgData = new XMLSerializer().serializeToString(svgElement);
const blob = new Blob([svgData], {type:"image/svg+xml;charset=utf-8"});
svgdataUriStr = URL.createObjectURL(blob);
// Approach 2: using base64 SVG => security error (anything from SVG is cross origin...)
svgdataUriStr = `data:image/svg+xml;base64,${btoa(svgTextUpdated)}`;

Reason behide: Security Error when calling toDataUrl() or msToBlob()

With further debug, it’s discovered that the security

If the source of the foreign content is an HTML <img> or SVG <svg> element, attempting to retrieve the contents of the canvas isn't allowed.

Final solution

What I use is a library called canvg, what it does, according to my limited understanding (by reading their code), it seems to parse the SVG and convert them to normal HTML element like Div and translate the positioning.

const svgElement = svgContainerElem.querySelector("svg");
const svgData = new XMLSerializer().serializeToString(svgElement);
const canvgProcess = await Canvg.fromString(ctx, svgData);
canvgProcess.render();
const bgStr = `url('${canvas.toDataURL()}')`
const divToDisplay = document.getElementById("DivToDisplay");
divToDisplay.style.backgroundImage = bgStr;

Additional items that bugged me (but minor compare to above)

I would like to create some inner block like:

<g transform=translate(165 180)>
<text>Some text</text>
</g>
let gElem = document.createElementNS('http://www.w3.org/2000/svg','g');      
gElem.setAttribute("transform", "translate(165 180)");
// simply assign the SVG text element as inner html text
gElem.innerHTML = `<text>Some text</text>`;
let gElem = document.createElementNS('http://www.w3.org/2000/svg','g');      
gElem.setAttribute("transform", "translate(165 180)");
let textElem = document.createElementNS('http://www.w3.org/2000/svg','text');
textElem.textContent = "Some text";
gElem.appendChild(textElem)

Conclusion

IE is going to retire (as planned) in Jun 2022, I wish it rest in peace as supporting IE is always haunting a lot of developers.