Convert SVG to PNG/JPEG with custom width and height - javascript

I have a SVG code with width and height. I want to download this SVG in PNG and JPEG Format with custom width and Height.
I have tried HTML canvas approach to achieve this but when canvas draws image it crops out the SVG.
Here is the Code
SVG Code
<svg id="svgcontent" width="640" height="480" x="640" y="480" overflow="visible" xmlns="http://www.w3.org/2000/svg" xmlns:se="http://svg-edit.googlecode.com" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 480"><!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit--><g class="layer" style="pointer-events:all"><title style="pointer-events:inherit">Layer 1</title><ellipse fill="#FF0000" stroke="#000000" stroke-width="5" cx="280.5" cy="235.5" rx="217" ry="198" id="svg_1"></ellipse></g></svg>
JavaScript Function for conversion of SVG to png/jpeg
function save() {
// Converting SVG to String
var stringobJ = new XMLSerializer();
var svg = document.getElementById('svgcontent');
var svgString = stringobJ .serializeToString(svg );
// IE9 doesn't allow standalone Data URLs
svg = '<?xml version="1.0"?>\n' + svgString ;
// Creating an Image Element
var image = new Image();
image.src = 'data:image/svg+xml;base64,' + btoa(svg);
image.width = 300; // This doesn't have any effect
image.height = 150; // This doesn't have any effect
// Creating Canvas Element
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
image.onload = function() {
context.drawImage(image, 0, 0);
var a = document.createElement('a');
a.download = "image.png"; //Saving in PNG
a.href = canvas.toDataURL('image/png'); //Saving in PNG
a.style = 'display: none;';
a.click();
}
}
It gives me Imgae in PNG format but its not complete image of SVG its just the part of image according to width of canvas bcz canvas draws image from top right corner of image and it goes on drawing image till width and height of canvas.
By default canvas width is 300 and height is 150
So if canvas width and height is not give its just outputs and image of 300x150.
I have tried canvas.width = anyvalue;
canvas.height= anyvalue;
but it doesn't effect the output
what i want is
that no matter what is the dimensions of SVG
when user gives width and height the SVG should completely fit in canvas
This is the Actual SVG and this actually needed on download with all white background and image
this is this is the output but i want full image with these dimensions
Giving width and height to canvas as actual SVG have is not a solution to my problem..... width and height of canvas is dynamic
jsfiddle link to the problem

I've made a few changes:
your svg viewBox="0 0 640 480". This defines the size of the SVG canvas. When you draw the image (unless you want to crop it) you want to keep the height proportional, i.e. if you want the width to be 300 the height should be 225.
Then when you create a new canvas element you need to declare the width and the height of the canvas element canvas.width = image.width, canvas.height = image.height,before drawing the image.
function save() {
// Converting SVG to String
var stringobJ = new XMLSerializer();
var svg = document.getElementById('svgcontent');
var svgString = stringobJ .serializeToString(svg );
// IE9 doesn't allow standalone Data URLs
svg = '<?xml version="1.0"?>\n' + svgString ;
// Creating an Image Element
var image = new Image();
image.src = 'data:image/svg+xml;base64,' + btoa(svg);
image.width = 300;
image.height = 480*image.width / 640; // keep the height proportional
// Creating Canvas Element
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
image.onload = function() {
canvas.width = image.width,canvas.height = image.height,
context.drawImage(image, 0, 0);
var a = document.createElement('a');
a.download = "image.png"; //Saving in PNG
a.href = canvas.toDataURL('image/png'); //Saving in PNG
a.style = 'display: none;';
a.click();
}
}
//save()
<svg id="svgcontent" viewBox="0 0 640 480" xmlns="http://www.w3.org/2000/svg" xmlns:se="http://svg-edit.googlecode.com" xmlns:xlink="http://www.w3.org/1999/xlink"><!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit-->
<g class="layer" style="pointer-events:all">
<title style="pointer-events:inherit">Layer 1</title>
<ellipse fill="#FF0000" stroke="#000000" stroke-width="5" cx="280.5" cy="235.5" rx="217" ry="198" id="svg_1"></ellipse>
</g>
</svg>

Related

SVG to Image returning blank image blob

I have an interactive drawing app on my website, and I want to create a button where one could share their drawing on FB.
I'm trying to convert the SVG element to a blob, to then pass it to og:image, but I'm having some issues with the conversion.
I have two trials:
one doesn't trigger the onload function for some reason.
The other returns an empty blob
both trials work fine on jsfiddle however.
First Attempt
var xmlSerializer = new XMLSerializer();
var svgString = xmlSerializer.serializeToString(document.querySelector("#svg"));
var canvas = document.createElement("canvas");
var bounds = {
width: 1040,
height: 487
};
canvas.width = bounds.width;
canvas.height = bounds.height;
var ctx = canvas.getContext("2d");
var DOMURL = self.URL || self.webkitURL || self;
var img = new Image();
var svg = new Blob([svgString], {
type: "image/svg+xml;charset=utf-8"
});
var url = DOMURL.createObjectURL(svg);
img.onload = function() {
ctx.drawImage(img, 0, 0);
var png = canvas.toDataURL("image/png");
var mg = document.createElement("img");
mg.setAttribute("src", png);
document.body.appendChild(mg);
DOMURL.revokeObjectURL(png);
};
img.id = "testimg";
img.setAttribute("src", url);
Second Attempt
var svgString = new XMLSerializer().serializeToString(document.querySelector("svg"));
var canvas = document.createElement('CANVAS');
var ctx = canvas.getContext("2d");
var DOMURL = self.URL || sel.webkitURL || self;
var img = new Image();
var svg = new Blob([svgString], {
type: "image/svg+xml;charset=utf-8"
});
var url = DOMURL.createObjectURL(svg);
img.onload = function() {
ctx.drawImage(img, 0, 0);
var png = canvas.toDataURL("image/png");
var container = document.createElement('DIV');
container.innerHTML = '<img src="' + png + '"/>';
DOMURL.revokeObjectURL(png);
};
img.src = url;
document.body.appendChild(img);
Here's the app with the two attempts triggered by the two buttons "test1" and "test2"
The problem lies in the way you did define the xmlns:xlink attributes.
Currently from your page doing document.querySelector("use").attributes.getNamedItem("xmlns:xlink").namespaceURI will return null. This means that this attribute has been defined in the Document's namespace (HTML), so when you'll stringify it using the XMLSerializer, you will actually have two xmlns:xlink attributes on your elements, one in the HTML namespace, and the SVG one that is implied in an SVG embed in an HTML document.
It is invalid to have two same attributes on the same element in SVG, and thus your file is invalid and the image won't load.
If you are facing this issue it's certainly because you did set this attribute through JavaScript:
const newUse = document.createElementNS("http://www.w3.org/2000/svg", "use");
newUse.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
newUse.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#foo");
document.querySelector("svg").append(newUse);
console.log("set from markup:", document.querySelector("use").attributes.getNamedItem("xmlns:xlink").namespaceURI);
console.log("(badly) set from JS:", document.querySelector("use+use").attributes.getNamedItem("xmlns:xlink").namespaceURI);
// the last <use> has two xmlns:xlink attributes
console.log("serialization:", new XMLSerializer().serializeToString(document.querySelector("svg")));
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#foo"/>
</svg>
To set it correctly, you need to use setAttributeNS() and use the XMLNS namespace:
const newUse = document.createElementNS("http://www.w3.org/2000/svg", "use");
document.querySelector("svg").append(newUse);
// beware the last "/"
newUse.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
console.log("set from markup", document.querySelector("use").attributes.getNamedItem("xmlns:xlink").namespaceURI);
console.log("(correctly) set from JS", document.querySelector("use+use").attributes.getNamedItem("xmlns:xlink").namespaceURI);
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30">
<use xmlns:xlink="http://www.w3.org/1999/xlink"/>
</svg>
However the best is to not set at all these attributes.
As I said above, SVGs embedded in HTML do automatically have the correct xmlns and xlink namespaces defined without the need for attributes. And since you are creating your elements through JS, you already do define them in the correct namespace too.
So don't bother with these attributes:
const SVGNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(SVGNS, "svg");
// To be able to draw an SVG image on a canvas in Firefox
// you must set absolute width and height to the root svg node
svg.setAttribute("width", 50);
svg.setAttribute("height", 50);
const target = document.createElementNS(SVGNS, "symbol");
target.id = "target";
const rect = document.createElementNS(SVGNS, "rect");
rect.setAttribute("width", 50);
rect.setAttribute("height", 50);
rect.setAttribute("fill", "green");
const use = document.createElementNS(SVGNS, "use");
// since SVG2 we don't even need to set href in the xlink NS
use.setAttribute("href", "#target");
target.append(rect);
svg.append(target, use);
const svgString = new XMLSerializer().serializeToString(svg);
console.log(svgString); // contains all the NS attributes
const blob = new Blob([svgString], { type: "image/svg+xml" });
const img = new Image();
img.src = URL.createObjectURL(blob);
document.body.append(img);

Canvas drawImage using the position of div on top of image

I have a div that is on top of the image. I have to get the image content which is inside the overlaying div and draw using canvas.
The problem I am facing is, I am not able to draw the correct image. I have to programmatically get the correct image below the div.
The image below shows the comparison of the original image and drawn image in canvas.
Below is my code which drawsImage by taking the coordinates of the div:
DrawImage() {
var canvas: any = document.getElementById("myCanvas");
var canvasRef = canvas.getBoundingClientRect();
var ctx = canvas.getContext("2d");
var imagemarker = document.getElementById("imagemarker");
var markerRef = imagemarker.getBoundingClientRect();
var img = document.getElementById("myimage");
ctx.drawImage(
img,
markerRef.left,
markerRef.top,
markerRef.width,
markerRef.height,
0,
0,
canvasRef.width,
canvasRef.height
);
}
And the html code to display image and canvas element:
<button (click)="DrawImage()">Draw</button>
<div id="imagemarkercontainer">
<div id="imagemarker"></div>
</div>
<img id="myimage" src="https://imgur.com/NWQo2xk.jpeg" width="300px"
height="250px">
<br>
<canvas id="myCanvas" width="300px" height="250px" style="border:1px
solid #d3d3d3;"></canvas>
I have attached a stackblitz example that shows the problem. https://stackblitz.com/edit/angular-4untgz?file=src%2Fapp%2Fapp.component.ts
How can I achieve this?
You need to take care of img bounding rect and also natural width & height of image.
As you are setting image width & height, get naturalWidth and calculate proportional width as below
var imgRef = img.getBoundingClientRect();
var nWidth = img.naturalWidth;
var nHeight = img.naturalHeight;
ctx.drawImage(
img,
( markerRef.left - imgRef.left) * nWidth /img.width ,
( markerRef.top -imgRef.top) * nHeight / img.height ,
markerRef.width * nWidth /img.width,
markerRef.height * nHeight / img.height ,
0,
0,
canvasRef.width,
canvasRef.height
);
Here is working code

Overlay 2x image blobs

I've created a function that converts a Mapbox GL JS canvas to a png image blob.
map.getCanvas().toBlob(function (blob) {
mapBlob=blob;
saveAs(blob, 'map.png');
})
Unfortunatley this does not include DOM elements, one of which is the scalebar. I can create a seperate blob of the scalebar using dom-to-image.
This looks like below and creates a perfectly sized image blob of the scalebar.
var node = document.getElementById('scaleBar');
domtoimage.toBlob(node)
.then(function (blob) {
scaleBlob=blob
saveAs(scaleBlob, 'scale.png');
})
After these two functions I have 2x image blobs that I'd like to combine (scale bar on top of map).
Using Javascript, how can I combine the now named variables scaleBlob and mapBlob to a single blob, which will then be saved as a png locally?
Examples of the saved map image
And saved scaled image (just a line really but sized appropriately)
And this is what I want as a new, final image
Here is an example:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var circle = "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' height='150' width='150'><circle cx='50' cy='50' r='20' stroke='black' stroke-width='3' fill='red' /></svg> ";
var blob1 = new Blob([circle], {type: "image/svg+xml;charset=utf-8"});
var rect = "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' height='50' width='50'><rect width='30' height='50' style='fill:rgb(0,0,255)'/></svg> ";
var blob2 = new Blob([rect], {type: "image/svg+xml;charset=utf-8"});
renderImage(blob1, 0, 0)
renderImage(blob2, 20, 50)
function renderImage(blob, x , y){
var img = new Image();
img.onload = function(){ ctx.drawImage(img, x, y) }
img.src = URL.createObjectURL(blob);
};
<canvas id = 'canvas'></canvas>
Here I'm just putting the blobs from svg, but on the same way you could do with two different blobs, and at the end you can do the saveAs

Image cropped while converting SVG to PNG

While saving an SVG to PNG, the image saved contains only the SVG rendered in the viewbox/window. How can one save a large PNG, containing the whole SVG?
// SVG element and XML string.
var svg = document.querySelector('svg');
var svgData = new XMLSerializer().serializeToString(svg);
// Canvas to hold the image.
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
// Canvas size = SVG size.
var svgSize = svg.getBoundingClientRect();
canvas.width = svgSize.width;
canvas.height = svgSize.height;
// Image element appended with data.
var img = document.createElement('img');
img.setAttribute('src', 'data:image/svg+xml;base64,' + btoa(svgData));
img.onload = function() {
// Draw image on canvas and convert to URL.
context.drawImage(img,0,0);
console.log(canvas.toDataURL('image/png'));
};
Instead of:
var svgSize = svg.getBoundingClientRect();
Use:
var svgSize = svg.viewBox.baseVal;
This will get you the true dimensions of the viewBox.
REFERENCE
https://stackoverflow.com/a/7682976/2813224
SNIPPET
// SVG element and XML string.
var svg = document.querySelector('svg');
var svgData = new XMLSerializer().serializeToString(svg);
// Canvas to hold the image.
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
// Canvas size = SVG size.
var svgSize = svg.viewBox.baseVal;
canvas.width = svgSize.width;
canvas.height = svgSize.height;
// Image element appended with data.
var img = document.createElement('img');
img.setAttribute('src', 'data:image/svg+xml;base64,' + btoa(svgData));
img.onload = function() {
// Draw image on canvas and convert to URL.
context.drawImage(img,0,0);
console.log(canvas.toDataURL('image/png'));
};
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="64px" height="64px" viewBox="-0.5 0.5 64 64" enable-background="new -0.5 0.5 64 64" xml:space="preserve">
<g>
<circle fill="#FFFFFF" cx="31.325" cy="32.873" r="30.096"/>
<path id="text2809_1_" d="M31.5,14.08c-10.565,0-13.222,9.969-13.222,18.42c0,8.452,2.656,18.42,13.222,18.42 c10.564,0,13.221-9.968,13.221-18.42C44.721,24.049,42.064,14.08,31.5,14.08z M31.5,21.026c0.429,0,0.82,0.066,1.188,0.157 c0.761,0.656,1.133,1.561,0.403,2.823l-7.036,12.93c-0.216-1.636-0.247-3.24-0.247-4.437C25.808,28.777,26.066,21.026,31.5,21.026z M36.766,26.987c0.373,1.984,0.426,4.056,0.426,5.513c0,3.723-0.258,11.475-5.69,11.475c-0.428,0-0.822-0.045-1.188-0.136 c-0.07-0.021-0.134-0.043-0.202-0.067c-0.112-0.032-0.23-0.068-0.336-0.11c-1.21-0.515-1.972-1.446-0.874-3.093L36.766,26.987z"/>
<path id="path2815_1_" d="M31.433,0.5c-8.877,0-16.359,3.09-22.454,9.3c-3.087,3.087-5.443,6.607-7.082,10.532 C0.297,24.219-0.5,28.271-0.5,32.5c0,4.268,0.797,8.32,2.397,12.168c1.6,3.85,3.921,7.312,6.969,10.396 c3.085,3.049,6.549,5.399,10.398,7.037c3.886,1.602,7.939,2.398,12.169,2.398c4.229,0,8.34-0.826,12.303-2.465 c3.962-1.639,7.496-3.994,10.621-7.081c3.011-2.933,5.289-6.297,6.812-10.106C62.73,41,63.5,36.883,63.5,32.5 c0-4.343-0.77-8.454-2.33-12.303c-1.562-3.885-3.848-7.32-6.857-10.33C48.025,3.619,40.385,0.5,31.433,0.5z M31.567,6.259 c7.238,0,13.412,2.566,18.554,7.709c2.477,2.477,4.375,5.31,5.67,8.471c1.296,3.162,1.949,6.518,1.949,10.061 c0,7.354-2.516,13.454-7.506,18.33c-2.592,2.516-5.502,4.447-8.74,5.781c-3.2,1.334-6.498,1.994-9.927,1.994 c-3.468,0-6.788-0.653-9.949-1.948c-3.163-1.334-6.001-3.238-8.516-5.716c-2.515-2.514-4.455-5.353-5.826-8.516 c-1.333-3.199-2.017-6.498-2.017-9.927c0-3.467,0.684-6.787,2.017-9.949c1.371-3.2,3.312-6.074,5.826-8.628 C18.092,8.818,24.252,6.259,31.567,6.259z"/>
</g>
</svg>
This is because you are setting your canvas size to your rendered svg ones.
In your CSS, you probably do resize your svg, which results in a difference between it's computed size and it's natural one.
By default, drawImage(img, dx, dy, dWidth, dHeight) will use the source's width and height as destinationWidth and destinationHeight if these parameters are not passed.
You can check this example showing the same behavior with a raster image :
window.onload = function(){
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var svgSize = inDoc.getBoundingClientRect();
canvas.width = svgSize.width;
canvas.height = svgSize.height;
var img = document.createElement('img');
img.setAttribute('src', inDoc.src);
img.onload = function() {
context.drawImage(img,0,0);
document.body.appendChild(canvas);
};
}
img{ width: 64px; height: 64px}
<img id="inDoc" src="http://lorempixel.com/128/128"/>
And here is a little graphic showing what's happening.
So the solution is just to set your canvas' width and height properties to your img's ones, just like you would do with any other source :
img.onload = function(){
canvas.width = this.width;
canvas.height = this.height;
ctx.drawImage(this, 0,0);
}
and if you want to include some scaling factor :
img.onload = function(){
canvas.width = this.width * scale;
canvas.height = this.height * scale;
ctx.drawImage(this, 0,0, canvas.width, canvas.height);
}
Now, one not related to your actual code but still huge difference between raster images and svg images is that svg width and height can be set to relative units (like %).
Browsers have no direct clue about what it's relative to. (Chrome does a guess, I don't know how, others won't render your image).
So you need to check for this before exporting to a dataURI :
var absoluteUnits = [1,5,6,7,8,9,10];
if (absoluteUnits.indexOf(svg.width.baseVal.unitType)<0) {
svg.setAttribute('width', aboluteWidth);
}
if (absoluteUnits.indexOf(svg.height.baseVal.unitType)<0) {
svg.setAttribute('height', aboluteHeight);
}
Here absoluteWidth and absoluteHeight can be the results of svg.getBoundingClientRect().
Also note that IE9 won't be able to show the img.width and img.height values, so you've got to make a special case for it...
... but since you should have already checked for the absoluteSize, this should not be a problem :
var svg = document.querySelector('svg');
// we'll use a copy to not modify the svg in the document
var copy = svg.cloneNode(true);
var absoluteWidth, absoluteHeight, BBox;
var absoluteUnits = [1,5,6,7,8,9,10];
if (absoluteUnits.indexOf(svg.width.baseVal.unitType)<0) {
BBox = svg.getBoundingClientRect();
absoluteWidth = BBox.width
copy.setAttribute('width', absoluteWidth);
}
else{
absoluteWidth = svg.getAttribute('width');
}
if (absoluteUnits.indexOf(svg.height.baseVal.unitType)<0) {
if(!BBox){
BBox = svg.getBoundingClientRect();
}
absoluteHeight = BBox.height;
copy.setAttribute('height', absoluteHeight)
}
else{
absoluteHeight = svg.getAttribute('height');
}
var svgData = new XMLSerializer().serializeToString(copy);
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var img = document.createElement('img');
img.setAttribute('src', 'data:image/svg+xml;base64,' + btoa(svgData));
img.onload = function() {
// here you set your canvas width and height
canvas.width = this.width || absoluteWidth;
canvas.height = this.height || absoluteHeight;
context.drawImage(img,0,0);
document.body.appendChild(canvas);
};
svg{width: 64px; height:64px; border: 1px solid green;}
canvas{border: 1px solid blue;}
<svg width="128px" height="128px" viewBox="0 0 128 128">
<rect x="20" y="20" width="84" height="84"/>
</svg>

Image on Canvas is drawn larger than original size

On load of my image I:
var img = new Image();
img.src = e.target.result;
var canvas = $('#test-canvas')[0];
$('#test-canvas').width(img.width);
$('#test-canvas').height(img.height);
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
But the image is drawn larger than it's original size? I've tried a few images, same problem.
What's the fix?
jQuery will resize the element through CSS, which won't actually change the canvas internal height and width. This will resize the actual canvas element.
var canvas = document.GetElementById('test-canvas');
canvas.width = img.width;
canvas.height = img.height;
jsFiddle (http://jsfiddle.net/L0drfwgL/) just show it drawing it to scale, and resizing the canvas item itself.
It looks like you are pulling the image from an image element on load. You can use the src from the image element rather than recreating an image object and then get the image width/height from the element to draw the image to canvas.
<script>
$(document).ready(function(){
$('#testImg').on('load',function(){
var image = document.getElementById('testImg');
var canvas = document.getElementById('test-canvas');
canvas.width = image.width;
canvas.height = image.height;
var ctx=canvas.getContext("2d");
ctx.drawImage(image,0,0);
})
});
</script>
/** with vue3 -(to fix drawing canvas image is larger than actual image size **/
const captureImage = () => {
let width = video.value.getBoundingClientRect().width;
let height = video.value.getBoundingClientRect().height;
let context = canvas.value.getContext('2d')
canvas.value.width = width
canvas.value.height = height
context.drawImage(video.value, 0, 0, width, height)
}

Categories

Resources