JavaFX - How to create SnapShot/Screenshot of (invisble) WebView
Asked Answered
T

3

12

I want to create a SnapShot/Screenshot/Image from a WebView in JavaFX(8).
This WebView does not need to be visible (in my case).

My question:
Is it possible (in any way), to create a Screenshot/Image from a WebView,
when the WebView is not visible (or not added to any visible container)?

See my example code, when WebView (or it's parent ScrollPane) is visible=false,
the Screenshot won't work (respectively is emtpy/blank).

Example code:

package test;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.SnapshotResult;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.util.Duration;

public class JavaFXApplication extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      ImageView webviewPreviewImage = new ImageView();
      Label waitLabel = new Label("Please wait...");
      WebView webView = new WebView();
      webView.setMaxHeight(480d);
      webView.setMinHeight(480d);
      webView.setMaxWidth(640d);
      webView.setMinWidth(640d);
      webView.setZoom(0.4);

      ScrollPane scrollpane = new ScrollPane(webView);
      scrollpane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
      scrollpane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
      scrollpane.setMaxWidth(0); //WORKAROUND: hide the WebView/ScrollPane
      scrollpane.setMaxHeight(0); //WORKAROUND: hide the WebView/ScrollPane
      scrollpane.setMinWidth(0); //WORKAROUND: hide the WebView/ScrollPane
      scrollpane.setMinHeight(0); //WORKAROUND: hide the WebView/ScrollPane

      //scrollpane.setVisible(false); //when WebView is invisible,  SnapShot doesn't work!

       webView.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() 
       {
          @Override
          public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) 
          {
              if (newState == Worker.State.SUCCEEDED) 
              {
                 //When SUCCEEDED is called, the WebPage may not has fully finished rendering!
                 //so, wait for few seceonds before making the screenshot...
                 Timeline timeline = new Timeline(new KeyFrame(
                         Duration.millis(1500),
                         ae -> takeSnapshot()));
                 timeline.play();
              }
           }

           private KeyFrame takeSnapshot()
           {
              webView.snapshot((SnapshotResult param) ->
              {
                 webviewPreviewImage.setImage(param.getImage());
                 webviewPreviewImage.setFitHeight(240d);
                 webviewPreviewImage.setFitWidth(320d);
                 webviewPreviewImage.setPreserveRatio(true);
                 waitLabel.setVisible(false);
                 return null;
              }, null, null);
              return null;
          }
      });
      webView.getEngine().load("http://www.bing.com");

      VBox root = new VBox();
      root.setAlignment(Pos.CENTER);
      root.setSpacing(10d);
      root.getChildren().add(waitLabel);
      root.getChildren().add(scrollpane);
      root.getChildren().add(webviewPreviewImage);

      Scene scene = new Scene(root, 800, 600);
      primaryStage.setScene(scene);
      primaryStage.show();
  }

  public static void main(String[] args)
  {
     launch(args);
  }
}
Tumescent answered 8/5, 2015 at 10:8 Comment(3)
Do you want to take snapshot without showing application or scene??Unaunabated
My Application is showing (and has, as result of it, a Scene). But this WebView does not need to be part of it.Tumescent
This is some requested feature that is unresolved, as for this issue on Jira. I've tested 8u60b13 (new webkit implementation), with no sucess. I think you have a nice workaround there.Handstand
O
2

The class WebView contains the following function:

private boolean isTreeReallyVisible() {
    if (getScene() == null) {
        return false;
    }

    final Window window = getScene().getWindow();

    if (window == null) {
        return false;
    }

    boolean iconified = (window instanceof Stage) ? ((Stage)window).isIconified() : false;

    return impl_isTreeVisible()
           && window.isShowing()
           && window.getWidth() > 0
           && window.getHeight() > 0
           && !iconified;
}

As long as the function returns false, the rendering is blocked. So it might be quite tricky to get it to render. Normally, for snapshot you don't have to put the Node in the scene at all.

Oleic answered 24/10, 2018 at 20:34 Comment(1)
Although this is not a solution, it's very nice to see a solution which explains why the solution may not be (easily) possible. Thanks!Kei
C
1

After a lot of searching and scraping several pieces together I found an example a SO-poster posted in an oracle forum that only had the problem that the size of the webview was fixed and that my css used in the html (not in JavaFX) needed:

overflow-x: hidden;
overflow-y: hidden;

to hide the last scrollbar.

So I come up with the following snapshot method (application with animation just as example of your application):

package application;

import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import javafx.animation.Animation;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.web.WebView;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;

public class WebViewSnapshot extends Application {

    BorderPane rootPane;
    TranslateTransition animation;

    @Override
    public void start(Stage primaryStage) {

        Rectangle rect = new Rectangle(50, 50, 50, 50);
        rect.setFill(Color.CORAL);

        animation = createAnimation(rect);

        Button snapshotButton = new Button("Take snapshot");

        Pane pane = new Pane(rect);
        pane.setMinSize(600, 150);

        rootPane = new BorderPane(pane, null, null, snapshotButton, new Label("This is the main scene"));

        snapshotButton.setOnAction(e -> {
            // html file being shown in webview
            File htmlFile = new File ("generated/template.html");
            // the resulting snapshot png file
            File aboutFile = new File ("generated/about.png");
            generate(htmlFile, aboutFile, 1280, 720);
        });

        BorderPane.setAlignment(snapshotButton, Pos.CENTER);
        BorderPane.setMargin(snapshotButton, new Insets(5));
        Scene scene = new Scene(rootPane);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private TranslateTransition createAnimation(Rectangle rect) {
        TranslateTransition animation = new TranslateTransition(Duration.seconds(1), rect);
        animation.setByX(400);
        animation.setCycleCount(Animation.INDEFINITE);
        animation.setAutoReverse(true);
        animation.play();
        return animation;
    }

    public void generate(File htmlFile, File outputFile, double width, double height) {
        animation.pause();
        // rootPane is the root of original scene in an FXML controller you get this when you assign it an id
        rootPane.setEffect(new GaussianBlur());
        Stage primaryStage = (Stage)rootPane.getScene().getWindow();
        // creating separate webview holding same html content as in original scene
        WebView webView = new WebView();
        // with the size I want the snapshot
        webView.setPrefSize(width, height);
        AnchorPane snapshotRoot = new AnchorPane(webView);
        webView.getEngine().load(htmlFile.toURI().toString());
        Stage popupStage = new Stage(StageStyle.TRANSPARENT);
        popupStage.initOwner(primaryStage);
        popupStage.initModality(Modality.APPLICATION_MODAL);
        // this popup doesn't really show anything size = 1x1, it just holds the snapshot-webview
        popupStage.setScene(new Scene(snapshotRoot, 1, 1));
        // pausing to make sure the webview/picture is completely rendered
        PauseTransition pt = new PauseTransition(Duration.seconds(2));
        pt.setOnFinished(new EventHandler<ActionEvent>() {
            @Override public void handle(ActionEvent event) {
                WritableImage image = webView.snapshot(null, null);
                // writing a png to outputFile
                // writing a JPG like this will result in a pink JPG, see other posts
                // if somebody can scrape me simple code to convert it ARGB to RGB or something
                String format = "png";
                try {
                    ImageIO.write(SwingFXUtils.fromFXImage(image, null), format, outputFile);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } finally {
                    rootPane.setEffect(null);
                    popupStage.hide();
                    animation.play();
                }
            }
        });
        // pausing, after pause onFinished event will take + write snapshot
        pt.play();
        // GO!
        popupStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
Cog answered 22/9, 2019 at 8:51 Comment(0)
D
0

Why the invisible WebView doesn't render at all is answered here: https://mcmap.net/q/951084/-javafx-how-to-create-snapshot-screenshot-of-invisble-webview, code circumventing that problem is here https://mcmap.net/q/951084/-javafx-how-to-create-snapshot-screenshot-of-invisble-webview.

But a clean solution to solve the problem that a WebView might not be ready for taking a snapshot when it says "I'm ready" is provided here: https://mcmap.net/q/1012439/-when-is-a-webview-ready-for-a-snapshot.

This is as a summarization, because there are actually 2 problems preventing easy snapshotting of a WebView. I had to merge both solutions to get a working clean solution.

Doublejointed answered 9/11, 2021 at 19:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.