Search code examples
javajavafxjavafx-css

Setting the -fx-shape property with SVG gives a blurry result in JavaFX


I want to set an icon for my button using SVG and CSS(!). This is my code:

public class NewMain extends Application {

    @Override
    public void start(Stage primaryStage) {
        var path = "M0,2 H8 V4 H0 Z M0,2 H1 V9 H0 Z M0,8 H8 V9 H0 Z M7,2 H8 V9 H7 Z";
        var svgPath = new SVGPath();
        svgPath.setContent(path);

        var region = new Region();
        region.setStyle("-fx-shape:\"" + path +  "\";"
                + "-fx-scale-shape: false;"
                + "-fx-pref-width: 20px;"
                + "-fx-pref-height: 20px;"
                + "-fx-background-color: black");
        var button = new Button(null, region);

        VBox root = new VBox(svgPath, button);
        root.setPadding(new Insets(20));
        root.setSpacing(20);

        Scene scene = new Scene(root, 100, 100);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

And this is the result (zoom 400%):

enter image description here

As you see svgPath is rendered correctly, but the icon in the button (with the same svg) is blurry. Could anyone say how to fix it?


Solution

  • Background

    For general info on why shapes in JavaFX can be displayed fuzzy and some hints on how to address this see javadoc sections

    Summarizing: thin strokes for shape boundaries (e.g. 1 pixel in width), will appear fuzzy unless they exactly line up with the coordinate system (due to antialiasing). This can be addressed by either ensuring the strokes are lining up with the coordinate system so they fall exactly on a pixel completely covering it, or making the strokes wider so that they completely cover two pixels.

    If a hi-res "retina" display is used, the human eye won't be able to reasonably detect the fuzziness unless a screenshot of the image to pixels is made, then the pixels scaled up.

    Debugging layout bounds

    Sometimes you can debug issues by checking the BoundsInParent or BoundsInLocal of a node.

    Adding these lines to your sample code:

    System.out.println("Path local bounds:    " + svgPath.getBoundsInLocal());
    System.out.println("Graphic local bounds: " + button.getGraphic().getBoundsInLocal());
    

    Gives:

    Path local bounds:    BoundingBox [minX:0.0, minY:2.0, minZ:0.0, width:8.0, height:7.0, depth:0.0, maxX:8.0, maxY:9.0, maxZ:0.0]
    Graphic local bounds: BoundingBox [minX:6.0, minY:6.5, minZ:0.0, width:8.0, height:7.0, depth:0.0, maxX:14.0, maxY:13.5, maxZ:0.0]
    

    It can be seen that the graphic is larger, and the minY is falling on a non-integral number, which is the cause of the fuzzy rendering.

    Potential fixes or workarounds

    Now, if we don't position the shape in the graphic by adding to your style statement:

    + "-fx-position-shape: false;"
    

    The graphic bounds now match the path bounds and the lines are clean and not fuzzy.

    Path local bounds:    BoundingBox [minX:0.0, minY:2.0, minZ:0.0, width:8.0, height:7.0, depth:0.0, maxX:8.0, maxY:9.0, maxZ:0.0]
    Graphic local bounds: BoundingBox [minX:0.0, minY:2.0, minZ:0.0, width:8.0, height:7.0, depth:0.0, maxX:8.0, maxY:9.0, maxZ:0.0]
    

    clean image

    This is what -fx-position-shape has done (default is true):

    If true means the shape centered within the region's width and height, otherwise the shape is positioned at its source position. Has no effect if a shape string is not specified.

    So, in your case, it is this centering that is causing an issue. The graphic size is larger than the unscaled path size, so if the graphic height is an odd number of pixels, the shape will be offset by half a pixel and blurry (similarly for width).

    The default is to position the shape in the center and scale the shape, which is what you usually want for an SVG. One of the chief advantages of an SVG is the vectors can scale smoothly, otherwise a pixel-based representation such as a PNG may be better.

    Using the defaults for positioning and scaling the SVG gives:

    Path local bounds:    BoundingBox [minX:0.0, minY:2.0, minZ:0.0, width:8.0, height:7.0, depth:0.0, maxX:8.0, maxY:9.0, maxZ:0.0]
    Graphic local bounds: BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:20.0, height:20.0, depth:0.0, maxX:20.0, maxY:20.0, maxZ:0.0]
    

    This also renders clearly in this case as the scaled strokes are lining up with pixels. However, the desired aspect ratio is not obtained, so additional work would be required if you wanted that.

    clear

    There is an additional region CSS property -fx-snap-to-pixel, which defaults to true and is defined as:

    Defines whether this region rounds position/spacing and ceils size values to pixel boundaries when laying out its children.

    In the case of centering the shape position of a region based on an unscaled svgpath, it would appear that the -fx-snap-to-pixel setting does not apply to the centering algorithm.

    FAQ

    Do I understand it correctly, that if I want to have always a correct result I shouldn't use SVG for icons?

    No that is not what I am trying to say. I am just giving some reasons why some things can look fuzzy, even if SVG is used.

    There are sometimes workarounds for that (JavaFX internal layout implementations often have snap-to-pixel logic).

    To diagnose exactly why you have got the result you have, it is necessary to debug further. Then to address that, apply additional modifications to your code to address the issues found.