Convert SVG polygon to path - svg

I have a fairly large SVG file of administrative subdivisions that I need to work with in Raphael.JS (it has 600 polygons and weights 1.2 Mb).
Now, I need to convert these polygons to paths so that it works in Raphael. The great poly2path tool does that, but it doesn't support any batch command, so that each polygon's position relative to the others is lost.
Do you know of any tool to convert SVG polygons to paths? (I also have the AI file which was used to export the SVG).
Many thanks

Open your SVG in a web browser.
Run this code:
var polys = document.querySelectorAll('polygon,polyline');
[].forEach.call(polys,convertPolyToPath);
function convertPolyToPath(poly){
var svgNS = poly.ownerSVGElement.namespaceURI;
var path = document.createElementNS(svgNS,'path');
var pathdata = 'M '+poly.getAttribute('points');
if (poly.tagName=='polygon') pathdata+='z';
path.setAttribute('d',pathdata);
poly.getAttributeNames().forEach(function(name){
if(name !== 'points')
path.setAttribute(name, poly.getAttribute(name))
})
poly.parentNode.replaceChild(path,poly);
}
Using the Developer Tools (or Firebug) of the browser, use "Copy as HTML" (or Copy SVG) on the element to get the modified source onto the clipboard.
Paste into a new file and enjoy.
I have a demo of the above method (slightly modified) on my website:
http://phrogz.net/svg/convert_polys_to_paths.svg
There are two methods in use on that page; one (like the above) uses string-based techniques to get and set the points; the other uses the SVG DOM for accessing points and setting path commands.
As noted by #Interactive in the comments, you can do this via text-only transformations by:
Convert all <polyline and <polygon to <path
Change all points=" to d="M
For any elements that were <polygon>, you need to add z as the last character of the d attribute to connect the last point to the first. For example:
<polygon points="1,2 3,-4 5,6"/>
becomes
<path d="M1,2 3,-4 5,6z"/>
This 'hack' works because the specifications declare that a moveto command (M or m) followed by multiple coordinates is legal, with all coordinates after the first interpreted as lineto commands.

A clicky-bunty answer:
open the svg in inkscape vector graphics editor
select all objects (ctrl-a)
at the drop down menu point "path" select first entry "object to path" (shift-ctrl-c)
save the svg and check out the path properties
Might be not an appropriate answer (because with large files the program needs some time).

Little fix for polygon id, fill and stroke attributes save
var polys = document.querySelectorAll('polygon,polyline');
[].forEach.call(polys,convertPolyToPath);
function convertPolyToPath(poly){
var svgNS = poly.ownerSVGElement.namespaceURI;
var path = document.createElementNS(svgNS,'path');
var points = poly.getAttribute('points').split(/\s+|,/);
var x0=points.shift(), y0=points.shift();
var pathdata = 'M'+x0+','+y0+'L'+points.join(' ');
if (poly.tagName=='polygon') pathdata+='z';
path.setAttribute('id',poly.getAttribute('id'));
path.setAttribute('fill',poly.getAttribute('fill'));
path.setAttribute('stroke',poly.getAttribute('stroke'));
path.setAttribute('d',pathdata);
poly.parentNode.replaceChild(path,poly);
}

Copying everything from the developer tools seems pretty inconvenient. You could use an XSLT to transform polygons and polylines to paths:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" exclude-result-prefixes="svg"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<!-- Identity transform: Copy everything
(except for polygon/polyline, handled below) -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!-- Turn polygons/polylines into paths,
copy all attributes and content
(except for #points: Will be matched
by template below) -->
<xsl:template match="svg:polygon|svg:polyline">
<path>
<xsl:apply-templates select="#*|node()"/>
</path>
</xsl:template>
<!-- Turn the points attribute into a d attribute -->
<xsl:template match="#points">
<xsl:attribute name="d">
<xsl:value-of select="concat('M',.)"/>
<!-- If we have a polygon, we need to make
this a closed path by appending "z" -->
<xsl:if test="parent::svg:polygon">
<xsl:value-of select="'z'"/>
</xsl:if>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
Any attributes of the polygon/polyline elements will be carried over to the path element. This is also suitable for batch processing. You can run this using any XSLT processor (Saxon, Xalan, xsltproc, Altova...) or even in-browser, using the XSLTProcessor object, like:
var xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(stylesheet);
var transformedSVG = xsltProcessor.transformToFragment(svgDocument).firstChild
(Similar question: Examples of polygons drawn by path vs polygon in SVG)

Building on #halfer's solution. If you have internal CSS and want to retain the class names, you can slightly modify this to:
var polys = document.querySelectorAll('polygon,polyline');
[].forEach.call(polys,convertPolyToPath);
function convertPolyToPath(poly){
var svgNS = poly.ownerSVGElement.namespaceURI;
var path = document.createElementNS(svgNS,'path');
var pathClass = poly.getAttribute('class');
var points = poly.getAttribute('points').split(/\s+|,/);
var x0=points.shift(), y0=points.shift();
var pathdata = 'M'+x0+','+y0+'L'+points.join(' ');
if (poly.tagName=='polygon') pathdata+='z';
path.setAttribute('id',poly.getAttribute('id'));
path.setAttribute('class', pathClass);
path.setAttribute('d',pathdata);
poly.parentNode.replaceChild(path,poly);
}

Related

Blazor - Add element to SVG object

I want to manipulate an SVG object with blazor, is it possible to do this via C# client side, or do I need to use javascript.
For example, draw a line programatically based on clicks in the SVG area.
Any pointers would be greatly appreciated. I found a lot on adding SVG component, but nothing on adding elements to the already existing svg component
You can use SVG file contents as the markup in a Blazor component, and then do any of the Blazor-y things you'd normally do.
Put a variable in the svg markup, and build it as a string.
Here's a highly-simplified excerpt:
(MySvgComponent.blazor)
<svg blah blah blah>
<polyline fill="none" stroke="#0074d9"
stroke-width="2" points="#PointString" />
</svg>
#code {
public string #PointString {get;set;}
public void AddPoint (int X, int Y){
#PointString += " " + X + "," + Y;
}
You'll have to add your own code to figure out where you want to add the points. You could make a List<Point> parameter to pass in from a parent or something, and then call AddPoint in a foreach loop in OnInitialized(). You could also very easily replace the stroke color or anything else by replacing literals like "0074d9" with variables "#myColorString."
Handling mouse-click locations will require some fancy JS work. Try using Javascript Interop with something like the following:
How to get the click coordinates relative to SVG element holding the onclick listener?

D3js: finding path's bounding box (without getBBox() )?

The following code works on Chromium :
var node = window.d3.selectAll('#L1 > *:nth-child(2)');
var bbox = node.node().getBBox();
console.log(bbox) // {height: 44, width: 44, y: -13, x: 144}
but not with nodejs + jsdom:
"TypeError: Object [ PATH ] has no method 'getBBox' "
M. Bostock pointed out that JSDOM doesn't support getBBox()
What D3js replacement to use to get the bounding box of #L1 > *:nth-child(2) ?
Past efforts lead me there : getBBox() based fiddle
Path's bounding box
Digging straight into the element's path data d="..." should work. An svg line is basically a set of x,y points. Assuming absolute coordinates without translation nor big bezier curves, which is the case of my D3js-generated svg lines, I'am finding in this data the min and max values for both x and y.
To do so, I get the d="..." svg line or multilines code. For simplicity sake, I rudely removes possible relative jumps such h30 or v20 since I never saw any in my D3js output, then clean out letters (aka svg commands : M,L,H,V,C,S,Q,T,A,Z), simplify the spaces and line jumps, then split by the remaining spaces. I get a clean arrays of coordinates.
Important to note, my selector directly target a single non-translated path.
var getBBox = function(selector){
var xmin, xmax, ymin, ymax,p;
// clean up path
var t = d3.select(selector).attr("d"); // get svg line's code
console.log(t)
t = t.replace(/[a-z].*/g," ") // remove relative coords, could rather tag it for later processing to absolute!
.replace(/[\sA-Z]+/gi," ").trim().split(" "); // remove letters and simplify spaces.
console.log(t)
for(var i in t){ // set valid initial values
if(t[i].length>1){
p = t[i].split(",");
xmin = xmax = p[0]; ymin = ymax = p[1]; }
}
for(var i in t){ // update xmin,xmax,ymin,ymax
p = t[i].split(",");
if(!p[1]){ p[0]=xmin; p[1] = ymin;} // ignore relative jumps such h20 v-10
xmin = Math.min(xmin, p[0]);
xmax = Math.max(xmax, p[0]);
ymin = Math.min(ymin, p[1]);
ymax = Math.max(ymax, p[1]);
} return [[xmin,ymax],[xmax,ymin]]; // [[left, bottom], [right, top]] as for https://github.com/mbostock/d3/wiki/Geo-Paths#bounds
}
var bb = getBBox("path");
JSfiddle DEMO
Groups bounding boxes
For groups of multiple paths, you may want to traverse the svg DOM to loop upon each single path of the group in order to update xmin, ymin, xmax, ymax.
Translated elements
To handle translated elements, adapt further.
Alternatives
Other better approaches may exist. Remember to check if getBBox() and getBoundingClientRect() are available in your context, since they are native and very convenient.
The reason why getBBox/getBoundingClientRect/getClientRect does not work in NodeJS+JSDOM is that calculating these values of an SVG (or HTML) element involves massive amounts of computation.
First, all CSS code in <style> elements must be parsed (which is already not trivial). Then the CSS selectors, cascading and inheritance rules must be applied to know what size, position or line width an element has. And even after you know all style property values, you need to do some non-trivial maths to calculate the bounding boxes: definition of different SVG transform functions, compositions of these, bounding boxes of SVG primitives and Bezier curves. Browsers support all of these (they have to, in order to draw the element), but JSDOM is simply not meant for all of these.
But fortunately, canvg is a JavaScript implementation of most of SVG, which uses a <canvas> element to draw the image. It does support most of the above, and although it does not have an interface for giving you those data, fortunately it has very nice (and MIT licensed) code, so hopefully you can copy and reuse parts of it. As of now, the code is written in a single file, and it has CSS parsing, applying cascading rules, path data parsing, definitions of SVG transforms, applying transformations, and bezier curve bounding box calculation. That is, almost everything you need to calculate bounding boxes :) It does not, however, support CSS selectors, but it can reuse another library. But unfortunately, as far as I can tell, canvg is not ready for running in NodeJS, you probably need some tweaks.
There is, however canvgc, an SVG to JS compiler, which contains an older version of canvg, and it is capable of running in NodeJS. So it is easier to start with that.

getting text width in SVG prior to rendering

I want to put a rectangle around a text in SVG.
The height of the text is known to me (the font-size attribute of the text element). But the width is dependent on the actual content. Using getBBox() or getComputedTextLength() should work. But this only works after rendering.
Is there a way to specify that in an other way? For example defining the x and width attributes relative to other values? I didn't find anything like that in the SVG Spec.
Figuring where text ends presumably requires roughly the same underlying code path as the rendering itself implements - going through the width of each character based on the font and style, etc... As I am not aware the SVG standards define a method for directly getting this information without doing the actual full rendering, till such methods emerge or are reported here by others, the approach should be to render invisibly before doing the actual rendering.
You can do that in a hidden layer (z-index, opacity and stuff) or outside the visible viewport, whichever works best in experimentation. You just need to get the browser do the rendering to find out, so you render invisibly for that sake, then use getComputedTextLength()
I know this is old, but a few ideas:
If you can choose a mono-spaced font, you know your width by a simple constant multiplication with glyph count
If you are bound to proportional fonts, you can find an average glyph size, do the math as with mono-space, and leave enough padding. Alternatively you can fill the padding with text element textLength attribute. If the constant is chosen carefully, the results are not very displeasing.
EDIT: As matanster found it to be hacky
Predetermine glyph widths with getComputedTextLength() and build a lookup table. Downside is that it does not account for kerning, but if your cache size is not a problem, you can append glyph-pair widths to this lookup.
Going beyond that is to find some way to do server side rendering: Is there a way to perform server side rendering of SVG graphics using React?
It is possible using canvas with measureText():
// Get text width before rendering
const getTextWidth = (text, font) => {
const element = document.createElement('canvas');
const context = element.getContext('2d');
context.font = font;
return context.measureText(text).width;
}
// Demo
const font = '16px serif';
const text = 'My svg text';
const textWidth = getTextWidth(text, font);
document.body.innerHTML = `
<svg>
<text x="0" y="20" font="${font}">${text}</text>
<rect x="0" y="30" width="${textWidth}" height="4" fill="red" />
</svg>
`;
Adapted from https://stackoverflow.com/a/31305410/1657101

SVG polar gradients

I'm a beginner at SVG, but I'd like to learn some techniques.
To be short, is there a simple way to create something like this?
I was thinking about creating a polar gradient and then clipping it:
But how do I generate a polar gradient?
Even if there's no native method, maybe it could be made with a simple linear gradient and then using some rectangular-polar coordinate transformation. Is there a way to do so?
So this is the solution I developed:
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg viewBox="0 0 100 100" version="1.1" onload="makeGradient();">
<script>
function makeGradient() {
var root = document.rootElement, i = 256, cir, a;
for (; i--;) {
a = i*Math.PI/128;
cir = document.createElementNS("http://www.w3.org/2000/svg", "circle");
cir.setAttribute("cx", 50 - Math.sin(a)*45);
cir.setAttribute("cy", 50 - Math.cos(a)*45);
cir.setAttribute("r", "5");
cir.setAttribute("fill", "rgb(" + i + ", " + i + ", " + i + ")");
root.appendChild(cir);
}
}
</script>
</svg>
Minified version (395 bytes):
<?xml version="1.0" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" version="1.1" onload="g(this.namespaceURI,document,Math)"><script>function g(n,d,m,i,c,a,p,z){for(i=256;i--;){a=i*m.PI/128;c=d.createElementNS(n,"circle");for(p in z={cx:10-m.sin(a)*9,cy:10-m.cos(a)*9,r:1,fill:"rgb("+[i,i,i]+")"})c.setAttribute(p,z[p]);d.rootElement.appendChild(c)}}</script></svg>
This was made creating circles filled with 256 shades of gray (it sounds like porn literature for coders!) and conveniently placed.
The radii can be adjusted: I've chosen 45 for the whole spinner and 5 for the single circles. Moreover, the detail can be adjusted too if 256 are too many:
for (; i -= 2;) { ...
Use powers of 2 for optimal results. Or just define the number of steps:
var steps = 100, i = steps;
for (; i--;) {
a = i*2*Math.PI/steps;
...
cir.setAttribute("fill", "rgb(" + i*255/steps + ", " + ...);
}
A big "thank you" to Erik Dahlström for the hint, and thank you Michael Mullany for the attempt :)
Edit: Here's a fiddle to demonstrate the code.
Edit 2: Here's another fiddle using curved segments to create the spinner. You can adjust the number of segments and the size, and even see it spinning. I don't know why when the size is auto, there's a bottom margin of 5 pixels on the SVG, this making the spinning slightly off-centered...
There are no paintservers in SVG 1.1 that allow this directly, but you can e.g do it using a bit of script. Here's an article that explains how.
There is no support for polar gradients in SVG 1.1 (what's available in most edge browsers today) although there are proposals to allow capabilities like these in SVG 2. The only workaround I can think of is to apply a blend filter using an externally generated image as your multiply source. But then, I'm assuming the whole point it to try to avoid external images so this would be a little pointless:)

Calculating viewBox parameters based on path elements in SVG

I get an XML or JSON with paths only, and I need to recreate the SVG image.
I create an empty
<svg xmlns='http://www.w3.org/2000/svg' version='1.1'></svg>,
I add a <g transform="scale(1 -1)" fill='#aaa' stroke='black' stroke-width='5' ></g> in it, and then in this element I add all of the paths in it (e.g. <path d= ... />).
In the end I get a SVG image, but because I haven't set the viewBox attribute in the SVG element the image isn't properly displayed - when I open it in browser, a part of it is displayed full size.
Can the viewBox be calculated from the values from the paths?
Thank you!
Similar to Martin Spa's answer, but a better way to do get the max viewport area is using the getBBox function:
var clientrect = path.getBBox();
var viewBox = clientrect.x+' '+clientrect.y+' '+clientrect.width+' '+clientrect.height;
You can then set the viewbox to these co-ordinates.
n.b. i think you can change the viewbox of an svg after it's rendered so you may have to re-render the svg.
OK so I solved it the following way:
removed all letters from the paths string and made an array out of it with
var values = pathValue.split('L').join(' ').split('M').join(' ').split('z').join('').split(' ');
found max and min from those values:
var max = Math.max.apply( Math, values );
var min = Math.min.apply( Math, values );
set the viewBox:
viewBox = max min max max
This worked in my case excellent. Hope that it will be helpful to someone else too.

Resources