There are 2 identical snapshot methods in Node and Scene:
1. snapshot(SnapshotParameters params,
WritableImage image)
2. snapshot(Callback<SnapshotResult,Void> callback,
SnapshotParameters params,
WritableImage image)
If, for any reason, the snapshot capture fails, the exception is thrown in the first method. This makes it easier to catch and recover, if required.
While in the second snapshot method, the exception doesn't bubble up and there is no way to recover from the failure. The exception caused is caught in [Scene#addSnapshotRunnable|https://github.com/openjdk/jfx/blob/038556390ef30c912139fcb828ee4a41d8c5abc8/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java#L1438] and callback is never called. This leaves the caller with no feedback and no scope for recovery measures.
** Reproducible Sample **
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class SnapshotFailure extends Application {
private WebView webView;
@Override
public void start(Stage stage) {
webView = new WebView();
Scene scene = new Scene(webView);
stage.setScene(scene);
webView.getEngine().getLoadWorker().stateProperty().addListener(stateListener);
webView.getEngine().loadContent("<html><h1>test</h1></html>");
stage.show();
}
private final ChangeListener<Worker.State> stateListener = new ChangeListener<>() {
@Override
public void changed(ObservableValue<? extends Worker.State> ov, Worker.State oldState, Worker.State newState) {
// Sized intentionally too large; triggers NPE
webView.setPrefSize(100000, 100000);
webView.setMinSize(100000, 100000);
webView.autosize();
if (newState == Worker.State.SUCCEEDED) {
// ATTEMPT #1: Catchable, but NOT pulse-safe
System.out.println("======= SNAPSHOT #1 CATCHABLE =======");
try {
System.out.println("Starting snapshot...");
webView.snapshot(null, null);
System.out.println("Snapshot succeeded!");
} catch (Throwable t) {
System.out.println("Snapshot failed");
}
// ATTEMPT #2: Pulse-safe but NOT catchable
System.out.println("======= SNAPSHOT #2 UN-CATCHABLE =======");
try {
System.out.println("Starting snapshot...");
webView.snapshot(snapshotResult -> {
try {
if (snapshotResult != null) {
System.out.println("Snapshot succeeded!");
} else {
System.out.println("Snapshot failed");
}
} catch (Exception e) {
System.out.println("Snapshot failed");
}
return null;
}, null, null);
} catch (Throwable t) {
System.out.println("Snapshot failed");
}
}
}
};
}
** Solution **
Catch the exception in the snapshot method and call the callback with null. This will trigger the if..else block of case 2 in the above sample and is helpful in providing some feedback to the caller.
A better way to deal with this (would require a lot of change) would be to swap the Callback with a StatusListener which can be updated depending on the status of the snapshot.
1. snapshot(SnapshotParameters params,
WritableImage image)
2. snapshot(Callback<SnapshotResult,Void> callback,
SnapshotParameters params,
WritableImage image)
If, for any reason, the snapshot capture fails, the exception is thrown in the first method. This makes it easier to catch and recover, if required.
While in the second snapshot method, the exception doesn't bubble up and there is no way to recover from the failure. The exception caused is caught in [Scene#addSnapshotRunnable|https://github.com/openjdk/jfx/blob/038556390ef30c912139fcb828ee4a41d8c5abc8/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java#L1438] and callback is never called. This leaves the caller with no feedback and no scope for recovery measures.
** Reproducible Sample **
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class SnapshotFailure extends Application {
private WebView webView;
@Override
public void start(Stage stage) {
webView = new WebView();
Scene scene = new Scene(webView);
stage.setScene(scene);
webView.getEngine().getLoadWorker().stateProperty().addListener(stateListener);
webView.getEngine().loadContent("<html><h1>test</h1></html>");
stage.show();
}
private final ChangeListener<Worker.State> stateListener = new ChangeListener<>() {
@Override
public void changed(ObservableValue<? extends Worker.State> ov, Worker.State oldState, Worker.State newState) {
// Sized intentionally too large; triggers NPE
webView.setPrefSize(100000, 100000);
webView.setMinSize(100000, 100000);
webView.autosize();
if (newState == Worker.State.SUCCEEDED) {
// ATTEMPT #1: Catchable, but NOT pulse-safe
System.out.println("======= SNAPSHOT #1 CATCHABLE =======");
try {
System.out.println("Starting snapshot...");
webView.snapshot(null, null);
System.out.println("Snapshot succeeded!");
} catch (Throwable t) {
System.out.println("Snapshot failed");
}
// ATTEMPT #2: Pulse-safe but NOT catchable
System.out.println("======= SNAPSHOT #2 UN-CATCHABLE =======");
try {
System.out.println("Starting snapshot...");
webView.snapshot(snapshotResult -> {
try {
if (snapshotResult != null) {
System.out.println("Snapshot succeeded!");
} else {
System.out.println("Snapshot failed");
}
} catch (Exception e) {
System.out.println("Snapshot failed");
}
return null;
}, null, null);
} catch (Throwable t) {
System.out.println("Snapshot failed");
}
}
}
};
}
** Solution **
Catch the exception in the snapshot method and call the callback with null. This will trigger the if..else block of case 2 in the above sample and is helpful in providing some feedback to the caller.
A better way to deal with this (would require a lot of change) would be to swap the Callback with a StatusListener which can be updated depending on the status of the snapshot.