Extract SVG values and divide them by 1000 - ruby

I have SVG values in an XML file like 209499.728041:
<path id="dwg-object-136" d="M 209499.728041,19994.245007 209499.728041,27254.245007" style="fill:none;stroke:blue;strok e-width:4px" />
<path id="dwg-object-137" d="M 220719.728041,27254.245007 220719.728041,18564.245007" style="fill:none;stroke:blue;strok e-width:4px" />
<path id="dwg-object-138" d="M 220719.728041,19994.245007 209499.728041,19994.245007" style="fill:none;stroke:blue;strok e-width:4px" />
<path id="dwg-object-209" d="M 214304.728041,35614.245007 A 2000.000000,2000.000000 0 0 0 212304.728041,37614.245007" fi ll="none" stroke="blue" stroke-width="0.100000" />
<path id="dwg-object-210" d="M 230819.728041,37614.245007 A 2000.000000,2000.000000 0 0 0 228819.728041,35614.245007" fi ll="none" stroke="blue" stroke-width="0.100000" />
<path id="dwg-object-211" d="M 216007.728041,39574.245007 216007.728041,35614.245007" style="fill:none;stroke:blue;strok e-width:4px" />
Is there a way to divide all these values by 1000 so that it would be 209.499728041?

If you just want to cause your SVG content to display within the file at 1/100 scale, the easiest way to do this is wrap them in a common transform:
<g transform="scale(0.01)">
<!-- all your paths here -->
</g>
But, if you really want to modify the path data, you can do so using the SVG DOM:
// Untested code
function scalePath( path, scaleFactor ){
var scalable = ['x','y','x1','y1','x2','y2','r1','r2'];
for (var i=0,segs=path.pathSegList,len=segs.numberOfItems;i<len;++i){
var seg = segs.getItem(i);
for (var j=scalable.length;j--;){
var prop = scalable[j];
if (prop in seg) seg[prop] *= scaleFactor;
}
}
}
Note that this (desirably) will not touch the angle, largeArcFlag, or sweepFlag properties of an arc-to command.
You could make the above code slightly more performant by switching on the pathSegTypeAsLetter property of the segment and scaling the correct properties per segment type, but I'm too lazy to do so, and computers are fast.
If you are doing this in order to save bytes in the serialization, you probably want to round the values:
if (prop in seg) seg[prop] = Math.round(seg[prop]*scaleFactor);

Related

How to move some tags inside another tag with d3?

I have svg like this:
<svg id="mySvg">
<path></path>
<path></path>
<path></path>
</svg>
But I would like to group all paths in this svg inside the g tag, so it would look like that:
<svg id="mySvg">
<g>
<path></path>
<path></path>
<path></path>
</g>
</svg>
How should I do it?
d3.select("#mySvg").append("g") // -> and move all paths inside g
You can remove elements with selection.remove(). This method will return a selection of the removed elements.
You can also use selection.append() to append those elements. However, selection.append only takes a function or a string. If you supply a function it should return a (single) element/node. We can access the element/node of a selection of one element with selection.node()
This gives us the pattern:
let svg = d3.select('svg')
let path = svg.selectAll('path').remove();
svg.append('g').append(()=>path.node());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg>
<path d="M 30 30 C 60 30 50 50 80 50" stroke="black" stroke-width="1" fill="none"></path>
</svg>
However, append() is intended for a single element. If you have many elements, instead you could use selection.each() to add each selected element to a parent g:
let svg = d3.select('svg')
let path = svg.selectAll('path').remove();
let g = svg.append('g');
path.each(function() {
g.append(()=>this);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg>
<path d="M 30 30 C 60 30 50 50 80 50" stroke="black" stroke-width="1" fill="none"></path>
<path d="M 30 130 C 60 130 50 150 80 150" stroke="black" stroke-width="1" fill="none"></path>
</svg>
The use of function() and fat arrow notation is intended to ensure the correct this, which is the element we want to add to the parent g
This second snippet will work regardless of whether one or more nodes are selected.
In the past years I laid out some approaches similar to the one proposed by Andrew Reid in his answer: "Can I move an SVG element between SVG groups, in d3.js" and "D3.js how to embed selection into a new element". However, digging deeper into this issue, I found a solution that uses VanillaJS DOM methods and does the whole shebang in a single method call.
The API of an Element knows some methods for adding multiple elements to the DOM at the same time. The element.append() method can be used to elegantly and easily append your path elements to the new group in a single run.
const path = d3.selectAll("path");
d3.select("svg")
.insert("g") // insert the new g at the first position
.node() // get the DOM node
.append( // use the DOM's append, not the D3 one
...path.nodes() // add the nodes from the path selection
);
<script src="https://d3js.org/d3.v7.js"></script>
<svg id="mySvg">
<path></path>
<path></path>
<path></path>
</svg>

How to morph a path data to another path data in SVG?

I'm trying to figure out why it wont morph this path data to another path data, I need to make it look like an real animation.
This is my SMIL code:
<animate xlink:href="#Barra3"
repeatCount="indefinite"
attributeName="d"
dur="5s"
values="M52,346L56,346C61.523,346 66,350.477 66,356L42,356C42,350.477 46.477,346 52,346Z;
M54,225C60.627,225 66,230.373 66,237L66,356L42,356L42,237C42,230.373 47.373,225 54,225Z;"/>
Here is my codepen:
https://codepen.io/joannesalfa/pen/mdPBJxq
and go line 181. I'm using SMIL.
The most important when trying to morph a path in svg is thast the d attribute hes to have the same number of commands and the same commands. I've rewritten the short path so that the lines drawing the sides of the shape have a length = 0.
M54,346
C60.627,346,66,351.373,66,358
L66,358L42,358L42,358
C42,351.373,47.373,346,54,346Z
Please take a look:
svg{border:solid}
<svg viewBox="5 200 100 200" width="100">
<path d="M54,346
C60.627,346,66,351.373,66,358
L66,358L42,358L42,358
C42,351.373,47.373,346,54,346Z" stroke="red" fill="gold" >
<animate dur='5s'
attributeType="XML"
attributeName='d'
repeatCount='indefinite'
values="M54,225
C60.627,225 66,230.373 66,236
L66,356L42,356L42,236
C42,230.373 47.373,225 54,225Z;
M54,346
C60.627,346,66,351.373,66,356
L66,356L42,356L42,356
C42,351.373,47.373,346,54,346Z;
M54,225
C60.627,225 66,230.373 66,236
L66,356L42,356L42,236
C42,230.373 47.373,225 54,225Z" />
</path>
</svg>
Update
The OP is commenting:
Would you mind how to rewrite the short path step by step? I find it's very confusing to me
I'm taking both those paths and I'm breaking them in 5 paths of different colors, one for every command. Please note that I had to add a move to command (M) at the beginning of each path. The value for the move to is the last point of the previous path. The lines, are the blue paths.
For the short path you can see those blue paths in the code but not in the svg because their length is 0. I needed those 0 length lines because you have lines in the long path.
svg{width:200px;border:solid;overflow:visible; fill:none}
<svg viewBox="40 220 28 140" >
<desc>The short path</desc>
<path d="M54,346 C60.627,346,66,351.373,66,356" stroke="red" />
<path d="M66,356 L66,356" stroke="blue" />
<path d="M66,356 L42,356" stroke="green" />
<path d="M42,356 L42,356" stroke="blue" />
<path d="M42,356 C42,351.373,47.373,346,54,346" stroke="gold"/>
</svg>
<svg viewBox="40 220 28 140" >
<desc>The long path</desc>
<path d="M54,225 C60.627,225 66,230.373 66,237" stroke="red"/>
<path d="M66,237 L66,356" stroke="blue" />
<path d="M66,356 L42,356" stroke="green" />
<path d="M42,356 L42,237" stroke="blue"/>
<path d="M42,237 C42,230.373 47.373,225 54,225;" stroke="gold"/>
</svg>

Nokogiri can't get g tags from svg

I have a svg file like this:
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" height="700" width="1024" viewBox="0 0 1024 700" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="L_691200_group">
<g id="L_691200_background">
<path stroke-width="5" fill="none" fill-rule="nonzero" stroke="black" d="..."/>
</g>
<g id="L_691200_path"><path d="..."/></g>
<g id="L_691200_icon"><circle cx="387.413" cy="485.451" r="7.99993" stroke-width="1" stroke="black"/><circle cx="478.876" cy="379.327" r="2.50001" stroke-width="1" stroke="black"/><circle cx="276.717" cy="593.047" r="2.50001" stroke-width="1" stroke="black"/></g>
<g id="L_691200_symbol">
<path fill="white" fill-rule="nonzero" d="..."/>
</g>
</g>
</svg>
I tried to parse it in Ruby using Nokogiri but I cannot get the g tags.
I tried various things:
groups = doc.xpath('//g')
groups = doc.xpath('//xmlns:g')
groups = doc.xpath('//g', 'g' => 'http://www.w3.org/2000/svg')
groups = doc.css('g')
But groups is always empty. The only way I got it to work was to remove namespaces from doc. But I can't do it that way since I need to save the document again after changing a few thins.
How can I get the g tags without removing the namespace?
It sounds like you have an induced error. I will post this as an answer for a sake of code formatting.
▶ Nokogiri::VERSION
#⇒ "1.5.4"
▶ xml = Nokogiri.parse '<?xml version="1.0" encoding="UTF-8"?>...'
▶ xml.xpath('//xmlns:g').size
#⇒ 5
Besides that, xml.css 'g' works the same way.

XPath - Get node that only has children with specific attribute

I have an SVG document that contains nodes similar to the following:
<item>
<g>
<path fill="#FFFFFF" ... ></path>
<path fill="none" ... ></path>
<ellipse fill="none" ... ></path>
</g>
<g>
<path fill="none" ... ></path>
<path fill="none" ... ></path>
<ellipse fill="none" ... ></path>
</g>
<g>
<path fill="#FFFFFF" ... ></path>
<path fill="#FFFFFF" ... ></path>
<ellipse fill="#FFFFFF" ... ></path>
</g>
</item>
What I'm trying to do is only select the <g> nodes that ONLY contain children that have the fill="none" attribute. So I'm trying to find an XPath that only matches the second <g> node in my example.
I've done my share of googling and searching on stack overflow and nothing seems to work.
The closest one that I thought SHOULD work is giving me an error saying it's not a valid XPath Expression in Google Chrome's developer tools using the $x() helper.
*[name()="g" and descendants::*[#fill="none"]]
Any help is appreciated
Thanks
Try:
*[ name()="g"
and descendant::*[#fill="none"]
and count(descendant::*[#fill="none"]) = count(descendant::*)
]

Calculate path center after transformation matrices have been applied

I made a logo in Inkscape. For learning I wanted to make a wheel shape in the logo rotate by the animation support in SVG.
It was easy to implement the rotation, but it was difficult for me to be able to specify the correct axis of rotation. The shape was a cog wheel and I wanted it to rotate around its center. Trial and error gave that the xy-coordinate (47.1275, 1004.17) (whose components are strangely asymmetric, but I guess that has to do with the transformation matrices Inkscape applies) was a good approximation (see animateTransform tag below), but how would I get that from first principles?
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 321.281 150.799" xmlns:dc="http://purl.org/dc/elements/1.1/">
<g transform="translate(-9.9178912,-891.57237)">
<g transform="matrix(1.9522781,0,0,1.9522781,4.6434311,-1008.1558)">
<animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0 47.1275 1004.17" to="45 47.1275 1004.17" dur="2s" fill="freeze" additive="sum" repeatCount="indefinite" />
<g transform="matrix(0.65043772,0,0,0.65043772,-143.67477,980.4256)" stroke="#666" stroke-miterlimit="4" stroke-dasharray="none" stroke-width="7.68713093" fill="none">
<path stroke-linejoin="miter" d="m293.404-3.51576c-2.73916,0-5.41514,0.287192-8,0.8125v6.1875c-3.47484,0.838872-6.7198,2.18462-9.6875,4l-4.375-4.375c-2.24264,1.48612-4.29226,3.22977-6.1875,5.125s-3.63888,3.94486-5.125,6.1875l4.375,4.375c-1.81538,2.9677-3.16112,6.21265-4,9.6875h-6.1875c-0.5253,2.58486-0.8125,5.26083-0.8125,8s0.2872,5.41515,0.8125,8h6.1875c0.83888,3.47485,2.18462,6.7198,4,9.6875l-4.375,4.375c1.48612,2.24264,3.22976,4.29227,5.125,6.1875s3.94486,3.63888,6.1875,5.125l4.375-4.375c2.9677,1.81538,6.21266,3.16113,9.6875,4v6.1875c2.58486,0.525308,5.26082,0.8125,8,0.8125,2.73916,0,5.41514-0.287192,8-0.8125v-6.1875c3.47484-0.838872,6.7198-2.18462,9.6875-4l4.375,4.375c2.24264-1.48612,4.29226-3.22977,6.1875-5.125s3.63888-3.94486,5.125-6.1875l-4.375-4.375c1.81538-2.9677,3.16112-6.21266,4-9.6875h6.1875c0.5253-2.58485,0.8125-5.26083,0.8125-8s-0.2872-5.41515-0.8125-8h-6.1875c-0.83888-3.47485-2.18462-6.7198-4-9.6875l4.375-4.375c-1.48612-2.24264-3.22976-4.29227-5.125-6.1875s-3.94486-3.63888-6.1875-5.125l-4.375,4.375c-2.9677-1.81538-6.21266-3.16113-9.6875-4v-6.1875c-2.58486-0.525308-5.26084-0.8125-8-0.8125z" stroke-dashoffset="162" stroke="#666" stroke-linecap="butt" stroke-miterlimit="4" stroke-dasharray="none" stroke-width="7.68713093" fill="none"/>
</g>
</g>
</g>
</svg>
From what I've read in the specification I would say the transformation matrices applied are
1.9522781 0 4.6434311
0 1.9522781 -1008.1558
0 0 1
and
0.65043772 0 -143.67477
0 0.65043772 980.4256
0 0 1
Are they applied on the xyz-coordinate (-9.9178912,-891.57237,0) after the translation transformation?
I guess a correct analysis above would get me the top left point of the path described, or maybe the coordinate for the first handle. After that, does one have to parse the path to decide the bounding-box and thereby the center (since it concerns a somewhat circular object) of the path?
Is it all a lesson in not trying to manually do animation on freely created shapes?
I think the transformations will be applied from the innermost outward, so transform="translate(-9.9178912,-891.57237)" will be done last. But you can ignore the other transformations if you put your animation in the innermost region, i.e. within the path itself:
<g transform1>
<g transform2>
<g transform3>
<path d="coordinates">
<animateTransform your transformation here>
</path>
</g>
</g>
</g>
Then you just need to find the centre of your path, which is easy to do in Inkscape, but tricky to do on-the-fly (related question here: programmatically How to get shape width in SVG document using java).
Personally, I'd use a script within the svg so you can use getBBox to find the bounding box of your shape. If you add the following element into your SVG you can make any element with the id="cog" turn about its centre:
<script type="text/ecmascript"><![CDATA[
var svgNS = "http://www.w3.org/2000/svg";
function init(evt)
{
if ( window.svgDocument == null )
{
svgDocument = evt.target.ownerDocument;
}
addRotateTransform('cog');
}
function addRotateTransform(target_id)
{
var element_to_rotate = svgDocument.getElementById(target_id);
var my_transform = svgDocument.createElementNS(svgNS, "animateTransform");
var bb = element_to_rotate.getBBox();
var cx = bb.x + bb.width/2;
var cy = bb.y + bb.height/2;
my_transform.setAttributeNS(null, "attributeName", "transform");
my_transform.setAttributeNS(null, "attributeType", "XML");
my_transform.setAttributeNS(null, "type", "rotate");
my_transform.setAttributeNS(null, "dur", "4s");
my_transform.setAttributeNS(null, "repeatCount", "indefinite");
my_transform.setAttributeNS(null, "from", "0 "+cx+" "+cy);
my_transform.setAttributeNS(null, "to", "360 "+cx+" "+cy);
element_to_rotate.appendChild(my_transform);
my_transform.beginElement();
}
]]></script>
You also need to add onload="init(evt)" as an attribute to the SVG tag. e.g.
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 321.281 150.799"
xmlns:dc="http://purl.org/dc/elements/1.1/"
onload="init(evt)">
This will call the init() function when the SVG is first loaded. The init() function calls addRotateTransform() which find the element with a given id. It then finds the centre of that object using getBBox() and adds an animateTransform method with the relevant centres. You can change the dur attribute which determines the speed of a full rotation.
It might seem like a lot of code, but I think it's the easiest way to determine the centre of a path. It also means to can easily add other rotating elements by add addRotateTransform('whatever-id'); to the init() function.

Resources