jfree / jfreesvg

A fast, lightweight Java library for creating Scalable Vector Graphics (SVG) output.
http://www.jfree.org/jfreesvg
GNU General Public License v3.0
319 stars 58 forks source link

Could/should SVGGraphics2D dimensions use double precision? #37

Closed christianAppl closed 3 years ago

christianAppl commented 3 years ago

TL;DR: Why not use double precision for width and height in SVGGraphics2D?

When applying the matrix: PlanarMatrix transformation = new PlanarMatrix().translate(x, y).scale(1.0, -1.0); To a created svg, I encountered an odd behaviour and that lead to some questions.

The matrix is used to mirror shapes on the y axis in place. Meaning: The x and y translation is used to translate the mirrored shapes back to their original position, after they have been mirrored. This matrix had originally been defined and applied to paths rendered to BufferedImage objects.

However if this same method is applied to the SVG, then the position will be off.

The reason is rather obvious: My translation assumes that the Graphics dimensions equal the dimensions of the contained path. but the svg paths (as is true for paths in general) does not really need to have integer dimensions. A SVG could easily have a height of 5.678, but the used Graphics would have a height of 6px.

Obviously I have to adapt this to reflect the fact, that the path dimensions and the Graphics dimensions can differ. Easy and not a problem at all.

Question: But what about SVGGraphics2D? Why does it use Integer dimensions in the first place? (I am fully aware, that I am talking about differences <1 here, and that this might seem to be a non-issue - but the differences are just big enough to be visible and that bothers me. (although I can fix this on my end however.))

jfree commented 3 years ago

I don't recall exactly but I suppose my thinking was influenced by:

I'm not against switching the coordinates to use floating precision, but what are your thoughts on a robust implementation that takes care of the last point above?

christianAppl commented 3 years ago

Scientific notation: I really did not know, but I found an answer. CSS seems to support the scientific notation: grafik As SVGs should be using the same number format, this should be okay?

Precision: Using floating point precision clearly has it's issues... I already tested that out and encountered a number of problems, where browser rasterizers reduced precision (by rounding, antialiasing and so forth) and caused gaps and seams, where mathematically none should exist. I can see that my suggestion has flaws.

I want to derive from my initial question/suggestion: This should not be forced upon a user of this library - I can understand how this would only cause further issues for many use cases, if it was enforced.

However the option to use double precision for width and height would be nice! I already had a look at your solution for "transformDP" and "geometryDP" why not copy that logic? (I Initially had the thought, that I could find a way to improve upon that, but your solution already is absolutely fine, nice and adaptable as it is.)

Suggestion: The type of the fields width and height could be changed to double precision and additionally a "dimensionDP"/"svgDP"/whatever could be introduced. The dimensionDP would default to 0 (integer precision) and could be set to any value, that is to the users liking.

The only thing concerning me about the solution via those DP fields is the actual handling of the eliminated fractional digits (mostly because currently I don't really know how they are handled.) Would be nice being able to select how such values shall be treated (FLOOR, CEIL, ROUND) maybe even on a case by case basis. ie: round up to transformDP digits, but always round naturally to geometryDP digits)

Example using the case at hand: Whatever we do, we most likely don't want to cut off paths, so if we also want to use integer precision, we will always prefer providing larger dimensions than required. A possibly set double width of 68.79 should always be rounded up to the next higher integer (ceil).

Usecase: My path has Rectangle2D.Double bounds and I want to create a SVG with integer precision, that is just big enough to contain it.

mhschmieder commented 3 years ago

I would say there's an expectation of the availability of floating-point precision in SVG these days.

I’m just adding some perspective on the overall validity of the original question of supporting the precision, whether optional or default, separate from the caveats and also the limitations of overriding the AWT graphics2D class which has so many integer-based methods.

I did an awful lot of SVG work in my last job (which only ended a few weeks ago), in the JavaFX context vs. AWT/Swing, so I belatedly looked into what was going on behind the scenes in the API calls to see if it is informative towards this AWT context and/or the potential for browser rendering gaps.

In JavaFX, the Shape classes can set String-based content that is presumed to be SVG commands. These in turn get forwarded to the QuantumToolkit which then returns to the JavaFX Path2D class to do basic input parsing of the SVG commands in similar fashion to what we are used to when writing output parsers (the source code for the JavaFX Path2D class is open so is easy to find in the run-time JAR and to look at in an IDE, and I recommend doing so).

Everything is declared as single-precision, and my recollection is that this is the SVG standard par the W3C (yes, in Java double-precision is usually MORE efficient, but a lot of standards, libraries, and languages prefer single-precision).

Floating-point precision was absolutely critical for shrunk-down 16x16 vectorized icons in particular, in my last job. And as the use cases of an SVG output parser in downstream workflows is wide-open, I think it highly likely that a lot of consumers of SVG output from Java AWT (or from JavaFX via AWT using the excellent jfxConverter toolkit that I have also made some contributions to but did not author), will want, need, and/or demand floating-point precision vs. integers.

Speaking from my own experience, in my long-term job that I held right before the recent short-term startup company job, my review committee for end user beta feedback had me remove SVG support from the app (not the code; just the one line that exposed the menu export method), due to it not being as clean (gaps, integer-slamming, etc.) as PDF, EPS, and other vector output formats. But as SVG is an important part of today’s ecosystem, and is often the only valid choice for a lot of application stacks, I think it’s important we be as up to date as possible in addressing the downstream client needs.

The almost universal dominance of retina displays in today's world, means the question of integer addressing of pixels vs. floating-point has a far larger impact than when this library was initially authored.

mhschmieder commented 3 years ago

Some useful advice in this o'Reilly article on SVG and numeric precision:

https://oreillymedia.github.io/Using_SVG/extras/ch08-precision.html

"For reliable results cross-browser, use numbers with no more than 2 digits after the decimal and four digits before it."

Note that they do mention that scientific notation is supported by the spec (which, as I remembered correctly, is stuck at single-precision as well), as did several other references I found.

Additionally, the SVG's that I consumed in my last application development role often had scientific notation as the SVG's were produced by a UX mockup tool and that was how it exported the graphics.

https://github.com/cjlano/svg

I recommend looking at the Python code in the open source SVG parser linked above, even though it is input-based vs. output-based, as it covers scientific notation using Regex -- maybe not as important on the output side, but sometimes a parser has utility methods that collate information and then have to streamline to remove redundancies, so there might be some parsing of that nature at the output side as well. At any rate, as with the JavaFX Path2D code, it's another example of working with floating-point precision with SVG, and whether any assumptions get made.

As for how to enhance an AWT Graphics2D based approach to output parsing with floating-point support, you can see how I did this for EPS in my open source parser at my GitHub page:

https://github.com/mhschmieder/epstoolkit

Most of the work is decoupled from AWT and Graphics2D and placed in an EpsGraphicsOperators class. I think I borrowed this approach from the jFreeOrg PDF library, but I'll re-check that later.

If we needed to let the consumer of the SVG parser library decide on floating-point vs. integer, I think that could be cached as a RenderingHint like so many other things, and deferred until the lowest-level write-to-file operations that output the final SVG commands. The renderingHints approach also makes it easier to specify an algorithm for the integer-conversion ("algorithm" probably isn't the best word for a simple choice of floor, round, ceil, etc.).

christianAppl commented 3 years ago

TL;DR: Maybe read my next comment first and come back to this one, when and if you are interested what lead to my confusion.

Thank you very much for your response! I'm not that fluent in all things "web-development" and SVG, therefore a more experienced perspective is most welcome! I already tried to implement dimensions using doubles for SVGGraphics2D and tbh I expected more issues. There is one method in "SVGGraphicsConfiguration" that returns a Rectangle (integer precision) which I solved quick and dirty via: grafik

As most other methods, that would insist on using integer precision are mostly input methods (as far as I can see) whose parameters are interpreted and translated to the SVG context anyway: grafik (translation to SVG floating point precision in method call "fill()".

Nothing would however stop one, to implement an alternate method of the same name using double (or float) parameters. This doesn't bother me much however, as for my usecase I mostly require the ability to draw and fill "Shape" objects which already use double precision most of the time.

This ticket is really mostly talking about the fields "width" and "height" of "SVGGraphics2D" which currently are using integer precision where double precision could be used easily without having to change much about the inner workings of the rest of the class: grafik

I can see however, that alternate double precision draw"SomeShape" / drawImage methods could be usefull. Which however - is not one of my requirements yet.

Concerning "thin white lines" in the browser presentation of SVGs This is beyond the scope of this ticket! The SVG has not even been created using this library! This also currently has no priority - I will deal with this and fix it another time. Hints on how to do it are always welcome however. ;)

Thank you very much for your advice on precision (2 fractional digits)! I already found that recommendation, but currently it unfortunately didn't help me much. I wrote the SVG manually, that creates the following shape and I am absolutely certain, that those rectangles share identical borders (the constructing points are not close to one another, they are using identical coordinates). Therefore they should construct a seamless plane, but - as you can see - they don't. I played arround with "shape-rendering" but some settings seem to even worsen the issue. My assumption is: the SVG is fine, but not browser ready - there is something I am not seeing here: grafik grafik

As far as I am concerned the issue could even be found elsewhere entirely (transform-origin handling of matrices)... I could not reproduce such behaviour using natural numbers - which strongly points to the SVG in my opinion.

Edit: Typing error - sorry the matrix should translate to X: 25.68 instead of 25.67 Even more frustrating though! This leads to exactly the same result - even though I used a wrong value here. (Even tried clearing cache and switching browsers to rule that out.) I'm not entirely convinced, that chrome/firefox care much about my fractional digits at all. :)

christianAppl commented 3 years ago

Now I will derail my own ticket entirely - sorry about that!

However - this more or less questions whether or not the intention of this issue is relevant at all: @mhschmieder Would you recommend using natural numbers for SVGs wherever possible? I read through some of the links you posted and can understand why a perfectionistic view of "I want maximum precision wherever possible" can backfire terribly.

I'm aware, that somewhere along the line the SVG needs to be rastered to pixels, which must always result in "a loss of precision" - as one can't display 0.34 pixels.

TL;DR: What is the use of a feature, that no SVG viewer is expected and can be expected to display properly?

Edit: I think I answered my own questions and learned a thing or two... Thank you @mhschmieder.

Keep it simple! use natural numbers wherever possible and don't bother forcing things upon your result, that will overwhelm your rasterizer. Yes the paths will not be perfectly precise and you are loosing precision, but atleast it will work.

Why I can only blame myself here: It is obvious, that the rasterizer must make a binary decision at the end - shall I paint the pixel or not? When setting natural numbers for my rectangles there is no seam and everything is nice and uniform, whenever I introduce fractions (for example (simply) by zooming to 110% in the browser) white lines will appear.

The rasterizer asks: "Shall I paint the pixel?" and my answer is: 0.35... the rasterizer could now round up or down or try to blur or fill the pixel partially by introducing an alpha value. So: Either the rectangle will be too small, too large and/or white lines will appear.

I suppose my answer is: From my point of view this issue can be closed - the question seems to be irrelevant. A SVG path does allow such values, but whether or not it does is irrelevant. The results - of rastering real number coordinates and path construction parameters - most likely won't be what one might expect. (This possibly is only true for my case.) I don't know whether it is the task of this library to help circumvent such issues, or if the user should ensure that the used paths work.

This whole issue is caused by: Poor decisions based on the values I had and not on the results I wanted.

mhschmieder commented 3 years ago

I meant to add that the rejection of SVG by my former employer was before I switched to the JFreeOrg library. I did that on my own while unemployed last summer, and it made a big quality and performance difference.

This is my second or third SVG Export solution (the others were all based on Apache Batik) and definitely my final one, which is why I'm motivated to help where I can.

Just remember that some of my notes about precision are situationally important. For instance, showing every detail of an IFFT (Inverse Finite Fourier Transform, often used to show an Impulse Response), is critical from a data analysis and comprehension point of view vs. an aesthetic basis. And 16x16 down-sized vector graphics icons may be sharper when left to something like a JavaFX rendering engine to properly downscale, if the original larger-sized SVG vector graphics are precise.

On the other side of that issue, a small vector path/shape that is upsized quite a bit, is going to start looking pretty jaggedy if it was slammed to integers. So that's an aesthetic issue when dealing with GUI components that are defined as SVG paths (which in turn might be consuming exports from an app, using an SVG writer). But also a detailed chart with millions of data points (such as an IFFFT), where the point of interest is quite momentary in the time signal, won't give the end user any more detail once zoomed, if the necessary precision was lost in translation.

christianAppl commented 3 years ago

Concerning this ticket: What I can say concerning this issue is. It would be relatively easy to handle the dimensions with double precision instead of int and for some usecases this might simplify the handling of the SVGGraphics2D. (as explained above.) I agree, that precision should be limited similar to how precision and fractional digits are already limited in the current solution concerning other values.

If integer dimensions are required, a value of 0 for the maximum fractional digits can be set.

I would not opose such a solution and would still claim, that this would be closer to the SVG specification, as SVGs indeed need not have integer dimensions.

To also have said the following:

As you already stated - whether and if something is a good idea depends highly on the context and the intended purpose of the SVG.

Concerning my other ramblings: I can imagine, that other rasterizers might handle this situation in another way, so that more precise results are possible and/or necessary. Maybe this ticket still has relevance. But you more or less proved to me, that in my specific usecase "precision" is the cause of my issues and not the solution. (Creating SVGs for usage in html, that is.) Maybe there would be options to improve the appearance of such SVGs in Firefox/Chromel. But from my point of view flattening the shapes seems to be the easiest way to circumvent such issues altogether. I could find a number of suggested workarrounds, that were not really to my liking and that partially didn't even convince me, that they would work. (Adding a 1px border for example.)

Having read some articles and discussions concerning this topic convinced me, that I should rather prepare the values to be fit being displayed without issues, than trying to convince browsers to handle it differently. There even exist tools to "clean" SVGs to make them "browser ready" which more or less simply round all contained path coordinates up or down. Therefore I question whether my goal to preserve as much precision as possible was the right approach in the first place. Superficially - sure... more precision always is nice - but in the end: I want to display shapes, so that they look mostly identical to their original appearance. If I can reach that goal, it does not really matter that much, whether they are exactly and mathematically identical to their source. If also the required steps for that are simpler, than all that I'm currently doing...

Maybe this issue still has merit - I don't know how fx webviews/other components are handling this tbh. I know however how Swing/AWT would be handling paths. And that also points me to:

Your SVG is rastered at the end - be ready for that and expect losses.

This is more or less what I learned here. :) (Even though it is indeed painfully obvious.)

mhschmieder commented 3 years ago

Thanks for the thoughtful response. I have improved the clarity of my previous response and have augmented with an additional point of interest; still situational in nature.

Which just leads me to conclude that the user domain will determine the need, but it should default to what it has been, for the element of least surprise.

And using the RenderingHint mechanism to help isolate the changes (along with floating-point versions of the key drawing commands), should also allow for backward compatibility of results (though this would need testing).

jfree commented 3 years ago

Thanks for all the inputs. I'm going to attempt switching the width and height attributes to double precision.

One side-note: all the methods that have int parameters and are marked with @Override annotations come from the Java2D Graphics2D API and won't ever be changed in JFreeSVG. Most of them are, in fact, there to ensure the Graphics2D class maintains compatibility with the older java.awt.Graphics class. You can generally perform the same operations, but with double precision, using the draw(Shape) and fill(Shape) methods that were introduced by Java2D (back in 1999).

mhschmieder commented 3 years ago

Sounds like a good plan, David. And of course this is what I was alluding to when mentioning limitations (integer basis) of AWT's Graphics base class earlier. I think I closely followed your own methodology when I did my EPS library over again from scratch last summer. It allows for a very clean decoupling of integer-vs-float parameter passing and handling, using draw() and shape() as primary "heavy lifters" that then forward to format-specific writers outside the Graphics class hierarchy.

As for scientific notation, SVG supports it and many of the numbers I received in SVG icons exported from the UX tool had them, but there was no real rhyme or reason to it, so it was clearly based on nothing mort than space savings in a text-based file. There may be other contexts where scientific notation is desired and should be consistently applied.

jfree commented 3 years ago

Fix will be included in version 5.0