JavaFX Image (PNG) transparency crispness being lost when rendering


Question

I am loading and displaying a transparent PNG on a gradient background with the following result:

enter image description here

If I open the same file in Paint.NET, add the background it looks like that:

enter image description here

Somehow, JavaFX makes the image loose crispness, I fear this may be an issue for ALL images in my app and its just that its most visible in this particular case.

Here is extracted code showing how I load this particular image:

ImageView imgDownload = new ImageView(this.getClass().getResource("/img/docstore/document_downloaded_btn.png").toExternalForm());
imgDownload.setFitWidth(59);
imgDownload.setFitHeight(32);
GridPane.setHalignment(imgDownload, HPos.CENTER);
grid_item.add(imgDownload, 3, 0);

For reference, here is a link to the original image.

I'm looking for an answer highlighting a possible reason this is happening

1
4
3/1/2013 2:33:34 PM

Accepted Answer

Update

JavaFX images in Java 8 are now rendered crisply in all cases.

The original issue described in the question has been addressed.

The bug Dreen filed regarding this behaviour, RT-28765 Incorrect subpixel placement for an Image, was closed as a duplicate of RT-19282 [StackPane] unwanted blur with ImageView. RT-19282 was closed as fixed for Java 8.

I tested Java 8 build 108 on Windows 7. The sample application in this answer now displays correctly (no fuzzy image sometimes offset by half a pixel in the x or y direction).


This is an excellent question and very curious behaviour.

I put together a sample program which offers a possible explanation and a workaround.

The output of this program after running it and dragging the border around a little to resize it, is as below. The fuzzy cloud on the left is a standard ImageView placed in a GridPane. The crisp cloud on the right is an ImageView wrapped in my workaround fix class (CenteredRegion), placed in the same GridPane.

fuzzyAndCrispClouds

While the above image is displayed, the output of the program is:

Layout SnapToPixel: true
...
fuzzy: New Bounds: BoundingBox [minX:14.5, minY:12.5, minZ:0.0, width:59.0, height:32.0, depth:0.0, maxX:73.5, maxY:44.5, maxZ:0.0]
fuzzy: xDisplacement: 0.5, yDisplacement: 0.5
crisp: New Bounds: BoundingBox [minX:84.0, minY:13.0, minZ:0.0, width:59.0, height:32.0, depth:0.0, maxX:143.0, maxY:45.0, maxZ:0.0]
crisp: xDisplacement: 0.0, yDisplacement: 0.0

As you can see, the fuzzy image has not been pixel aligned and is offset by half a pixel in the x and y direction, causing it to look fuzzy. This is despite the grid region having it's snapToPixel setting as true.

As the stage containing the layout is dynamically resized, the fuzzy image will alternate between being pixel aligned and not being pixel aligned (depending on the oddness or evenness of the width and height of the scene). This creates an an annoying shimmering effect as you resize the stage borders, due to the fuzzy image continuously alternating between being fuzzy and clear.

The behaviour of the fuzzy image would seem to be a bug when snapToPixel is set to true on the parent container, so I'd advise filing a bug against the JavaFX runtime project and linking back to this stack overflow question in the bug and placing a link to the created bug in a comment.

The crisp version remains crisp because it is housed in a custom region implementation which ensures pixel alignment.

Test system was win7 + jdk8b77 + ATI HD4600 graphics card.

import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.value.*;
import javafx.geometry.*;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import javafx.stage.Stage;

public class TransparentPngSample extends Application {  
  public static final String IMAGE_LOC = "http://i.imgur.com/byY8whh.png";

  @Override public void start(Stage stage) {
    Pane layout = createSceneContent();
    stage.setScene(new Scene(layout));
    stage.show();

    System.out.println("Layout SnapToPixel: " + layout.snapToPixelProperty().get());
  }

  private Pane createSceneContent() {
    final Image cloudImage = new Image(IMAGE_LOC);
    final ImageView fuzzyCloud = new ImageView(cloudImage);
    final CenteredRegion crispCloud = new CenteredRegion(new ImageView(cloudImage));

    GridPane layout = new GridPane();
    layout.setHgap(10);
    layout.setVgap(10);
    layout.addRow(0, fuzzyCloud, crispCloud);
    layout.setAlignment(Pos.CENTER);
    layout.setStyle("-fx-padding: 10px; -fx-background-color: slategrey;");

    fuzzyCloud.boundsInParentProperty().addListener(new BoundsReporter("fuzzy"));
    crispCloud.boundsInParentProperty().addListener(new BoundsReporter("crisp"));

    return layout;
  }

  class CenteredRegion extends Region {
    private Node content;

    CenteredRegion(Node content) {
      this.content = content;
      getChildren().add(content);
    }

    @Override protected void layoutChildren() {
      content.relocate(
        Math.round(getWidth()  / 2 - content.prefWidth(USE_PREF_SIZE)  / 2), 
        Math.round(getHeight() / 2 - content.prefHeight(USE_PREF_SIZE) / 2)
      );

      System.out.println("crisp content relocated to: " +
        getLayoutX() + "," + getLayoutY()
      );
    }

    public Node getContent() {
      return content;
    }
  }

  class BoundsReporter implements ChangeListener<Bounds> {
    final String logPrefix;

    BoundsReporter(String logPrefix) {
      this.logPrefix = logPrefix;
    }

    @Override public void changed(ObservableValue<? extends Bounds> ov, Bounds oldBounds, Bounds newBounds) {
      System.out.println(logPrefix + ": " + 
        "New Bounds: " + newBounds
      );
      double xDisplacement = newBounds.getMinX() - Math.floor(newBounds.getMinX());
      double yDisplacement = newBounds.getMinY() - Math.floor(newBounds.getMinY());
      System.out.println(logPrefix + ": " + 
        "xDisplacement: " + xDisplacement + ", " + 
        "yDisplacement: " + yDisplacement
      );
    }
  }          

  public static void main(String[] args) { launch(args); }
}
10
9/30/2013 11:19:28 PM

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon