Using FitWidth and FitHeight results in a snapshot with a smaller rendered image than expected

4 days ago 8
ARTICLE AD BOX

Intro

The following code implements a draw-by-brush on an image using a canvas. The Idea is to stack a Canvas object over an ImageView, draw on it, and finally render the StackPane (image + canvas) using the SnapshotParameters and WritableImage.

Code

The saveSnapshot(StackPane stack, int width, int height) method simply creates a WritableImage with the specified width and height (matching the original image's dimensions), and takes a snapshot of the StackPane on it.

This is the full code:

import javafx.application.Application; import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; import javafx.embed.swing.SwingFXUtils; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.SnapshotParameters; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.Button; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.image.WritableImage; import javafx.scene.input.MouseButton; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.stage.Stage; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.Objects; public class DrawOverImageCanvas extends Application { private double lastX; private double lastY; @Override public void start(Stage stage) { Image image = new Image(Objects.requireNonNull(getClass().getResourceAsStream("yourPath"))); int height = (int) image.getHeight(); int width = (int) image.getWidth(); System.out.println("width: " + width + " height: " + height); if (image.isError()) System.err.println("Could not load image: " + image.getException()); ImageView imageView = new ImageView(image); imageView.setPreserveRatio(true); imageView.setSmooth(true); imageView.setFitWidth(500); Canvas canvas = new Canvas(); GraphicsContext gc = canvas.getGraphicsContext2D(); gc.setLineWidth(3); gc.setStroke(Color.RED); canvas.setOnMousePressed(e -> { if (e.getButton() == MouseButton.PRIMARY) { lastX = e.getX(); lastY = e.getY(); } }); canvas.setOnMouseDragged(e -> { if (e.getButton() == MouseButton.PRIMARY) { double x = e.getX(); double y = e.getY(); gc.strokeLine(lastX, lastY, x, y); lastX = x; lastY = y; } }); StackPane stack = new StackPane(imageView, canvas); stack.setAlignment(Pos.CENTER); stack.setStyle("-fx-background-color: transparent;"); DoubleBinding imageWidth = Bindings.createDoubleBinding( () -> imageView.getLayoutBounds().getWidth(), imageView.layoutBoundsProperty() ); DoubleBinding imageHeight = Bindings.createDoubleBinding( () -> imageView.getLayoutBounds().getHeight(), imageView.layoutBoundsProperty() ); stack.minWidthProperty().bind(imageWidth); stack.prefWidthProperty().bind(imageWidth); stack.maxWidthProperty().bind(imageWidth); stack.minHeightProperty().bind(imageHeight); stack.prefHeightProperty().bind(imageHeight); stack.maxHeightProperty().bind(imageHeight); canvas.widthProperty().bind(imageWidth); canvas.heightProperty().bind(imageHeight); Button saveButton = new Button("Save PNG"); saveButton.setOnAction(e -> saveSnapshot(stack, width, height)); Button clearButton = new Button("Clear Drawing"); clearButton.setOnAction(e -> { gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); }); VBox root = new VBox(10, stack, new VBox(5, saveButton, clearButton)); root.setAlignment(Pos.CENTER); root.setStyle("-fx-padding: 10; -fx-background-color: #333333;"); Scene scene = new Scene(root); stage.setTitle("Draw over Image with Canvas"); stage.setScene(scene); stage.sizeToScene(); stage.show(); } private void saveSnapshot(StackPane stack, int width, int height) { SnapshotParameters params = new SnapshotParameters(); params.setFill(Color.TRANSPARENT); WritableImage writableImage = new WritableImage(width, height); stack.snapshot(params, writableImage); BufferedImage bufferedImage = SwingFXUtils.fromFXImage(writableImage, null); File outFile = new File("500.png"); try { ImageIO.write(bufferedImage, "png", outFile); System.out.println("Saved to: " + outFile.getAbsolutePath()); } catch (IOException ex) { ex.printStackTrace(); } } }

Issue

The issue is that when the image's FitWidth and FitHeight are set to values smaller than their actual width and height, the snapshot captures the entire StackPane, using the FitWidth and FitHeight values rather than the image's actual width and height, since the StackPane size is bound to the ImageView's.

The resulting snapshot image will keep its original size (since the WritableImage size matches the image's), but with a StackPane rendered inside it because FitWidth and FitHeight were smaller than the image's actual height and width.

The screenshot below is from my PC's file explorer, showing different results generated by using various Fit Width values from 200 to 500, compared to the original image at its actual size. All the images have the same dimensions, but the rendered ones StackPane varies depending on the value used for the setFitWidth.

enter image description here

Is there a way for the rendered StackPane to occupy the full height and width as the original image?

Read Entire Article