Reputation: 718
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?
Upvotes: 24
Views: 16841
Reputation: 159
Just now I was facing the same issue, but I'm not loading any image, I'm taking snapshots of Nodes. So, I wanted to expand on the @jewelsea answer with my workaround specifically for this use case:
// Get the screen on which the node is displayed
public Screen getScreenFor(Node node) {
Bounds bounds = node.localToScreen(node.getLayoutBounds());
Rectangle2D toRect = new Rectangle2D(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
return Screen.getScreens().stream()
.filter(screen -> screen.getBounds().intersects(toRect))
.findFirst()
.orElse(null);
}
// Node that w and h can be retrieved directly from Node, but I needed them because they may not match in my case
public WriteableImage snapshot(Node node, double w, double h, SnapshotParameters parameters) {
Screen screen = getScreenFor(node);
// Use this workaround only for HiDPI screens
if (screen.getOutputScaleX() != 1.0) {
double scale = screen.getOutputScaleX();
int scaledW = (int) (w * scale);
int scaledH = (int) (h * scale);
WriteableImage snapshot = new WriteableImage(scaledW, scaledH);
parameters.setTransform(Transform.scale(scale, scale));
node.snapshot(parameters, snapshot);
return snapshot;
}
return node.snapshot(parameters, null);
}
Edit: just a little rant, I'm quite tired of dwelling with JavaFX bs! Hope this could save someone a headache
Upvotes: 0
Reputation: 159566
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:
ImageView
with fitWidth/fitHeight.ImageView
.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.
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".
Upvotes: 39
Reputation: 957
No fix for ImageView, but it helped me a lot. After searching for ages, I stumbled upon this post: How can I disable antialiasing on a JavaFX Canvas?
For drawing the image on a canvas, the smoothing can be disabled since JavaFX 12
canvas.getGraphicsContext2D().setImageSmoothing(false);
Upvotes: 2
Reputation: 353
When you add the following constructor to Martin Sojka's answer you can simply pass the javafx Image to the constructor. Also despite the warnings about deprecated functions his answer still works fine (on JDK 1.8_121).
public PixelatedImageView (javafx.scene.image.Image image) {
super(image);
}
Upvotes: 0
Reputation: 266
I know this is a bit older, but I recently had a need for such ImageView, and the following little hack does exactly what I want on my (Windows) machine. No guarantees that it works everywhere.
import com.sun.javafx.sg.prism.NGImageView;
import com.sun.javafx.sg.prism.NGNode;
import com.sun.prism.Graphics;
import com.sun.prism.Texture;
import com.sun.prism.impl.BaseResourceFactory;
import com.sun.prism.Image;
import javafx.scene.image.ImageView;
@SuppressWarnings("restriction")
public class PixelatedImageView extends ImageView {
@Override protected NGNode impl_createPeer() {
return new NGImageView() {
private Image image;
@Override public void setImage(Object img) {
super.setImage(img);
image = (Image) img;
}
@Override protected void renderContent(Graphics g) {
BaseResourceFactory factory = (BaseResourceFactory) g.getResourceFactory();
Texture tex = factory.getCachedTexture(image, Texture.WrapMode.CLAMP_TO_EDGE);
tex.setLinearFiltering(false);
tex.unlock();
super.renderContent(g);
}
};
}
}
The trick here is that the texture gets re-used, so the linear filtering setting remains "sticky". Why NGImageView
couldn't simply pass the "smooth" flag to the texture's linear filtering setting is beyond me however.
Upvotes: 6