Convert SVG to PNG and maintain CSS integrity - svg

I am currently using canvg() and Canvas2Image to copy my SVG to a canvas and then convert the canvas to PNG. I would like to maintain the image format and not use PDF.
How can I maintain the CSS integrity? Chart is made using NVD3.js.
downloadPhoto: function() {
var chartArea = document.getElementsByTagName('svg')[0].parentNode;
var svg = chartArea.innerHTML;
var canvas = document.createElement('canvas');
canvas.setAttribute('width', chartArea.offsetWidth);
canvas.setAttribute('height', chartArea.offsetHeight);
canvas.setAttribute('display', 'none');
canvas.setAttribute(
'style',
'position: absolute; ' +
'top: ' + (-chartArea.offsetHeight * 2) + 'px;' +
'left: ' + (-chartArea.offsetWidth * 2) + 'px;');
document.body.appendChild(canvas);
canvg(canvas, svg);
Canvas2Image.saveAsPNG(canvas);
canvas.parentNode.removeChild(canvas);
}

Style definitions for svg elements defined in stylesheets are not applied to the generated canvas. This can be patched by adding style definitions to the svg elements before calling canvg.
Inspired on this article, I've created this:
function generateStyleDefs(svgDomElement) {
var styleDefs = "";
var sheets = document.styleSheets;
for (var i = 0; i < sheets.length; i++) {
var rules = sheets[i].cssRules;
for (var j = 0; j < rules.length; j++) {
var rule = rules[j];
if (rule.style) {
var selectorText = rule.selectorText;
var elems = svgDomElement.querySelectorAll(selectorText);
if (elems.length) {
styleDefs += selectorText + " { " + rule.style.cssText + " }\n";
}
}
}
}
var s = document.createElement('style');
s.setAttribute('type', 'text/css');
s.innerHTML = "<![CDATA[\n" + styleDefs + "\n]]>";
//somehow cdata section doesn't always work; you could use this instead:
//s.innerHTML = styleDefs;
var defs = document.createElement('defs');
defs.appendChild(s);
svgDomElement.insertBefore(defs, svgDomElement.firstChild);
}
// generate style definitions on the svg element(s)
generateStyleDefs(document.getElementById('svgElementId'));

The key thing here is that all the style rules need to be part of the SVG, not in external style files. So you would need to go through all the CSS for NVD3 and set all of those attributes in the code. Anything that is set via an external stylesheet will be ignored.

just to make #Lars Kotthoff's answer more concrete. "example of how to export a png directly from an svg" has a working example. the code snippet/gist tries to first apply all css to the svg inline and then draw the image on the canvas and export the data as png. (internally it adopted svg-crowbar code). and i apply the technique in my project and it works smoothly - a download button that can download the svg image rendered using nvd3.

Related

jquery: fancybox 3, responsive image maps and zoomable content

I want to use image-maps inside fancybox 3. Goal is to display mountain panoramas, where the user could point on a summit and get name and data. The usual recommendation is to use a SVG based image map for this like in this pen. Due to the size of the images the fancybox zoom functionality is important.
While fancybox will display SVGs as an image like in this pen it is not possible to use the <image> tag with an external source inside the SVG file. Even worse: SVG files used as source of an <img> tag would not show the map functionality (see this question).
I tried to replace the <img> tag in fancybox with an <object> tag using the SVG file as data attribute. This shows the image with the map functionality correctly but fancybox won't zoom it any more.
So eventually the question boils down to how I can make an <object> (or an inline SVG or an iframe) zoomable just like an image in fancybox 3.
I'm open to other solutions as well. I only want to use fancybox to keep the appearance and usage the same as other image galleries on the same page. I'd even use an old style <map>, where I would change the coords using jquery to have it responsive. I tried that, attaching the map manually in developer tools as well as programmatically in afterLoad event handler, but apparently this doesn't work in fancybox 3 either.
The areas are polygons, so using positioned div's as overlays is no solution.
Edit: I just discovered that I can replace <img> with a <canvas> in fancybox without loosing the zoom and drag functionality. So in theory it would be possible to use canvas paths and isPointInPath() methode. Unfortunately I need more than one path, which requires the Path2D object, which is not available in IE...
Since all options discussed in the question turned out to be not feasible and I found the pnpoly point in polygon algorithm, I did the whole thing on my own. I put the coordinates as percentages (in order to be size-independent) in an array of javascript objects like so:
var maps = {
alpen : [
{type:'poly',name:'Finsteraarhorn (4274m)',vertx:[56.48,56.08,56.06,56.46], verty:[28.5,28.75,40.25,40.25]},
{type:'rect',name:'Fiescherhörner (4049m)',coords:[58.08,29.5,59.26,43.5]},
{type:'poly',name:'Eiger (3970m)',vertx:[61.95,61.31,61.31,60.5,60.5], verty:[43,35.25,30.25,30.25,45.5]}
]
}; // maps
Since the pnpoly function requires the vertices for x and y separately I provide the coordinates this way already.
The Id of the map is stored in a data attribute in the source link:
<a href="/img/bilder/Alpen.jpg" data-type='image' data-Id='alpen' data-fancybox="img" data-caption="<h5>panorama of the alps from the black forest Belchen at sunset</h5>">
<img src="/_pano/bilder/Alpen.jpg">
</a>
CSS for the tooltip:
.my-tooltip {
color: #ccc;
background: rgba(30,30,30,.6);
position: absolute;
padding: 5px;
text-align: left;
border-radius: 5px;
font-size: 12px;
}
pnpoly and pnrect are provided as simple functions, the handling of that all is done in the afterShow event handler:
// PNPoly algorithm checkes whether point in polygon
function pnpoly(vertx, verty, testx, testy) {
var i, j, c = false;
var nvert = vertx.length;
for(i=0, j=nvert-1; i<nvert; j=i++) {
if (((verty[i] > testy) != (verty[j] > testy)) &&
(testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i])) {
c = !c;
}
}
return c;
}
// checks whether point in rectangle
function pnrect(coords,testx,testy) {
return ((testx >= coords[0]) && (testx <= coords[2]) && (testy >= coords[1]) && (testy <= coords[3]));
}
$("[data-fancybox]").fancybox({
afterShow: function( instance, slide ) {
var map = maps[$(slide.opts.\$orig).data('id')]; // Get map name from source link data-ID
if (map && map.length) { // if map present
$(".fancybox-image")
.after("<span class='my-tooltip' style='display: none'></span>") // append tooltip after image
.mousemove(function(event) { // create mousemove event handler
var offset = $(this).offset(); // get image offset, since mouse coords are global
var perX = ((event.pageX - offset.left)*100)/$(this).width(); // calculate mouse coords in image as percentages
var perY = ((event.pageY - offset.top)*100)/$(this).height();
var found = false;
var i;
for (i = 0; i < map.length; i++) { // loop over map entries
if (found = (map[i].type == 'poly') // depending on area type
?pnpoly(map[i].vertx, map[i].verty, perX, perY) // look whether coords are in polygon
:pnrect(map[i].coords, perX, perY)) // or coords are in rectangle
break; // if found stop looping
} // for (i = 0; i < map.length; i++)
if (found) {
$(".my-tooltip")
.css({bottom: 'calc(15px + '+ (100 - perY) + '%'}) // tooltip 15px above mouse coursor
.css((perX < 50) // depending on which side we are
?{right:'', left: perX + '%'} // tooltip left of mouse cursor
:{right: (100 - perX) + '%', left:''}) // or tooltip right of mouse cursor
.text(map[i].name) // set tooltip text
.show(); // show tooltip
} else {
$(".my-tooltip").hide(); // if nothing found: hide.
}
});
} else { // if (map && map.length) // if no map present
$(".fancybox-image").off('mousemove'); // remove event mousemove handler
$(".my-tooltip").remove(); // remove tooltip
} // else if (map && map.length)
} // function( instance, slide )
});
Things left to do: Find a solution for touch devices, f.e. provide a button to show all tooltips (probably rotated 90°).
As soon as the page is online I'll provide a link here to see it working...

How to get char code of fontawesome icon?

I'd like to use fontawesome icons in SVG scope. I cannot achieve it in common way, but I can add <text> element containing corresponding UTF-8 char and with font set to fontawesome, like that:
<text style="font-family: FontAwesome;">\uf0ac</text>
To make it clear I wrote a switch for getting useful icons:
getFontAwesomeIcon(name) {
switch (name) {
case 'fa-globe':
return '\uf0ac'
case 'fa-lock':
return '\uf023'
case 'fa-users':
return '\uf0c0'
case 'fa-ellipsis-h':
return '\uf141'
default:
throw '# Wrong fontawesome icon name.'
}
}
But of course that's ugly, because I must write it myself im my code. How can I get these values just from fontawesome library?
You can avoid producing such a list and extract the information from the font-awesome stylesheet on the fly. Include the stylesheet and set the classes like usual, i. e.
<tspan class="fa fa-globe"></tspan>
and you can do the following:
var icons = document.querySelectorAll(".fa");
var stylesheet = Array.from(document.styleSheets).find(function (s) {
return s.href.endsWith("font-awesome.css");
});
var rules = Array.from(stylesheet.cssRules);
icons.forEach(function (icon) {
// extract the class name for the icon
var name = Array.from(icon.classList).find(function (c) {
return c.startsWith('fa-');
});
// get the ::before styles for that class
var style = rules.find(function (r) {
return r.selectorText && r.selectorText.endsWith(name + "::before");
}).style;
// insert the content into the element
// style.content returns '"\uf0ac"'
icon.textContent = style.content.substr(1,1);
});
My two answers for two approaches to the problem (both developed thanks to ccprog):
1. Setting char by class definition:
In that approach we can define element that way:
<text class="fa fa-globe"></text>
And next run that code:
var icons = document.querySelectorAll("text.fa");
// I want to modify only icons in SVG text elements
var stylesheets = Array.from(document.styleSheets);
// In my project FontAwesome styles are compiled with other file,
// so I search for rules in all CSS files
// Getting rules from stylesheets is slightly more complicated:
var rules = stylesheets.map(function(ss) {
return ss && ss.cssRules ? Array.from(ss.cssRules) : [];
})
rules = [].concat.apply([], rules);
// Rest the same:
icons.forEach(function (icon) {
var name = Array.from(icon.classList).find(function (c) {
return c.startsWith('fa-');
});
var style = rules.find(function (r) {
return r.selectorText && r.selectorText.endsWith(name + "::before");
}).style;
icon.textContent = style.content.substr(1,1);
});
But I had some problems with that approach, so I developed the second one.
2. Getting char with function:
const getFontAwesomeIconChar = (name) => {
var stylesheets = Array.from(document.styleSheets);
var rules = stylesheets.map(function(ss) {
return ss && ss.cssRules ? Array.from(ss.cssRules) : [];
})
rules = [].concat.apply([], rules);
var style = rules.find(function (r) {
return r.selectorText && r.selectorText.endsWith(name + "::before");
}).style;
return style.content.substr(1,1);
}
Having that funcion defined we can do something like this (example with React syntax):
<text>{getFontAwesomeIconChar('fa-globe')}</text>

How to draw custom dynamic billboards in Cesium.js

I'm currently using Cesium for a mapping application, and I have a requirement to have status indicators for each item I'm plotting (for example, if the item I'm plotting is an airplane, then I need to have a fuel status indicator). I can't use Cesium's drawing tools to do this because they are drawn using geographic locations, but I need my status indicators to simply be located near the billboard and not get farther away from the billboard as users zoom in and out.
Cesium's CZML documentation states that the billboard's "image" property can take a data URI, so I figured the easiest way to handle this would be for me to create an SVG path on the fly and embed it in the image property, but when I do this in Cesium, it does not show up. For example, I tried a simple test like this:
"data:image/svg+xml,<svg viewBox='0 0 40 40' height='25' width='25'
xmlns='http://www.w3.org/2000/svg'><path fill='rgb(91, 183, 91)' d='M2.379,
14.729L5.208,11.899L12.958,19.648L25.877,6.733L28.707,9.561L12.958,25.308Z'
/></svg>"
When that didn't show up, I tried just simple HTML and text values, like this:
"data:,Hello%2C%20World!"
and:
"data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E"
but those don't show up either. I am able to get a png to show up if I put the base64 string in the data URI, as well as a path to an image stored on the server, but I really need to be able to draw custom images on the fly. I can't use a fixed set of pre-generated images set with various statuses as a hack (I can explain why if anyone wants those details :) ).
Does anyone know if there's something I'm doing wrong here, or if there is another way to accomplish what I need to do?
Edit Just wanted to add that I am using Firefox version 29 and it normally has no problem displaying the non-encoded embedded SVGs like that. Just in case, that's one of the reasons I was also trying simple HTML or text.
Edit2 I am using CZML streaming from the back end to plot my items, here is a simple test example showing where I am trying to put the image information:
{
"id":"test",
"billboard" : {
"image" : "data:image/svg+xml,<svg viewBox='0 0 40 40' height='25' width='25' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(91, 183, 91)' d='M2.379,
14.729L5.208,11.899L12.958,19.648L25.877,6.733L28.707,9.561L12.958,25.308Z'
/></svg>",
"show" : [ {"boolean" : true} ]
},
"position":{
"cartographicDegrees":[0.0, 0.0, 0.0]
},
"label":{"text":"TEST"},
}
If I put a base64 png string in there, or a path to a static image file, it works fine.
Thank you!
Simple JS code to insert SVG into cesium
// create the svg image string
var svgDataDeclare = "data:image/svg+xml,";
var svgCircle = '<circle cx="10" cy="10" r="5" stroke="black" stroke-width="3" fill="red" /> ';
var svgPrefix = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="40px" height="40px" xml:space="preserve">';
var svgSuffix = "</svg>";
var svgString = svgPrefix + svgCircle + svgSuffix;
// create the cesium entity
var svgEntityImage = svgDataDeclare + svgString;
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(-80.12, 25.46),
billboard: {
image: svgEntityImage
}
});
// test the image in a dialog
$("#dialog").html(svgString );
$("#dialog").dialog({
position: {my: "left top", at: "left bottom"}
});
My method to accomplish this is to create a canvas element, add text to it, crop it and convert that result into a data URL. It works great and it's quick.
I'm not using CZML, but this is how I did it:
buildImage:function(text,text2){
var self = this;
var canvas = new Element('canvas',{width:500,height:500});
var ctx = canvas.getContext("2d");
ctx.font = "bold 16px monospace";
ctx.fillText(text, 60, 20);
ctx.fillText(text2, 60, 50);
imagedata = self.cropCanvas(canvas,ctx);
return imagedata;
},
cropCanvas:function(canvas,ctx){
ww = canvas.width;
wh = canvas.height;
imageData = ctx.getImageData(0, 0, ww, wh);
var topLeftCorner = {};
topLeftCorner.x = 9999;
topLeftCorner.y = 9999;
var bottomRightCorner = {};
bottomRightCorner.x = -1;
bottomRightCorner.y = -1;
for (y = 0; y < wh; y++) {
for (x = 0; x < ww; x++) {
var pixelPosition = (x * 4) + (y * wh * 4);
a = imageData.data[pixelPosition+3]; //alpha
if (a > 0) {
if (x < topLeftCorner.x) {
topLeftCorner.x = x;
}
if (y < topLeftCorner.y) {
topLeftCorner.y = y;
}
if (x > bottomRightCorner.x) {
bottomRightCorner.x = x;
}
if (y > bottomRightCorner.y) {
bottomRightCorner.y = y;
}
}
}
}
topLeftCorner.x -= 2;
topLeftCorner.y -= 2;
bottomRightCorner.x += 2;
bottomRightCorner.y += 2;
relevantData = ctx.getImageData(topLeftCorner.x, topLeftCorner.y, bottomRightCorner.x -topLeftCorner.x, bottomRightCorner.y - topLeftCorner.y);
canvas.width = bottomRightCorner.x - topLeftCorner.x;
canvas.height = bottomRightCorner.y - topLeftCorner.y;
ww = canvas.width;
wh = canvas.height;
ctx.clearRect(0,0,ww,wh);
ctx.putImageData(relevantData, 0, 0);
return canvas.toDataURL();
}
These are two methods of a MooTools class, but can be easily rewritten into whatever framework (or no framework) you need.
Drawing an SVG does work as I expected it to, but I believe I may have just been getting one of the size elements wrong (height/width or x, y). Whenever those values don't match up just right, the image isn't shown because it's outside of the view area I've defined for it.
Note that I never did get the simple html example work, but that's not what I needed anyway, so I didn't pursue it further.

Adding background image to markers in jvectormap

Found a couple of solutions here about adding SVG patterns dynamically but it doesn't seem to work with jvectormap. I think the problem may be that there is no XMLNS attribute defined on the <SVG> tag by jvectormap but my attempt to add these attributes does not work.
I also tried changing all of the setAttribute to setAttributeNS for pattern and image. But no dice.
Here is my attempt (based on this solution: How to dynamically change the image pattern in SVG using Javascript):
// Set namespace for SVG elements.
var svgMap = $('.jvectormap-container > svg').get(0);
var svgNS = 'http://www.w3.org/2000/svg';
var svgNSXLink = 'http://www.w3.org/1999/xlink';
svgMap.setAttribute('xmlns', svgNS);
svgMap.setAttribute('xmlns:link', svgNSXLink);
svgMap.setAttribute('xmlns:ev', 'http://www.w3.org/2001/xml-events');
// Create pattern for markers.
var pattern = document.createElementNS(svgNS, 'pattern');
pattern.setAttribute('id', 'markeryellow');
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
pattern.setAttribute('width', '38');
pattern.setAttribute('height', '38');
// Create image for pattern.
var image = document.createElementNS(svgNS, 'image');
image.setAttribute('x', '0');
image.setAttribute('y', '0');
image.setAttribute('width', '38');
image.setAttribute('height', '38');
image.setAttributeNS(svgNSXLink, 'xlink:href', '/path/to/image.png');
// Put it together
pattern.appendChild(image);
var defs =
svgMap.querySelector('defs') ||
svgMap.insertBefore(document.createElementNS(svgNS, 'defs'), svgMap.firstChild);
defs.appendChild(pattern);
var $markers = $(svgMap).find('.jvectormap-marker');
$.each($markers, function(i, elem) {
$(elem)
.attr({
'fill': 'url(#markeryellow)'
});
});

Display Size/Width Adjustment

I was wandering if there is a way to adjust width of the math mathjax renders. Some math expression I have are longer and won't fit in a box I have created. Is there a way to squeeze it and make it fit maybe by changing the size or width? I have tried using line breaks but that isn't what I want. An example would be a mathjax like this:
2x+3+4 - /intcos(x) dx
234567897+sin(2x)+34567890987654.
Displaying the last line would be a problem because it won't fit in the box. It overflows
Well, you could use \small or \scriptsize or \Tiny (non-standard) or \tiny within the mathematics to make it appear in a smaller size.
Alternatively, you could put a <span style="font-size:70%">...</span> around the mathematics to get the math to be scaled to whatever size you need. E.g.,
<span style="font-size:70%">\(234567897+sin(2x)+34567890987654\)</span>
Note that the math delimiters must be inside the <span>.
I found a solution that doesn't require adding elements or css code:
// resize all LaTeX Display elements to they fit in on screen
function cvonk_ResizeMathJax() {
jQuery('.MathJax_Display').each(function(ii, obj) {
var latex = obj.children[0];
var w = latex.offsetWidth;
var h = latex.offsetHeight;
var W = obj.offsetWidth;
if (w > W) {
obj.style.fontSize = 95 * W / w + "%";
}
});
}
window.MathJax = {
AuthorInit: function() {
MathJax.Hub.Register.StartupHook("Begin", function() {
MathJax.Hub.Queue(function() {
cvonk_ResizeMathJax();
});
});
},
jax: ["input/TeX", "output/HTML-CSS", "output/NativeMML"],
extensions: ["tex2jax.js"]
};
window.addEventListener("resize", function() {
cvonk_ResizeMathJax();
});
From the Google Groups discussion linked to above:
function changeSize(button) {
var myeqn = document.getElementById('myeqn');
myeqn.style.fontSize = button.textContent;
MathJax.Hub.Queue(
['Rerender', MathJax.Hub, 'myeqn'],
function () {
document.getElementById('mylabel').innerHTML =
'width: ' + myeqn.offsetWidth + ", height: " + myeqn.offsetHeight;
});
}
See http://jsfiddle.net/nLyraL1f/ or http://jsfiddle.net/s2bjepk6/.
This is also nice because it gets the width and height of the rendered latex, useful for things like rendering it as an element positioned over a canvas since you can draw things on the canvas around it.

Resources