Font style of text in svg not propagating to png on download - javascript

I am generating an svg, with text, using D3.js for a user to download.
Unfortunately, when an svg is downloaded, the font family applied to the text on the svg does not appear to be included when the svg is converted to canvas, then downloaded as a png.
The font styling appears to be missing.
Is there a way to construct the png with this font family applied?
To download the svg as a png, I adapted the answer described here.
Here is my process:
First, I create the svg.
await d3.xml(`img_templates/${template}_${dimensions}.svg`)
.then(data => {
divsvg.append(data.documentElement) //Append svg to DOM
});
Then, I add my text elements to the svg
const quoteText = quoteGroup.append("text")
.attr("class", "quote")
.style("font-size", `15px`)
.style("font-family", 'Merriweather')
Merriweather is a Google font family that I load in the head of a html page like so:
<head>
<link href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght#0,700;1,400&display=swap" rel="stylesheet"> <!--Another font I use-->
<script src="d3.min.js"></script>
<script src=genImage.js></script> <!--The js code to interact with the svg -->
</head>
On download, I execute the following functions on the svg,
function downloadSVG(svgObj){
var data = (new XMLSerializer()).serializeToString(svgObj);
var DOMURL = window.URL || window.webkitURL || window;
var img = new Image();
var svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
var url = DOMURL.createObjectURL(svgBlob);
img.onload = function () {
var canvas = document.createElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
DOMURL.revokeObjectURL(url);
var imgURI = canvas
.toDataURL('image/png')
.replace('image/png', 'image/octet-stream');
// console.log(imgURI)
triggerDownload(imgURI);
};
img.src = url;
}
function triggerDownload (imgURI) {
var evt = new MouseEvent('click', {
view: window,
bubbles: false,
cancelable: true
});
var a = document.createElement('a');
a.setAttribute('download', 'MY_COOL_IMAGE.png');
a.setAttribute('href', imgURI);
a.setAttribute('target', '_blank');
a.dispatchEvent(evt);
}
When serialized to string the svg appears as such:
<svg xmlns="http://www.w3.org/2000/svg" width="484" height="484" viewBox="0 0 484 484" fill="none">
<g class="quoteGroup" transform="translate(42,100)">
<text class="quote" fill="#FFFFFF" style="font-size: 25px; font-family: Merriweather;"><tspan x="0">"I’m staying alone in Montauk at a</tspan><tspan x="0" dy="40px">friend’s house while he’s in Portugal</tspan><tspan x="0" dy="40px">(something about taxes and quality of</tspan><tspan x="0" dy="40px">life). After my morning coffee and</tspan><tspan x="0" dy="40px">power shake, it’s time to water the</tspan><tspan x="0" dy="40px">plants."</tspan></text>
</g>
<g class="headlineGroup" transform="translate(42,365)">
<text class="headline" font-size="15px" fill="#FFFFFF" style="font-family: Roboto; font-style: italic;"><tspan x="0">Stop Thinking of the Breakup of Big Tech as Punishment</tspan></text>
</g>
<g class="publisherGroup" transform="translate(42,430)">
<text class="publisher" font-size="15px" fill="#FFFFFF" style="font-family: Roboto; font-weight: bold;">Marker</text>
</g>
<g class="dateGroup" transform="translate(353,430)"><text class="date" font-size="15px" fill="#FFFFFF" style="font-family: Roboto; font-weight: bold;">Aug 04, 2020</text></g>
</svg>
From what I can tell, the font family is being correctly specified in the string, yet when the svg is converted to canvas and then png, it is being lost. Why might this happen?
Please let me know if I can provide any additional information. Any assistance is greatly appreciated!

To anybody who finds this with the same problem, I was able to figure it out.
Thanks to Mr. Longson's suggestion, I found the following answer by Kaiido on this question.
Kaiido's GFontToDataURI method worked perfectly. I incorporated it into my code like so:
GFontToDataURI("https://fonts.googleapis.com/css2?family=Merriweather&display=swap")
.then(cssRules => {
let fontRules = cssRules.join('\n')
d3.select("svg").append('defs').append('style').attr('type', 'text/css').text(fontRules)
console.log("Added Merriweather")
})
.catch(reason => console.log(reason))
GFontToDataURI("https://fonts.googleapis.com/css2?family=Roboto:ital,wght#0,700;1,400&display=swap")
.then(cssRules => {
let fontRules = cssRules.join('\n')
d3.select("svg").append('defs').append('style').attr('type', 'text/css').text(fontRules)
console.log("Added roboto")
})
.catch(reason => console.log(reason))

Related

color of svg not appearing in png version

so I have a style in the svg image and I am using that to change the fill using javascript with a random colors function and that works fine. but when I need the svg to be in png format after the color is changed, the changed colors colors are not retained for the PNG but the original colors are kept instead
<style type="text/css">
:root{
--back:#662D91;
--shadow:0.28;
--highlight:#F3B4AE;
--stomach:#EA8A82;
--lightHigh:#FEE9E5;
--lasso:#9B5D12;
--gloves:#29A6DE;--glovesStroke:#000674;--gloves2:#29A6DE;--gloves3:#1C7FC9;--gloves4:#1D7DC6;--gloves5:#1C7DC4;
--hatBelow:#DB8556;
--hatBelow2:#8A3C13;
--hatInner:#8C4017;
--hatInner2:#934A24;
--hatInner3:#9D5C3A;
--stripe:#8A7454;
--stripeEnd:#382000;
}
.st0{fill:var(--back);}
.st1{opacity:var(--shadow);}
.st2{fill:var(--highlight);}
.st3{fill:var(--stomach);}
.st4{fill:var(--lightHigh);}
.st5{fill:var(--lasso);}
.st6{filter:url(#Adobe_OpacityMaskFilter);}
.st7{filter:url(#Adobe_OpacityMaskFilter_1_);}
.st8{clip-path:url(#SVGID_3_);mask:url(#SVGID_4_);fill:url(#SVGID_5_);}
.st9{opacity:0.7;clip-path:url(#SVGID_3_);fill:url(#SVGID_6_);}
.st10{fill:url(#SVGID_10_);}
.st11{fill:#FFD2B3;}
.st12{fill:#EEBD9C;}
.st13{fill:#FFE3CE;}
.st14{fill:#AF7B6E;}
.st15{fill:var(--gloves);stroke:var(--glovesStroke);stroke-miterlimit:10;}
.st16{fill:var(--gloves2);}
.st17{fill:var(--gloves3);}
.st18{fill:var(--gloves4);}
.st19{fill:var(--gloves5);}
</style>
this is the style element in my svg and I am changing it using this
root.style.setProperty('--back',colors[0]);
the colors is an array of colors that I get my randomly making a color using a script in the svg
help plz
so this is how i am converting svg to png
<script>
var svgString = new XMLSerializer().serializeToString(document.querySelector('svg'));
var canvas = document.getElementById("canvas");
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");
document.querySelector('#png-container').innerHTML = '<img src="'+png+'"/>';
DOMURL.revokeObjectURL(png);
};
img.src = url;
</script>
Edit: Took a minute to thing about it, and it makes sense that it doesn't work. svg blob only includes svg tag contents and hence has no idea there's a CSS variable set on html tag outside of it. Styles applied with :root are defined within SVG, so when you yank it out of HTML context, I assume :root points to the svg tag itself.
Not sure why it works this way, but when using canvas CSS variables set on ancestors of svg tag seem to be ignored. What you can do to fix this is set inline styles on either svg tag itself or its children.
Here's a reproduction with a fix, don't mind changes to the JS code, it does the exact same thing yours does just but sets CSS variable on svg tag instead of html.
<!DOCTYPE html>
<html lang="en">
<head>
<title>SVG thingy</title>
</head>
<body>
<svg
viewBox="0 0 10 10"
xmlns="http://www.w3.org/2000/svg"
>
<style>
:root {
--color: red;
}
circle {
fill: var(--color);
}
</style>
<circle
cx="5"
cy="5"
r="4"
/>
</svg>
<canvas></canvas>
<div id="png-container"></div>
<script>
const root = document.documentElement; // html tag
// root.style.setProperty('--color', 'black'); // Doesn't work
const svgElement = document.querySelector('svg');
svgElement.style.setProperty('--color', 'black'); // Works
const svgString = new XMLSerializer().serializeToString(document.querySelector('svg'));
const svgBlob = new Blob([svgElement.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob);
const canvasElement = document.querySelector('canvas');
const ctx = canvasElement.getContext('2d');
const img = new Image();
img.src = url;
img.onload = () => {
ctx.drawImage(img, 0, 0);
const png = canvasElement.toDataURL('image/png');
document.querySelector('#png-container').innerHTML = '<img src="' + png + '"/>';
URL.revokeObjectURL(png);
};
</script>
</body>
</html>

Loading javascript libraries included in an appended SVG file

I am trying to build a browser-based SVG rasterizer. The SVG files can contain javascript code that affects the output (for example, randomly changes colors of elements) and use libraries (for example chroma.js) to do that.
The process I am trying to implement is:
load the SVG file
load libraries linked in the SVG file
execute the javascript included inside script tags
display on a canvas
save the canvas as PNG
This is necessary because appending the SVG to the HTML element does not run the JS included inside SVG.
Everything works well - except the loading of external libraries (point 2). The snippet that does that is as follows
$.get('/some_svg_file.svg', function(d) {
// this will be replaced with file drag/drop later
var getLibs = Array.from(d.querySelectorAll('script'))
.filter(p => p.getAttribute('xlink:href'))
.map(p => {
return axios.get(p.getAttribute('xlink:href'))
})
// only run embedded js after libraries have been downloaded
Promise.all(getLibs).then((values) => {
values.forEach(d => {
try {
eval(d.data);
} catch (e) {
console.log(e)
}
});
Array.from(d.querySelectorAll('script'))
.forEach(p => {
if (p.textContent.length) {
try {
eval(p.textContent)
} catch (e) {
console.log(e)
}
}
})
// code that writes to canvas etc goes here
})
})
So, if I put link chroma.js in the html file header, everything works ok. If I download it and eval it, the scripts inside the SVG file fail with ReferenceError: chroma is not defined
Another attempt I did was using the script tag technique like so:
$.get('/foo_with_js.svg', function(d) {
var getLibs = Array.from(d.querySelectorAll('script'))
.filter(p => p.getAttribute('xlink:href'))
.map(p => {
var s = document.createElement('script');
s.src = p.getAttribute('xlink:href');
s.type = "text/javascript";
s.async = false;
document.querySelector('head').appendChild(s);
})
// load scripts and save to canvas
})
which also failed in the same way (despite chroma.js being neatly included in the head section.
So how could I get this to work - so that I can load SVG, append it to HTML and run the scripts inside it without having to pre-link all the possible scripts in the HTML?
Ah - and if you're asking why not use some other conversion process, the answer is "lack of consistent support for SVG filters"
If you can make this work with any JavaScript library embedded in SVG is a good question, but here I have an example where I use chroma.js like you suggested.
A reference to the SVG document is added to the data attribute of <object>. Here it is static, but I guess it could be dynamically updated. Now the SVG is doing its thing, like setting a color on the <rect> and animating the cy attribute of the <circle>. At any point I can grab the contentDocument (contentDocument.documentElement.outerHTML) of <object>, that is the SVG document at a particular point. If values in the document is changing this will affect the contentDocument. Now I can take the string (outerHTML) and turn that into a data URI. The data URI can be set as src on an image object and the image can be rendered in the <canvas> element. In the outerHTML you can see that there are also references to the JavaScript library and the JavaScript code that I wrote, but this is not executed in the image when rendered into <canvas>.
SVG document:
<?xml version="1.0"?>
<svg viewBox="0 0 200 200" width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="drop-shadow">
<feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur"/>
<feoffset in="blur" dx="4" dy="4" result="offsetBlur"/>
<feMerge>
<feMergeNode in="offsetBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<script href="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.1.2/chroma.min.js"/>
<rect x="0" y="0" width="200" height="200" />
<circle cx="40" cy="40" r="20" fill="blue" style="filter: url(#drop-shadow);"/>
<script>
// <![CDATA[
var cy = 40;
var speed = 5;
document.querySelector('rect').style.fill = chroma('hotpink');
setInterval(function(){
if(cy > 160 || cy < 40) speed *= -1;
cy += speed;
document.querySelector('circle').attributes['cy'].value = cy;
}, 500);
// ]]>
</script>
</svg>
HTML document:
<!DOCTYPE html>
<html>
<head>
<title>SVG</title>
<script type="text/javascript">
var object, canvas, img, loop;
document.addEventListener('DOMContentLoaded', e => {
object = document.querySelector('object');
canvas = document.querySelector('canvas');
img = new Image();
img.addEventListener('load', e => {
canvas.getContext('2d').drawImage(e.target, 0, 0);
});
object.addEventListener('load', e => {
loop = setInterval(function(){
var svg64 = btoa(e.target.contentDocument.documentElement.outerHTML);
var b64Start = 'data:image/svg+xml;base64,';
img.src = b64Start + svg64;
}, 500);
});
});
</script>
</head>
<body>
<object type="image/svg+xml" width="200" height="200" data="test.svg"></object>
<canvas width="200" height="200"></canvas>
</body>
</html>
These two documents need to run from a server, so not directly from the filesystem.
From what I have tested the SVG animations and CSS animations are not working in this setup. Only the "animations" in JavaScript where you manipulate the DOM is working.

Not able to generate png from from svg because of transform attribute by using angular5 and javascript

I am not able to generate png from svg created in the application because of transform attribute. If there is no value in transformation, I am getting the svg. This problem I am facing in IE11.
let targetElem = <any>document.getElementById('body');
let nodesToRecover = [];
let nodesToRemove = [];
let svgElem = targetElem.querySelector('svg#svg-annotations')
let parentNode = svgElem.parentNode;
// let svg = parentNode.innerHTML.trim();
let svg = '<svg width="100" height="100"> <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" transform="translate(295 157)" fill="yellow" /> </svg>';
this.canvas = document.createElement('canvas');
this.canvas.setAttribute('id', '_canvas' + this.counter);
canvg(this.canvas, svg);
parentNode.appendChild(this.canvas);
let data = this.canvas.toDataURL('image/png', 0.5);
console.log(data);
let blobData = this.base64toBlob(data.split(',')[1], 'image/png');
saveAs(blobData, 'myblob.png');
console.log(blobData);
this.counter++;
Can anyone help me how to resolve this problem with transform? Any help will be much appraciated.
this issue is resolved by putting width and height to the required value. the problem was, translate property value is 297, 195 which is more than width and height given.

Including fonts when converting SVG to PNG

I am trying to generate some SVG and allow users of my website to download these SVGs as PNGs.
After reading this I get all my external images included in the downloaded PNG.
Now I am trying to get the fonts on the PNG correct. This seems to answer that and so I added:
<defs>
<style type="text/css">
#font-face {
font-family: Parisienne;
src: url('data:application/font-woff;charset=utf-8;base64,.....')
}
</style>
</defs>
Where ..... is base64 encoded woff2 font. And then used it in text like so:
<text x="55" y="55" stroke="red" font-family="Parisienne">Blah</text>
The font gets displayed in the browser correctly (I haven't installed it on my OS), however it is still not included in the PNG.
Do I have to add some additional processing to the script I used from the first link?
Thanks.
--EDIT--
I have been asked for a complete example, here it is:
<svg id="generated-svg" class="generated-svg" width="300px" height="500px"
version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns-xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
#font-face {
font-family: Parisienne;
src: url('data:application/font-woff;charset=utf-8;base64,.....')
}
</style>
</defs>
<rect width="300" height="500" fill="#222"/>
<text x="55" y="55" stroke="red" font-family="Parisienne" font-size="20px">Test text</text>
</svg>
I haven't added the base64 encoded font as it's simply too big. But you can encode any font you like and replace the ....... I am using Parisienne.
Here is working jsfiddle with the actual font: https://jsfiddle.net/z8539err/
In my browser this is the output:
Whilst after using the download script above I would end up with:
I'm able to include the font in the png itself with the following code, give it a try
var svg = document.getElementById('generated-svg');
var svgData = new XMLSerializer().serializeToString( svg );
var canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 500;
var ctx = canvas.getContext("2d");
//display image
var img = document.createElement( "img" );
img.setAttribute( "src", "data:image/svg+xml;base64," + btoa( svgData ) );
img.onload = function() {
ctx.drawImage( img, 0, 0 );
//image link
console.log( canvas.toDataURL( "image/png" ) );
//open image
window.location.href=canvas.toDataURL( "image/png" );
};
https://jsfiddle.net/user3839189/hutvL4ks/1/
A working proof of concept project on GitHub to address OP problem statement.
https://github.com/nirus/SVG-PNG-Convert
Built generic Javascript module that can be modified and used anywhere. Read the documentation. Give it a try!

How to map polygon in JavaScript

Please can you help me? I´m looking for a way, how to map a polygon in canvas with image file (.png or .jpg) but I´m quite confused and I don´t know how to do it... Can you tell me or give a link to a tutorial how to map a polygons in canvas with images? Thank you very much
Ok, I think I understand your question now.
You want to draw text a curved path...
Good news/Bad news:
Canvas cannot do this directly.
However you can use Html SVG to do it.
You can use a free program like Inkscape to design your logo.
Then just save it in SVG format (myLogo.svg).
Then you can load it into canvas like this:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var img=new Image();
img.onload=function(){
ctx.drawImage(img,0,0);
}
img.src="http://mySite.com/myLogo.svg";
Here is a quick demo of the kind of SVG you can create to fit your needs.
And here is a Fiddle: http://jsfiddle.net/m1erickson/v4EX3/
<!doctype html>
<html>
<head>
<style>
body{ background-color: ivory; }
div{float:left;}
</style>
</head>
<body>
<div>
<svg viewBox = "0 0 200 120">
<defs>
<path id = "curvedPath" d = "M 10,90 Q 100,15 200,70 Q 340,140 400,30"/>
</defs>
<g fill = "white">
<use x = "0" y = "0" xlink:href = "#curvedPath" stroke = "black" stroke-width="40" fill = "none"/>
<text font-size = "20">
<textPath xlink:href = "#curvedPath">
Use SVG to put your text on a curved path like this!
</textPath>
</text>
</g>
</svg>
</div>
</body>
</html>

Categories