I want to converty my SVG into CANVAS and then save it as image. I have svg already genereated by javascript in my page. I use this code:
$("#menu-save-image").click(function () {
var svg = document.getElementsByTagName('svg');
var canvas = document.getElementById("test");
canvg(canvas, svg);
// or second way
var c = document.getElementById('test');
var ctx = c.getContext('2d');
ctx.drawSvg(svg, 0, 0, 500, 500);
});
Both ways doesn't work. Why?
canvg method needs SVG source string (or url or XMLDocument), so you should convert the svg element to svg source by using XMLSerializer like this.
var svg = document.querySelector('svg');
var serializer = new XMLSerializer();
var svgString = serializer.serializeToString(svg);
var canvas = document.getElementById("test");
canvg(canvas, svgString);
see https://code.google.com/p/canvg/source/browse/trunk/canvg.js
Related
For a project I am working on I need to transform a svg to png file. In order to do so I have found multiple guides and explanations online. One of these can be found here: Exporting D3 charts to PDF
In order to transform the svg to a png they use the following code:
let canvas = document.createElement('canvas');
canvg(canvas, svg);
let imgData = canvas.toDataURL('image/png');
But I keep on getting an error when I try to implement this in my own project: "TypeError: Cannot call a class as a function". I have found multiple explanations online where they use the canvg(canvas, svg); notation. I also have read the Github documentation form Canvg and found nothing about this type of notation or an alternative way to do this.
The way I import the package into my project is as follows:
import canvg from "canvg";
This is the full code I am using to convert my d3 svg chart to a pdf:
exportToPDF() {
let svg = document.getElementsByClassName("svg")[0].innerHTML;
var canvas = document.createElement("canvas");
canvg(canvas, svg);
let imgData = canvas.toDataURL("image/png");
var doc = new jsPDF("l", "pt", [1020, 768]);
doc.addImage(imgData, "PNG", 0, 0, 1020, 768);
doc.save("svg-png-chart.pdf");
}
The error is clear, you are calling the class canvg without the new keyword.
Also, you referred to the GitHub Documentation where there is clearly write how to use it:
window.onload = () => {
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
// ==== HERE YOUR FUNCTION ===
v = canvg.Canvg.fromString(ctx, '<svg width="600" height="600"><text x="50" y="50">Hello World!</text></svg>');
// Start SVG rendering with animations and mouse handling.
v.start();
};
Probably the article you read Exporting D3 charts to PDF was refering to an older API of Canvg
This should do:
exportToPDF() {
let svg = document.getElementsByClassName("svg")[0].innerHTML;
let canvas = document.createElement("canvas");
let context = canvas.getContext('2d')
let v = canvg.Canvg.fromString(context, svg);
v.start();
let imgData = canvas.toDataURL("image/png");
var doc = new jsPDF("l", "pt", [1020, 768]);
doc.addImage(imgData, "PNG", 0, 0, 1020, 768);
doc.save("svg-png-chart.pdf");
}
I have a found the solution. The fix was also thanks to DDomen. He got me on the right path. canvg.fromString(context, svg) should be used in order to get the transform the svg to a png.
But then the next problem would arise that the dimensions need to be set on the canvas in order to have the imaged being cropped when it is bigger then the default dimensions of a canvas object.
exportToPDF() {
let svgElement = document.getElementsByClassName("svg")[0];
const width = svgElement.getBoundingClientRect().width;
const height = svgElement.getBoundingClientRect().height;
let svg = svgElement.innerHTML;
let canvas = document.createElement("canvas");
let context = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
let v = canvg.fromString(context, svg);
v.start();
let imgData = canvas.toDataURL("image/png");
var doc = new jsPDF("l", "pt", [1020, 768]);
doc.addImage(imgData, "PNG", 0, 0, width, height);
doc.save("svg-png-chart.pdf");
}
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;
};
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;
}
}
When one svg file has multiple paths(lets say objects) I need to clip those paths to multiple images. I should maintain actual positions of the paths. When adding an image can I clip this image to selected path?
Currently I can only clip svg to one image. But Thats not what I want.
https://jsfiddle.net/hydride/wh35ymev/
var svgEl = document.body.getElementsByTagName('svg')[0];
var serializer = new XMLSerializer();
var svgStr = serializer.serializeToString(svgEl);
var canvas = new fabric.Canvas('c');
canvas.backgroundColor = 'rgb(150,150,150)';
var obj;
var path = fabric.loadSVGFromString(svgStr,function(objects, options) {
obj = fabric.util.groupSVGElements(objects, options);
obj.scaleToHeight(canvas.height);
obj.setFill('transparent');
canvas.add(obj).renderAll();
});
$("#addButton").click(function(){
fabric.Image.fromURL('http://fabricjs.com/assets/pug_small.jpg', function (img) {
img.scaleToHeight(canvas.height).set({
clipTo: function (ctx) {
obj.render(ctx);
}
});
canvas.add(img).setActiveObject(img);
canvas.renderAll();
});
});
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();
});