Integrate browser SVG-to-PNG conversion in a Plotly/Dash app - svg

I have a multipage Dash app that generates some SVG graphics which are also converted to PNG to be inserted into markdown and ODT/LibreOffice documents. Currently I use cairosvg for conversion but it needs more than 400MB of installed software, which isn't good for a docker container.
Looking for an alternative I stumbled upon:
Save inline SVG as JPEG/PNG/SVG
https://mybyways.com/blog/convert-svg-to-png-using-your-browser
https://stackoverflow.com/a/40122418/694360
They all describe a way to convert SVG to PNG through the browser, a solution that seems very smart to me since the SVG source code generated by my app is already natively and perfectly rendered by the browser in a Dash html.Iframe.
Here is the code from the above mentioned links shrinked to the bone, if you open it in a browser you will see how it converts the provided SVG source to PNG, that's few lines of code running in client's browser instead of 400MB of software installed on server:
<html>
<head>
<title>Browser SVG to PNG Converter</title>
</head>
<body bgcolor="#DDDDEE">
<h1>Browser SVG to PNG Converter</h1>
<div id="div_source"><svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"><style>rect,path {fill:none; stroke:#000000;} text {fill:#000000; font-family:sans-serif; font-size:12px; stroke-width:0;} text.fascia {fill:#0000FF; font-size:10px;} line.fascia {stroke:#0000FF;} text.colmo {fill:#FF00FF; font-size:10px;} line.colmo {stroke:#FF00FF;}</style><path d="M 25.0 475.0 h 50.0 l -10.0 5.0 10.0 -5.0 -10.0 -5.0"/><text x="50.0" y="488.64" text-anchor="middle" transform="rotate(0 50.0 485.0)">X</text><path d="M 25.0 475.0 v -50.0 l 5.0 10.0 -5.0 -10.0 -5.0 10.0"/><text x="15.0" y="453.64" text-anchor="middle" transform="rotate(0 15.0 450.0)">Y</text><g transform="translate(250.0 250.0)"><rect x="-110.0" y="-110.0" width="220.0" height="220.0"/><text x="-55.0" y="107.12" text-anchor="middle" transform="rotate(0 -55.0 110.0)" class="fascia">5,00 m</text><text x="55.0" y="107.12" text-anchor="middle" transform="rotate(0 55.0 110.0)" class="fascia">5,00 m</text><line x1="0.0" y1="-110.0" x2="0.0" y2="110.0" stroke-dasharray="10" stroke-dashoffset="5.0" class="fascia" font-size="10"/><g transform="translate(-220.0 0) rotate(0)"><path d="M 20 13.5 v -27 h 42 v -6 l 18,19.5 l -18,19.5 v -6 Z"/></g></g></svg></div><br/>
<button id="btn_convert">Convert SVG to PNG and display it below</button><br/><br/>
<canvas id="img_out"></canvas>
<script>
document.getElementById('btn_convert').addEventListener('click', function () {
var svg = document.getElementById('div_source').querySelector('svg');
var canvas = document.getElementById('img_out');
canvas.width = svg.getBoundingClientRect().width;
canvas.height = svg.getBoundingClientRect().height;
var data = new XMLSerializer().serializeToString(svg);
var win = window.URL || window.webkitURL || window;
var blob = new Blob([data], { type: 'image/svg+xml' });
var url = win.createObjectURL(blob);
var img = new Image();
img.src = url;
img.onload = function () {
canvas.getContext('2d').drawImage(img, 0, 0);
win.revokeObjectURL(url);
canvas.toDataURL("image/png");
};
});
</script>
</body>
</html>
Now the question; is it possible to integrate the above browser (client-side) SVG-to-PNG conversion functionality in a Plotly/Dash app (which is tipically server-side)? My Dash app provides SVG source, and should get in return a PNG image (as io.BytesIO() buffer or base64 encoded data), like I currently do using cairosvg.
I believe that could be very useful to many.

Related

svg blobbed image is broken after encoding it

I've tried parsing an image into blob and refer it in image tag in svg:
`<image xlink:href="${imgRef}" height="${h}" width="${w}" stroke="red" x="50%" y="50%" transform="translate(${-(w / 2)}, ${-(h / 2) - 4})"/>`
and embed it in svg:
const w = 100; // width
const h = 65; // height
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" xmlns:xlink="http://www.w3.org/1999/xlink">` +
`<image xlink:href="${imgRef}" height="${h}" width="${w}" stroke="red" x="50%" y="50%" transform="translate(${-(w / 2)}, ${-(h / 2) - 4})"/>` +
`</svg>`;
Then I tried encoding it through 'data:image/svg+xml;,' + encodeURIComponent(svgString) and it shows a broken image. I tested the above svg string before encoding it via document.body.innerHTML = svgString and the svg image is well displayed. So I'm suspecting the encoding part but it doesn't seem wrong based on many references.
What am I missing? Any insight would be appreciated.
Demo link: https://codepen.io/Dongbin/pen/poRmbmg?editors=0010
External images (ie <img> or CSS background-image) must be self contained. They cannot refer to resources outside the image itself. Basically that means if you need to reference any files (bitmap images, fonts, etc) as part of the SVG image, they need to be Data URLs.
The fix is to not use canvas.toBlob(). Use canvas.toDataURL() instead.
const imgRef = canvas.toDataURL();
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" ` +
`xmlns:xlink="http://www.w3.org/1999/xlink">` +
`<image xlink:href="${imgRef}" height="125" width="200"/>` +
`</svg>`;
Update
If you URI Encode the SVG, it should work.
See: https://jsfiddle.net/dxkeqfnr/
const img = new Image();
img.src = 'https://live.staticflickr.com/7272/7633179468_3e19e45a0c_b.jpg';
img.crossOrigin="anonymous"
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 125;
canvas.getContext('2d').drawImage(img, 0, 0, 200, 125);
canvas.toBlob((blob) => {
const imgRef = URL.createObjectURL(blob);
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="125" xmlns:xlink="http://www.w3.org/1999/xlink"><image xlink:href="${imgRef}" height="125" width="200"/></svg>`;
//div.innerHTML = svgString;
const newImg = new Image();
newImg.src = `data:image/svg+xml,${encodeURIComponent(svgString)}`;
document.body.appendChild(newImg);
});
};
It works in Firefox, but it doesn't work in Chrome. That seems to be a bug. I have reported the bug to Chrome

Download svg including it's text tag value

I'm working on a way to download svg from a webpage.
I have pretty much everything working apart from the last part:
Downloading the svg including the text tag included in it.
Now, to give a better background:
I will need to download images out of an svg using fontawesome icon as text.
The svg is properly downloaded, hower the text tag is not (it's downloaded as a broken image "square")
Here's a simplified version of my code:
function triggerDownload(imgURI, name, format) {
let evt = new MouseEvent('click', {
view: window,
bubbles: false,
cancelable: true
});
let a = document.createElement('a');
a.setAttribute('download', name + '.' + format);
a.setAttribute('href', imgURI);
a.setAttribute('target', '_blank');
a.dispatchEvent(evt);
}
function clickSVG(event) {
const dd = 300;
const format = "png"
let canvas = document.getElementById('canvas'),
target = event.currentTarget;
canvas.width = dd;
canvas.height = dd;
debugger
let newImage = target.cloneNode(true),
circle = newImage.getElementsByClassName('svgCircle-test') ? newImage.getElementsByClassName('svgCircle-test') : null,
image = newImage.getElementsByClassName('svgImage-test');
newImage.height.baseVal.value = dd;
newImage.width.baseVal.value = dd;
if (circle.length > 0) {
circle[0].cx.baseVal.value = dd / 2;
circle[0].cy.baseVal.value = dd / 2;
circle[0].r.baseVal.value = dd / 2;
}
let ctx = canvas.getContext('2d'),
data = (new XMLSerializer()).serializeToString(newImage),
DOMURL = window.URL || window.webkitURL || window,
name = newImage.getAttribute('data-name'),
img = new Image(),
svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'}),
url = DOMURL.createObjectURL(svgBlob);
img.onload = function () {
ctx.drawImage(img, 0, 0);
DOMURL.revokeObjectURL(url);
let imgURI = canvas
.toDataURL(`image/${format}`)
.replace(`image/${format}`, 'image/octet-stream');
triggerDownload(imgURI, name, format);
};
img.src = url;
}
document.getElementById("svg").addEventListener("click", clickSVG)
The html looks like this:
<svg id="svg" height="200" width="200" data-name="test">
<circle cx="100" cy="100" r="100" fill="#faa" class="svgCircle-test" />
<text x="0" y="120" width="200" height="200" class="svgImage-test"></text>
</svg>
<canvas id="canvas" />
And here's the CSS:
svg {
margin-top:10px;
cursor: pointer;
display: inline-block;
padding: 5px;
}
svg text{
font-family:'FontAwesome';
font-size: 100px;
}
#canvas {
display: none;
}
A codepen to help you understand the issue and help me out a bit better can be found here: https://codepen.io/NickHG/pen/QMmJvd
To see the issue, click on the circle (this will download the svg as a png image).
NB: If the download doesn't start, it's probably your browser blocking popups. Just allow it to see the downloaded image.
Thanks
There are a couple of things going on here.
Once you "convert" the SVG file to an HTMLImageElement (<image>), as you are doing here onto the canvas, things change:
the styling you have applied to the <text> no longer applies. That's because it is in the HTML file, not the SVG "file". You need to add the styling it to the SVG itself.
SVGs rendered as an <image> need to be self contained. They can't reference external files such as the Font Awesome font.
To make it self contained, you need to embed the font file in the SVG itself using a Data URL.
You'll need to add a <style> element to the SVG, and include a #font-face rule that specifies a Base64 encoded font file (or files).
See this question for an example

How to display svg markers that contain links to other svgs in HERE maps?

I am trying to display svg markers using the HERE Javascript API. I have followed the documentation, however my twist is that my svg contains links to other svgs. The reason for this is that I would like to display markers that have the same pin shape, but a different icon in the centre of the pin. As the icons will also be used in other places on my website, it makes sense to save the svgs separately so they only need updating in one place.
I can get standard svgs to display as markers, but when I try and nest the svgs, no marker is displayed on the map.
This is my code so far:
pin.svg - This is the basic pin shape that all markers will use
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="46.093765"
height="63.352634">
<g
transform="translate(-8.9327811,-0.45623957)"
id="layer1">
<path
d="M 22.15625,0 A 21.544797,22.053723 0 0 0 0,22.0625 21.544797,22.053723 0 0 0 10.40625,40.9375 c 1.062265,1.795846 11.125,19.40625 11.125,19.40625 l 12.125,-20.0625 A 21.544797,22.053723 0 0 0 43.09375,22.0625 21.544797,22.053723 0 0 0 22.15625,0 z"
transform="translate(10.432788,1.9651232)"
id="path5014"
style="fill:#cccccc;fill-opacity:1;stroke:#6e6e6e;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 50.723066,23.370655 a 17.218699,16.540131 0 1 1 -34.437397,0 17.218699,16.540131 0 1 1 34.437397,0 z"
transform="matrix(0.94189641,0,0,1.0010512,0.4199413,0.48436132)"
id="path5019"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
</g>
</svg>
here-example.svg - This is the example from the HERE site. I am using it as an icon that gets placed inside the pin
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
<rect stroke="black" fill="red" x="1" y="1" width="22" height="22" />
<text x="12" y="18" font-size="12pt" font-family="Arial" font-weight="bold" text-anchor="middle" fill="yellow">
C
</text>
</svg>
pin-with-icon.svg - This is my nested svg - it pulls in the pin and the icon and overlaps them
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<image x="20" y="20" width="300" height="80" xlink:href="pin.svg" />
<image x="120" y="32" width="100" height="30" xlink:href="here-example.svg" />
</svg>
index.html - This is where the HERE map is used and I try and display the markers. It makes heavy use of the HERE developer documentation. Replace <App_ID> and <App_Code> with the HERE credentials. In this page I also try and load the problematic svg into a div to prove that it works.
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<script src="http://js.api.here.com/v3/3.0/mapsjs-core.js" type="text/javascript" charset="utf-8"></script>
<script src="http://js.api.here.com/v3/3.0/mapsjs-service.js" type="text/javascript" charset="utf-8"></script>
<script src="http://js.api.here.com/v3/3.0/mapsjs-ui.js" type="text/javascript" charset="utf-8"></script>
<script src="http://js.api.here.com/v3/3.0/mapsjs-mapevents.js" type="text/javascript" charset="utf-8"></script>
<script src="http://code.jquery.com/jquery.min.js" type="text/javascript" charset="utf-8"></script>
<link rel="stylesheet" type="text/css" href="http://js.api.here.com/v3/3.0/mapsjs-ui.css" />
</head>
<body>
<div style="width: 640px; height: 480px" id="mapContainer"></div>
<!-- Display the svg just to prove that the loads correctly -->
<div id="svgPin"></div>
<script>
// Initialize the platform object:
var platform = new H.service.Platform({
'app_id': '<App_ID>',
'app_code': '<App_Code>'
});
// Obtain the default map types from the platform object
var maptypes = platform.createDefaultLayers();
// Instantiate (and display) a map object:
var map = new H.Map(
document.getElementById('mapContainer'),
maptypes.normal.map,
{
zoom: 6,
center: { lng: 13.4, lat: 52.51 }
});
// MapEvents enables the event system
// Behavior implements default interactions for pan/zoom (also on mobile touch environments)
var behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
// Example with svg pins not showing (svg contains links to other svgs)
var svgMarkupRetrieval = $.get('pin-with-icon.svg', function (svg) {
// Just to prove that the svg is loaded correctly
$('#svgPin').html(svg);
var icon = new H.map.Icon(svg);
// Add the first marker
var marker1 = new H.map.Marker({ lat: 52.4, lng: 13.3 },
{ icon: icon });
map.addObject(marker1);
// Add the second marker.
var marker2 = new H.map.Marker({ lat: 51.45, lng: 13.3 },
{ icon: icon });
map.addObject(marker2);
}, 'text');
// Example with svg pins showing (from HERE developer guide)
var svgMarkupRetrieval = $.get('here-example.svg', function (svg) {
var icon = new H.map.Icon(svg);
// Add the first marker
var marker3 = new H.map.Marker({ lat: 50.4, lng: 13.3 },
{ icon: icon });
map.addObject(marker3);
// Add the second marker.
var marker4 = new H.map.Marker({ lat: 49.45, lng: 13.3 },
{ icon: icon });
map.addObject(marker4);
}, 'text');
</script>
</body>
</html>
To use the code, create each file with the name I specify and put them in a folder in IIS. index.html has to be run through IIS, otherwise a cross origin request error occurs.
The pin-with-icon.svg loads separately (I load it under the map), as do the standard svg markers. But I cannot see why the markers I create using pin-with-icon.svg do not show on the map. Any help would be appreciated.
Use H.map.DomMarker and H.map.DomIcon

How to properly set width and height of SVG in javascript?

I am setting the width and height of a SVG image in javascript and it does not work. My code is like this:
var url = "https://dl.dropboxusercontent.com/u/74728667/left_arrow.svg";
var main = document.getElementById("main");
var xhr = new XMLHttpRequest();
xhr.open('get', url, true);
xhr.onreadystatechange = function(ev)
{
if (xhr.readyState === 4)
{
if (xhr.status === 200)
{
var e = xhr.responseXML.documentElement;
var svg = document.importNode(e, true);
svg.setAttribute("width", "28px");
svg.setAttribute("height", "28px");
svg.style.border="1px solid black";
svg.style.position="absolute";
svg.style.left="50px";
svg.style.top="50px";
main.appendChild(svg);
}
else
{
alert("request failed");
}
}
};
xhr.send();
Above code produces a result like this:
Note the SVG is not resized to the specified width and height. how can i fix this?. i have tried using svg.setAttributeNS(null, "width", "28px");
I inspected the resulting html produced by above code and if I copy paste it in a separate fiddle, I get this:
HTML:
<div id="main" style="position:relative"><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 18 18" width="28px" height="28px" style="border: 1px solid black; position: absolute; left: 50px; top: 50px;">
<path fill="#a00" fill-opacity="1" stroke="none" d="M 0,9 l 17,9 -6,-9 6,-9 Z" onmouseover="evt.target.setAttribute('fill', '#ac0');" onmouseout="evt.target.setAttribute('fill','#a00');"></path>
</svg></div>
Output:
How can I make my js code produce the same result as above?
This probrem is caused by spell miss of view"b"ox in source svg file.
Web browser doesn't treat view"b"ox attribute as view"B"ox attribute, because stand alone svg file is xml file.
Thus you should correct the svg source like this.
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 18 18"
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"
But these attributes are treated as same at inline svg in html.
This act is defined by HTML parsing rule, see this.
http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#creating-and-inserting-nodes

Finding the svg image center

Am a new in svg .I want to find out the svg image center programically,also display a 'dots' in center point.
Here i created a simple svg.How to shows dots in the center point.
<!DOCTYPE html>
<html>
<body>
<svg height="210" width="400">
<path d="M75 0 L56 105 L225 200 Z" />
</svg>
</body>
</html>
You'd need to do...
<script type="text/javascript">
var svg = document.querySelector("svg");
var svgns = "http://www.w3.org/2000/svg";
// get the center
var el = document.querySelector("path");
var bbox = el.getBBox();
var center = {
x: bbox.left + bbox.width/2,
y: bbox.top + bbox.height/2
};
// create the dot
var dot = document.createElementNS(svgns, circle);
dot.setAttribute("cx", center.x);
dot.setAttribute("cy", center.y);
dot.setAttribute("r", 10);
svg.appendChild(dot);
</script>

Resources