how to render svg in canvas across all browsers [duplicate] - javascript

I am trying to convert an external svg icon to a base64 png using a canvas. It is working in all browsers except Firefox, which throws an error "NS_ERROR_NOT_AVAILABLE".
var img = new Image();
img.src = "icon.svg";
img.onload = function() {
var canvas = document.createElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(this, 0, 0);
var dataURL = canvas.toDataURL("image/png");
return dataURL;
};
Can anyone help me on this please? Thanks in advance.

Firefox does not support drawing SVG images to canvas unless the svg file has width/height attributes on the root <svg> element and those width/height attributes are not percentages. This is a longstanding bug.
You will need to edit the icon.svg file so it meets the above criteria.

As mentioned, this is an open bug caused by limitations on what Firefox accepts as specification for SVG sizes when drawing to a canvas. There is a workaround.
Firefox requires explicit width and height attributes in the SVG itself. We can add these by getting the SVG as XML and modifying it.
var img = new Image();
var src = "icon.svg";
// request the XML of your svg file
var request = new XMLHttpRequest();
request.open('GET', src, true)
request.onload = function() {
// once the request returns, parse the response and get the SVG
var parser = new DOMParser();
var result = parser.parseFromString(request.responseText, 'text/xml');
var inlineSVG = result.getElementsByTagName("svg")[0];
// add the attributes Firefox needs. These should be absolute values, not relative
inlineSVG.setAttribute('width', '48px');
inlineSVG.setAttribute('height', '48px');
// convert the SVG to a data uri
var svg64 = btoa(new XMLSerializer().serializeToString(inlineSVG));
var image64 = 'data:image/svg+xml;base64,' + svg64;
// set that as your image source
img.src = img64;
// do your canvas work
img.onload = function() {
var canvas = document.createElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(this, 0, 0);
var dataURL = canvas.toDataURL("image/png");
return dataURL;
};
}
// send the request
request.send();
This is the most basic version of this solution, and includes no handling for errors when retrieving the XML. Better error handling is demonstrated in this inline-svg handler (circa line 110) from which I derived part of this method.

This isn't the most robust solution, but this hack worked for our purposes. Extract viewBox data and use these dimensions for the width/height attributes.
This only works if the first viewBox encountered has a size that accurately can represent the size of the SVG document, which will not be true for all cases.
// #svgDoc is some SVG document.
let svgSize = getSvgViewBox(svgDoc);
// No SVG size?
if (!svgSize.width || !svgSize.height) {
console.log('Image is missing width or height');
// Have size, resolve with new SVG image data.
} else {
// Rewrite SVG doc
let unit = 'px';
$('svg', svgDoc).attr('width', svgSize.width + unit);
$('svg', svgDoc).attr('height', svgSize.height + unit);
// Get data URL for new SVG.
let svgDataUrl = svgDocToDataURL(svgDoc);
}
function getSvgViewBox(svgDoc) {
if (svgDoc) {
// Get viewBox from SVG doc.
let viewBox = $(svgDoc).find('svg').prop('viewBox').baseVal;
// Have viewBox?
if (viewBox) {
return {
width: viewBox.width,
height: viewBox.height
}
}
}
// If here, no viewBox found so return null case.
return {
width: null,
height: null
}
}
function svgDocToDataURL(svgDoc, base64) {
// Set SVG prefix.
const svgPrefix = "data:image/svg+xml;";
// Serialize SVG doc.
var svgData = new XMLSerializer().serializeToString(svgDoc);
// Base64? Return Base64-encoding for data URL.
if (base64) {
var base64Data = btoa(svgData);
return svgPrefix + "base64," + base64Data;
// Nope, not Base64. Return URL-encoding for data URL.
} else {
var urlData = encodeURIComponent(svgData);
return svgPrefix + "charset=utf8," + urlData;
}
}

Related

Base64 image not displaying unless hardcoded or in setTimeout set to 0

Im converting an SVG to a PNG and it's working fine except for some super bizarre behavior that setting the image src to the b64 value only works if you put it in a setTimeout of 0. If you copy the b64 value and hardcode it as the src it also works. Here's the JS:
var testSVG = {
height: 31.987199999999998,
template: '<svg width="19.2" height="31.987199999999998" viewBox="0 0 149.39 248.95" xmlns="http://www.w3.org/2000/svg"><path fill="#c599fe" stroke="#000" stroke-miterlimit="10" stroke-width="5" d="M74.89,236.14c-5.35-26.25-14.78-48.1-26.2-68.35-8.47-15-18.29-28.88-27.37-43.45-3-4.86-5.65-10-8.56-15C6.94,99.2,2.21,87.5,2.51,72.32A68.92,68.92,0,0,1,13.28,35.88,71.31,71.31,0,0,1,63.34,3.32,75.55,75.55,0,0,1,112,12.53a70.38,70.38,0,0,1,24,23.22,68.1,68.1,0,0,1,10.9,36.32A67.12,67.12,0,0,1,144,92.82c-1.8,6-4.69,11-7.26,16.34-5,10.44-11.32,20-17.64,29.57C100.29,167.24,82.62,196.3,74.89,236.14Z"/><circle cx="74.69" cy="72.16" r="25.29"/></svg>',
width: 19.2
}
// Pass in the testSVG object
var convertSVGtoBitmap = function (svgObject) {
if(!svgObject) return null;
var canvas = document.createElement("canvas");
canvas.width = Math.ceil(svgObject.width);
canvas.height = Math.ceil(svgObject.height);
var ctx = canvas.getContext("2d");
// Convert the SVG string into a base64 and append a header
var svg = btoa(svgObject.template);
var b64Start = 'data:image/svg+xml;base64,';
var image64 = b64Start + svg;
// Draw the SVG onto the canvas instance
var img = new Image();
img.src = image64;
ctx.drawImage(img, 0, 0);
// Return both svg base64 and png base64
return {svg: image64, png: canvas.toDataURL()};
};
console.warn('test')
// Get SVG base64 stuff
var svgimg = document.createElement("img");
document.getElementById('svg-render').appendChild(svgimg);
svgimg.src = convertSVGtoBitmap(testSVG).svg;
// Get converted PNG base64 stuff
var pngimg = document.createElement("img");
document.getElementById('png-render').appendChild(pngimg);
setTimeout(function () {
pngimg.src = convertSVGtoBitmap(testSVG).png;
}, 0)
Note, this works because of the setTimeout of 0. If I change it to just pngimg.src = convertSVGtoBitmap(testSVG).png; without the setTimeout it doesn't display. If I do pngimg.src = '...' where the ... is the hardcoded b64 value it also works. Here's a JSBin if you want to mess with it.
https://jsbin.com/qecedev/2/edit?html,js,output
I also tried putting it in a document.ready from jQuery as well as adding an onload to the pngimg object and adding the src in that.
I figured it out. The problem area was this
// Draw the SVG onto the canvas instance
var img = new Image();
img.src = image64;
ctx.drawImage(img, 0, 0);
// Return both svg base64 and png base64
return {svg: image64, png: canvas.toDataURL()};
The png value in the return wasnt loaded yet. I converted it to have a callback (you could use async or promises too) and it started working.
// USE CALLBACK
var convertSVGtoBitmap = function (svgObject, callback) {
if(!svgObject) return null;
var canvas = document.createElement("canvas");
canvas.width = Math.ceil(svgObject.width);
canvas.height = Math.ceil(svgObject.height);
var ctx = canvas.getContext("2d");
var svg = btoa(svgObject.template);
var b64Start = 'data:image/svg+xml;base64,';
var image64 = b64Start + svg;
var img = new Image();
// WAIT FOR IMAGE TO LOAD
img.onload = function () {
ctx.drawImage(img, 0, 0);
// NOW WE CAN RETURN VALUES!
callback(image64, canvas.toDataURL());
}
img.src = image64;
};

Blob: Unable to decode image at URL

I'm currently making a Word Web Add-in.
This uses Internet Explorer as engine.
My Add-in needs to load multiple selected images from the users computer.
Because some of the selected images might be quite big, I resize them using HTML5 canvas. This is my code to resize:
function makeSmallImage(imageContainer, retries)
{
if (retries === undefined)
retries = 0;
console.log('Resizing image..')
return new Promise(function (resolve, reject)
{
img = img || new Image();
img.onload = function ()
{
// calculate new size
var width = 200;
var height = Math.floor((width / img.naturalWidth) * img.naturalHeight);
console.log('new size', width, height);
try
{
// create an off-screen canvas
canvas = canvas || document.createElement('canvas'),
ctx = ctx || canvas.getContext('2d');
// antialiasing
ctx.imageSmoothingEnabled = true;
// set its dimension to target size
canvas.width = width;
canvas.height = height;
// draw source image into the off-screen canvas:
ctx.drawImage(img, 0, 0, width, height);
// clean up
imageContainer.largeData = undefined;
if (img.src.substr(0, 4) === 'blob')
URL.revokeObjectURL(img.src);
img.src = '';
// encode image to data-uri with base64 version of compressed image
var newDataUri = canvas.toDataURL();
ctx.clearRect(0, 0, canvas.width, canvas.height);
console.log('Image resized!');
imageContainer.resizedData = newDataUri;
resolve(imageContainer);
}
catch (e)
{
if (img.src !== undefined && img.src.substr(0, 4) === 'blob')
URL.revokeObjectURL(img.src);
console.log(e);
if (e.message === "Unspecified error." && retries < 5)
{
setTimeout(function (imgContainer, re)
{
makeSmallImage(imgContainer, re).then(resolve).catch(reject);
}, 2000, imageContainer, retries + 1);
}
else
reject('There was an error while trying to resize one of the images!');
}
};
try
{
var blob = new Blob([imageContainer.largeData]);
img.src = URL.createObjectURL(blob);
} catch (e)
{
reject(e);
}
});
}
'img', 'canvas' and 'ctx' are global variables, so the same elements are reused.
'imgcontainer.largedata' is an uint8array. To avoid a lot of memory usage i'm loading and resizing the images one by one.
Despite of that, after loading for example 120 images of 10mb, it might happen that I get an error:
Unable to decode image at URL:
'blob:D5EFA3E0-EDE2-47E8-A91E-EAEAD97324F6'
I then get an exception "Unspecified error", with not a lot more info.
You can see in the code now that I added a litle mechanism to try again, but all new attempts fail.
I think the reason is that internet explorer is using too much memory. I think some resources are not being cleaned up correctly, but I can't seem to spot a memory leak in my code here (if you can, please let me know).
Does anybody have an idea of how I could fix this, or work around this?
if you try to resize the image, why not directly use the office APIs? you can first get the images, then use the height/width property to resize it, such as
image1.height = 5; image1.width = 5;

Getting a base64 png from a PDF.JS pdf converted to a canvas tag?

I'm trying to get a base64 encoded png value from a canvas that was created by converting a pdf with the PDF.JS library by Mozilla.
So I had a base64 encoded PDF that I converted into a HTML canvas. I now need to convert that HTML canvas into a base64 encoded png value that I can use with a HTML img tag that displays properly.
I tried the HTMLCanvasElement.toDataURL(), however it doesn't display anything. I tried using the same method with some green boxes drawn in canvas and it worked fine, meaning that the base64 encoded pdf converted to canvas just doesn't want to work with that method.
Any other solutions or a workaround for the solution I tried?
**I need to do this inside my JS code with my HTML.
function convertDataURIToBinary() {
var raw = window.atob(BASE64 OF PDF);
var rawLength = raw.length;
var array = new Uint8Array(new ArrayBuffer(rawLength));
for(var i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i);
}
return array;
}
var pdfAsArray = convertDataURIToBinary();
PDFJS.workerSrc = "http://mozilla.github.io/pdf.js/build/pdf.worker.js";
var canvas = _el.querySelector(".insertHere");
PDFJS.getDocument(pdfAsArray).then(function getPdfHelloWorld(pdf) {
pdf.getPage(1).then(function getPageHelloWorld(page) {
var scale = 1.5;
var viewport = page.getViewport(scale);
var context = canvas.getContext("2d");
canvas.height = viewport.height;
canvas.width = viewport.width;
var renderContext = {
canvasContext: context,
viewport: viewport
};
page.render(renderContext);
canvas = _el.querySelector(".insertHere");
var imgSrc = canvas.toDataURL("image/png");
var img = new Image();
img.src = imgSrc;
_el.querySelector(".insertImageLabel").appendChild(img);
});
});
You are not waiting on the promise returned by render() to finish async operation. See https://github.com/mozilla/pdf.js/blob/master/examples/learning/prevnext.html#L76 how to wait on completion:
...
var renderTask = page.render(renderContext);
// Wait for rendering to finish
renderTask.promise.then(function () {
// Page rendered, now take snapshot.
});
...

Download Svg as png doesnt work on IE11

The svg d3 graph needs to be downloaded.I have used saveSvgAsPngJpgSvg to initiate download .The code works fine on all other browsers expect IE11.
Fiddle
I have tried :
svgAsDataUri(svg, {}, function(svg_uri) {
render_width=1000;
render_height=1000;
var canvas = document.createElement('canvas');
canvas.height = render_height;
canvas.width = render_width;
document.body.appendChild(canvas);
// Create an image and draw the SVG onto the canvas
var image = new Image;
image.onload = function() {
canvas.getContext('2d').drawImage(this, 0, 0, render_width, render_height);
};
image.src = svg_uri;
setTimeout(function(){//download_in_ie(canvas, 'filename' + '.png');
if (canvas.msToBlob) { //for IE
var blob = canvas.msToBlob();
window.navigator.msSaveBlob(blob, 'dicomimage.png');
}
}, 3000);
});
using msSaveBlob as suggested in the given link results to securityError
How can i achieve this?

javascript, d3 save svg as png loses color and font

I have svg-png almost working perfectly client side, just in javascript/d3 it all works, but it loses large amounts of detail. Can anyone shed light on why this might be?:
The original image is:
var svg = document.querySelector( "svg" );
var svgData = new XMLSerializer().serializeToString( svg );
var canvas = document.createElement("canvas");
canvas.width = d3.select("svg").attr("width");
canvas.height = d3.select("svg").attr("height");
ctx = canvas.getContext("2d");
var img = document.createElement( "img" );
img.setAttribute( "src", "data:image/svg+xml;base64," + btoa( svgData ) );
img.onload = function() {
ctx.drawImage( img, 0, 0 );
var canvasdata = canvas.toDataURL("image/png");
console.log(canvasdata)
var pngimg = '<img src="'+canvasdata+'">';
d3.select("#pngdataurl").html(pngimg);
var a = document.createElement("a");
a.download = "name"+".png";
a.href = canvasdata;
console.log(a.click())
};
The output of that is:
I also tried:
var html = d3.select("svg")
.attr("version", 1.1)
.attr("xmlns", "http://www.w3.org/2000/svg")
.node().parentNode.innerHTML;
var width = d3.select("svg").attr("width");
var height = d3.select("svg").attr("height");
image.src = 'data:image/svg+xml;base64,'+ btoa(unescape(encodeURIComponent(html)));
but that fails - on the image.onload() I get "HTMLImageElement provided is in the 'broken' state"
Saving an SVG to PNG in the browser will ignore all linked CSS styles applied to the SVG elements.
If you are using d3.js you can add the styles directly to the elements of a particular class using:
d3.selectAll(".mg-main-area").style("color","red");
For more details see this article, which I wrote about rasterising SVG in the browser.
From the comments it seems you have library applying the CSS, so if you don't want to override the styles manually using d3, you could use jQuery code like the below to pull all CSS elements from a class and apply them as inline styles:
$('.mg-main-area').each(function(i, e) {
var st = e.style;
var props = [];
for(var prop in st) {
if($(this).css(prop)) {
props.push(prop + ':' + $(this).css(prop));
}
}
this.style.cssText = props.join(';');
$(this).children().makeCssInline();
});

Categories