javafxjavafx-2

JavaFX ImageView without any smoothing


Is it possible to render a scaled image in an ImageView in JavaFX 2.2 without any smoothing applied? I'm rendering a 50x50 image into a 200x200 ImageView, with setSmooth(false), so each pixel in the source image should map to a 4x4 square on the screen.

However, the resulting render still smooths the source pixel across all 16 destination pixels. Does anyone know of a way to do this without manually copying over each pixel into a new image?


Solution

  • In JavaFX versions up to at least 21, ImageView will always do some smoothing regardless of the smooth hint you provide to the ImageView (I don't know why the implementation works this way).

    Tested on JavaFX 21 on OS X 14, but reports show that the functionality works similarly for some other platforms, such as Windows.

    Perhaps it is a bug that ImageView will always smooth the Image, but the documentation doesn't specify exactly what smoothing does or doesn't do, so it's hard to say what its real intent is. You may want to post a reference to this question to the openjfx-dev mailing list or log an issue in the JavaFX issue tracker to get a more expert opinion from a developer.


    I tried a few different methods for scaling the Image:

    1. Scale in the Image constructor.
    2. Scale in ImageView with fitWidth/fitHeight.
    3. Scale by using the scaleX/scaleY properties on an ImageView.
    4. Scale by sampling the Image with a PixelReader and creating a new Image with a PixelWriter.

    I found that methods 1 & 4 resulted in a sharp pixelated image as you wish and 2 & 3 resulted in a blurry aliased image.

    robot-sampling

    Sample code to generate the above output.

    import javafx.application.Application;
    import javafx.geometry.HPos;
    import javafx.geometry.Pos;
    import javafx.scene.Group;
    import javafx.scene.Node;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.control.Tooltip;
    import javafx.scene.image.*;
    import javafx.scene.layout.GridPane;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    
    public class ImageScaler extends Application {
        private static final String IMAGE_LOC =
                "http://icons.iconarchive.com/icons/martin-berube/character/32/Robot-icon.png";
    
        private static final int SCALE_FACTOR = 6;
    
        private Image image;
        private int scaledImageSize;
    
        @Override
        public void init() {
            image = new Image(
                    IMAGE_LOC
            );
    
            scaledImageSize = (int) image.getWidth() * SCALE_FACTOR;
        }
    
        @Override
        public void start(Stage stage) {
            GridPane layout = new GridPane();
            layout.setHgap(10);
            layout.setVgap(10);
    
            ImageView originalImageView = new ImageView(image);
            StackPane originalImageViewStack = new StackPane();
            originalImageViewStack.getChildren().add(originalImageView);
            originalImageViewStack.setMinWidth(scaledImageSize);
    
            ImageView sizedImageInView = new ImageView(
                    new Image(
                            IMAGE_LOC,
                            scaledImageSize,
                            scaledImageSize,
                            false,
                            false
                    )
            );
    
            ImageView fittedImageView = new ImageView(image);
            fittedImageView.setSmooth(false);
            fittedImageView.setFitWidth(scaledImageSize);
            fittedImageView.setFitHeight(scaledImageSize);
    
            ImageView scaledImageView = new ImageView(image);
            scaledImageView.setSmooth(false);
            scaledImageView.setScaleX(SCALE_FACTOR);
            scaledImageView.setScaleY(SCALE_FACTOR);
            Group scaledImageViewGroup = new Group(scaledImageView);
    
            ImageView resampledImageView = new ImageView(
                    resample(
                            image,
                            SCALE_FACTOR
                    )
            );
    
            layout.addRow(
                    0,
                    withTooltip(
                            originalImageViewStack,
                            "Unmodified image"
                    ),
                    withTooltip(
                            sizedImageInView,
                            "Image sized in Image constructor - Image smoothing false"
                    ),
                    withTooltip(
                            fittedImageView,
                            "Image fitted using ImageView fitWidth/fitHeight - ImageView smoothing false"
                    ),
                    withTooltip(
                            scaledImageViewGroup,
                            "ImageView scaled with Node scaleX/scaleY - ImageView smoothing false"
                    ),
                    withTooltip(
                            resampledImageView,
                            "Image manually recreated as a new WritableImage using a PixelWriter"
                    )
            );
    
            layout.addRow(
                    1,
                    centeredLabel("Original"),
                    centeredLabel("Sized"),
                    centeredLabel("Fitted"),
                    centeredLabel("Scaled"),
                    centeredLabel("Resampled")
            );
            layout.setAlignment(Pos.CENTER);
    
            layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10;");
            stage.setScene(
                    new Scene(layout)
            );
            stage.show();
        }
    
        private Node withTooltip(Node node, String text) {
            Tooltip.install(node, new Tooltip(text));
            return node;
        }
    
        private Label centeredLabel(String text) {
            Label label = new Label(text);
            GridPane.setHalignment(label, HPos.CENTER);
    
            return label;
        }
    
        private Image resample(Image input, int scaleFactor) {
            final int W = (int) input.getWidth();
            final int H = (int) input.getHeight();
            final int S = scaleFactor;
    
            WritableImage output = new WritableImage(
                    W * S,
                    H * S
            );
    
            PixelReader reader = input.getPixelReader();
            PixelWriter writer = output.getPixelWriter();
    
            for (int y = 0; y < H; y++) {
                for (int x = 0; x < W; x++) {
                    final int argb = reader.getArgb(x, y);
                    for (int dy = 0; dy < S; dy++) {
                        for (int dx = 0; dx < S; dx++) {
                            writer.setArgb(x * S + dx, y * S + dy, argb);
                        }
                    }
                }
            }
    
            return output;
        }
    
        public static void main(String[] args) {
            Application.launch(args);
        }
    }
    

    Update with ideas on implementing your own image filter

    A JavaFX Effect is not the same as the Filter used for the Image loading routines, though an Effect to filter an image could be created. In JavaFX versions up to at least 21, there is no publicly documented API to support the creation of custom effect or image filter, so creating a custom effect or image filter may prove difficult.

    The native code for image support is open source as part of the openjfx project, so you could look at that to see how the filtering is currently implemented.

    You may also want to file a feature request against the JavaFX runtime project to "allow us to make our own 2D filters".