Svg.js: Can I tell SVG.js to use limited decimal precision?

Created on 26 Jan 2017  Â·  59Comments  Â·  Source: svgdotjs/svg.js

For the actual proposal look at this comment

I just created a hexagon using SVG.js and the SVG.shapes.js plugin. It works great!

However, many of the polygon’s points have decimals like:

  • 49.999999999999986 or 50.00000000000001 (I assume a rounding error—should be 50, right?), and
  • 173.2050803946182 (I assume it’s “just correct”, but I’d like to limit to 2 or 3 decimal places)

Is there a way to tell SVG.js to either:

  • check for .999999… or .0000000001 and round to the nearest integer?, or
  • limit decimal precision to 2/3/«x» number of places?, or
  • offer a hook-in location for client code to pre- or post-process calculations of numbers?

(I tried to fix this by calling the .spof() when calling SVG(), but it seemed to have no effect…)


good first issue proposal

All 59 comments

Short Answer: No, you cant.
Long answer: You can loop trough every attribute and shorten the numbers by hand. This is ofc quite much work for something like this. May I ask why you need that?

We are planning to integrate the precision() method which does exactly that. However, note that the precision is needed to calculate the correct position of all points of a polygon or path when resizing it.

About the 0.0000001 errors: Actually there is not much we can do. That are rounding errors and in javascript all numbers are floats - stuff like that happens

@Fuzzyma actually there is a trick to force a float into a 32 bit signed Integer by using num|0, where num is a float, | is bitwise or operator and the 0 is a false value that means we return the num variable, which is now a 32 bit signed Integer. We used this trick at one point.

But it won't help in this case. {float}.toFixed() might be more to what the OP is asking for.

What happens to numbers like 2.999999... when you use that trick with it? I guess it would convert it to 2 which would be really wrong at this point

@Fuzzyma yes. You would usually use it to convert a boolean to an Integer or to declare Integer variables like:

var i = 0|0

asm.js does this

Having a feature to change our decimal points will require some runtime overhead and make our calculations less precise. Other than esthetic's, is there any use-case for this @Zearin?

I hacked this a long time ago for my version. The unnecessary numbers were doubling the size of my SVGs, bloating my app, and were not hard to remove. I threaded through the path creation and did the kind of adjustments you can see in this sample:

  var App = App || {}  
  App.precision = 1;
  if (s == 'M' || s == 'L' || s == 'T')
    x.push(seg.x.toFixed(App.precision), seg.y.toFixed(App.precision))
  else if (s == 'H')
    x.push(seg.x.toFixed(App.precision))

I made other hacks that have kept me from updating for a while now - but this was not a feature I could go w/out - so if you really need those numbers to be smaller, you can always hack it up as long as you don't need updates! I have no idea if the structure would allow you to do as I did.

In the first version of SVG.js I applied rounding to keep the resulting SVG small. Initially, I developed the library with the eye on print (web-to-print) so rounding was acceptable. Further along the road, I noticed many issues with rounding (e.g. zoom, scale, ...) so I skipped it. But This is still something we should take into account. To be able to configure the rounding can be a useful feature for real-world applications (e.g. CNC, 2D printing, 3D printing, ...). So I'll write a proposition for SVG.js v3.

@wout This will also help with disparate rounding issue in various browsers in our unit tests, if we want to expand the list of browsers, we run in CI testing.

Guys this would be AWESOME if you would add - I am still using a hacked up old version of SVG.js, partly due to my reliance on the import script which workes perfectly including gradients and tspans (which I had to hack to get working right on my app) - but also because I would also have to rehack your new version to get rid of those extra decimals. My character creator uses tons of paths for each character, and I have a generator that creates multiple designs for the user to choose from - and those extra decimals slow my app down to a crawl.

I will be really excited to see this implemented, and will test out the new version w/ the native import ability to see if it still works with my stuff! Thanks so much for all your work on SVG.js, I've relied on it for years now.

@mspanish It's definitely planned for 3.0. I think it's a very useful feature.

But brace yourself! :) Going from a 1.x version to 2.x will be a big job because we introduced a lot of breaking changes, and even more with 3.0.

Better skip 2.x and go for 3.x directly ;)

yeah I can wait - I am keeping my eye on stuff and will try for the big overhaul for 3.x!!

The old version still works great though. Thanks again guys!

@wout @mspanish @Zearin @dotnetCarpenter @RmiTtro Where is the best place to apply rounding? At every update of any node? Or only when exporting svgs?

Good question.

Applying rounding to a path is pretty destructive. For example, resize a path down by factor 10, apply rounding, resize it back up by factor 10, and the path will have lost detail. Another downside of "active" permanent rounding is a performance hit.

I've been thinking about it, and my idea is to add a round() method (other name suggestions welcome) to the SVG.Shape class. This method will then round a predefined selection of numeric attributes. It can also take an argument to define the allowed number of decimals.

By using a method, you can round before exporting:

var svg = path.round(2).svg()

Or non-destructively:

var clone = path.clone().round(3)
var svg = clone.svg()
clone.remove()

We can also provide deep rounding for elements inheriting from SVG.Parent. Something like:

nested.round(2, true)

Finally, we'll also need the round() method on SVG.Array, SVG.PointArray, SVG.PathArray, SVG.Number...

What I like about this approach is that the user has to apply the rounding explicitly. Which is better than having it happen automagically in the background. And by putting this feature in a method, we avoid overhead for other users as well.

That sounds good. For me it was always clear that rounding never happens automatically. Something always has to be called to "activate" rounding (e.g. SVG.setGlobalPrecision(5)). But since I have the same thoughts about permanent rounding I like your approach.

Yeah I've tried using rounding in a Node.js script to batch convert svgs to icon fonts - and the results were pretty bad. I got rid of it and the icons looked better. I was able to use SVGCleaner https://sourceforge.net/projects/svgcleaner/ after the fact - and that works great. So I do appreciate that this isn't easy to do - and can see what happens when rounding goes bad. For 1000+ icons however the rounding saves a ton of data - in some cases from 1.9MB down to 500KB for a JSON file.

So for you this is only about space when saving the svgs? Because thats much easier to implement. We could also tidy up the paths using relative commands and so on.

Yup for me it's just about data - right now I have 30,000+ open source things in my DB and I've run them all through this font maker, then svg to json maker - and w/out the precision limiting I'd have a lot bigger db!

Hey a side question: right now the svg font maker is taking folders and putting them into a font file, which I then parse to json. But the problem I discovered is that fonts reverse the Y axis - caramba! If I reverse that on the client side w/ SVG.js - then it screws up the exporting of PNGs. Is there a way so parse through my actual data and reverse the axis - so my json would be correct? Sorry if this sounds confusing, too many hours working on this :)

I did that some time ago. I guess you have path data which you want to reverse? You can use size(width, -height) to reverse the path but you have to make sure that your baseline stays the same so you need to position the glyph correctly after reversing.

You can use in the svg.textmorph.js plugin where I used this technique. Short clip:

        // p is the glyph as svg path
        if(box.height && box.width)
          p.size(box.width * scale, -box.height * scale)

        p.move(x,(uPerEm - box.y - box.height)*scale)

Thanks @Fuzzyma - I'll check that out!

@Fuzzyma have you ever batch resized and centered svg folders? I think converting to icon fonts is beginning to look like perhaps it isn't the best way for me to go - I just need some kind of uniformity to my SVGs, as we are grabbing open source stuff from all over.

what are svg folders? I always think about directories filled with svg files but somehow this doesnt fit right :D. And no - never did batch resizing anywhere - but svg.js also runs on node.js ;)

ah sorry directory is the same as a folder :) Hmmm that does give me an idea - I'll keep looking at this!

Jeah i know but I never centered a folder - maybe the files in the folder :D

// EDIT: svg.js runs in node with svgdom but currently not with the v3 branch

actually I used your idea with https://github.com/fontello/svgpath - everything loading great - a little extra bloat, but not as bad as before so this is good. Thanks for the advice, it helped me a lot today 👍

Glad I could help :)

I just had the idea of adding pre processes to the export function. You can register a pre process which is executed on the given svg Dom before it is exported. So you could add those functions (from other libs) without us needing implement everything in the core.

That sounds pretty cool. It's nice to be able to clean stuff up and change things before export. Do you have a blog or something? Seems like you should, lol.

Haha I guess I lag the writing skills to make it interesting enough ^^

__Question__: can we assume rounding on numeric/point/path attributes is only of importance when exporting SVG data? Or is there also a use case for rounding on in-memory elements?

I would propose to add it for exports only and when there are still requests we can think about that again. It seems like all people asking for this feature where talking about export

I agree. After thinking about it, I see no use-case where this would be needed at runtime. It can even be as simple as doing a regex replace on the generated output string.

Hehe I want to see the regex that can replace all numbers correctly

m0-5.3e-10,.22.67-34.3.3e2

Those numbers are not an issue at all. The problem would be:

-5.300000000530001e-10

And approaching it from a Regex point of view, we'd only need to replace 5.300000000530001 with 5.3. The main goal would be to reduce the size of the output, not make calculations.

True. That would be something like this:

// a is the full match, b the decimal places
var round = function(a, b) {
  return '.'+Math.round(b*1000)/1000
}

// searching for a decimal point and digits and call round on every match
return pathData.replace(/\.(\d+)/g, round)

Exactly :). If we'd run this over the whole output of svg(), we're done.

And what about <text>2.0000341</text> ;)

I woke up with that one :) We'll have to do it in two go's then. First grab the node content (e.g. <([^>]+\.\d{2,}[^>]+)*>) then replace the decimal values in each instance.

I am quite certain that this is possible but I dislike it. We loop through every node anyway when exporting. So we can just do it node for node.
Imo it's a bit much regex magic :).
Well - tests will show what work and what not and which is the fastest way :).

Btw did you read about the idea to implement the possibility to add export modifier? Rounding would be one of these

Yeah, it's not a good idea anyway. Data attributes...

An export modifier sounds great. We can provide the possibility to pass a closure which is called on every exported element, so the element can be modified by the user as well. Just like it used to be implemented in the import plugin: https://github.com/svgdotjs/svg.import.js#block.

export sounds a lot better than nothing! My only use case for the smaller paths inside of the UI would be if I have a contact sheet of 20 designs, all with let's say 20+ layers of relatively complicated shapes. Under such a use case - I did find that going from 1.0 to the updated "full monty" numerically, I saw a slow down in the browser.

Well in such heavy cases it might help to hide the sheets you do not need atm

hey guys @Fuzzyma @wout -the rounding fix works great - since I'm lazy loading it doesn't have to run on that many paths - but it sure cleans up the huge numbers I was seeing, and makes exported files smaller and more manageable. Thanks for posting the workaround here. Just a few weeks from launch now - anybody up for beta testing in a couple weeks ? :)

Sounds great! Looking forward to see the end result :). I am a bit short on time to do beta testing though...

Ah don't worry I would not ask anybody to actually perform testing for free. But getting a look would be great.

The hack does fail on some images though, depends a bit how they were coded before they come in to SVG.js.

One question: where are these decimals being added to begin with? The art I'm adding all has 1 decimal place. Then it goes through SVG.js, and ends up w/ like 9 decimal places. So is there any way to cut this off BEFORE it gets added to begin with? Other than scaling, I don't really need transforms - there is no animation in what I'm doing. Any chance I could just eliminate these decimals from ever being added?

Try 0.1 + 0.2 and there you go. A simple move command could do that

ok cool but where would I add this?

I just stated where these rounding errors come from. And thats a thing of javascript. We cant change that.
You could try to change SVG.Numbers valueOf method to round for you.

ok thanks for that, I'll check it out.

Have we made any progress on this?
If we force all outputs to be SVG.Numbers, we could just add a precision option as a global variable.
This to me is the one time where using a global variable makes sense.

SO SVG.Number.Precision = undefined doesn't call tofixed on generated numbers,
but SVG.Number.Precision = 3 will force 3 decimal places of precision for every number everywhere.

That would mean that we have to use SVG.Number everywhere. I dont know if this is possible. Just think about the matrix values which are also numbers.

Also the x method:

    x: function (x) {
      return this.attr('x', x)
    },

Would then be:

    x: function (x) {
      return this.attr('x', new SVG.Number(x))
    },

I think it would hurt performance quite a bit

I think just having a helper to achieve this is our best bet then. I can't see why having lots of decimals would hurt performance, as long as the number is representable as a float32. As @mspanish was saying, her app seemed to slow down alot when there were lots of decimals, but maybe that was because the extra decimals required a float64 representation instead of a float32?

Thats just an idea, but maybe we should be doing more to limit the precision of the numbers we write into the dom?

Because after all, a rendering penalty is almost always going to be larger than a code execution time penalty I think. (statement requires testing to verify) :P

I think easiest we can do for now is adding an export modifier.
Additionally we can upgrade SVG.Number with a precision conversion when needed

As discussed with @Fuzzyma, we were thinking of including a plugin with a number of common export modifiers. So when you have an element and you want to output the svg string, you can just call:

element.svg()

But you can also modify the output by calling things like:

element.svg([bakeTransformations('all'), precision(3), yourFunc])

The exact format of yourFunc is still to be decided, but this is only really necessary for exporting purposes.

TL;DR

  • [x] implement export modifier for SVG.Element.svg() (#915)
  • [x] implement rounding for nodes (#916)

Long:

So this is my proposal:

As we agree, we want export modifiers which bring the possibility to change every node before export.
This will enable the user to round values before exporting them.

To bring these cases together I want to bring up wouts recommondation again:

By using a method, you can round before exporting:

var svg = path.round(2).svg()

Or non-destructively:

var clone = path.clone().round(3)
var svg = clone.svg()
clone.remove()

We can also provide deep rounding for elements inheriting from SVG.Parent. Something like:

nested.round(2, true)

This together with the export modifier an result in something like this:

// instance is the svg object and not the node itself
element.svg((instance) => {
  instance.round(2)
})

If you want to execute multiple modifier you can call these in your function:

element.svg((instance) => {
  doThis(instance)
  doThat(instance)
})

If you want to replace the node with another node you can return this new node:

element.svg((instance=> {
  var newInstance = new SVG.Rect()
  return newInstance 
})

If you want to drop this node from exporting you can return false explicitly:

// drops every node
element.svg((instance=> {
  return false
})

If you believe it or not, you can round your attributes now!
Try it out on the 3.0 branch :)

One last thing to mention: svg() does clone the nodes before passing them to the export modifier. So every change you do to the elements will not be reflected to the actuall dom. So do what you want - you cannot screw up :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

marciosalinodias picture marciosalinodias  Â·  4Comments

mtbradle-ge picture mtbradle-ge  Â·  6Comments

Fuzzyma picture Fuzzyma  Â·  8Comments

AlessandroDM picture AlessandroDM  Â·  5Comments

imkiven picture imkiven  Â·  8Comments