Skip to content

Instantly share code, notes, and snippets.

@jewelsea
Created May 23, 2013 05:38
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jewelsea/5632958 to your computer and use it in GitHub Desktop.
Save jewelsea/5632958 to your computer and use it in GitHub Desktop.
Captures web pages to image files using JavaFX WebView and ImageIO
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.beans.value.*;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.*;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.util.converter.NumberStringConverter;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class WebViewCaptureDemo extends Application {
private static final String HOME_LOC = "http://docs.oracle.com/javafx/2/get_started/animation.htm";
private WebView webView;
private File captureFile = new File("cap.png");
public static void main(String[] args) { Application.launch(WebViewCaptureDemo.class); }
@Override public void start(Stage stage) throws Exception {
webView = new WebView();
webView.setPrefSize(1000, 8000);
final TextField location = new TextField();
location.setOnAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent event) {
if (!location.getText().startsWith("http")) {
location.setText("http://" + location.getText());
}
webView.getEngine().load(location.getText());
}
});
webView.getEngine().locationProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observable, String oldLocation, String newLocation) {
location.setText(newLocation);
}
});
ScrollPane webViewScroll = new ScrollPane();
webViewScroll.setContent(webView);
webViewScroll.setPrefSize(800, 300);
final Button capture = new Button("Capture");
final ProgressIndicator progress = new ProgressIndicator();
progress.setVisible(false);
final TextField prefWidth = new TextField("1000");
final TextField prefHeight = new TextField("8000");
HBox controls = new HBox(10);
controls.getChildren().addAll(capture, progress, prefWidth, prefHeight);
final ImageView imageView = new ImageView();
ScrollPane imageViewScroll = makeScrollable(imageView);
imageViewScroll.setPrefSize(800, 300);
final PauseTransition pt = new PauseTransition();
pt.setDuration(Duration.millis(500));
pt.setOnFinished(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
WritableImage image = webView.snapshot(null, null);
BufferedImage bufferedImage = SwingFXUtils.fromFXImage(image, null);
try {
ImageIO.write(bufferedImage, "png", captureFile);
imageView.setImage(new Image(captureFile.toURI().toURL().toExternalForm()));
System.out.println("Captured WebView to: " + captureFile.getAbsoluteFile());
progress.setVisible(false);
capture.setDisable(false);
} catch (IOException e) {
e.printStackTrace();
}
}
});
capture.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
NumberStringConverter converter = new NumberStringConverter();
double W = converter.fromString(prefWidth.getText()).doubleValue();
double H = converter.fromString(prefHeight.getText()).doubleValue();
// ensure that the capture size has a reasonable min size and is within the limits of what JavaFX is capable of processing.
if (W < 100) {
W = 100;
prefWidth.setText("100");
}
if (W > 2000) {
W = 2000;
prefWidth.setText("2000");
}
if (H < 100) {
H = 100;
prefHeight.setText("100");
}
if (H > 16000) {
H = 16000;
prefHeight.setText("16000");
}
webView.setPrefWidth(W);
webView.setPrefHeight(H);
pt.play();
capture.setDisable(true);
progress.setVisible(true);
}
});
webView.getEngine().load(HOME_LOC);
VBox layout = new VBox(10);
layout.setStyle("-fx-padding: 10; -fx-background-color: cornsilk;");
layout.getChildren().setAll(
location,
webViewScroll,
controls,
imageViewScroll,
new Label("Capture File: " + captureFile.getAbsolutePath())
);
VBox.setVgrow(imageViewScroll, Priority.ALWAYS);
stage.setScene(new Scene(layout));
stage.show();
}
private ScrollPane makeScrollable(final ImageView imageView) {
final ScrollPane scroll = new ScrollPane();
final StackPane centeredImageView = new StackPane();
centeredImageView.getChildren().add(imageView);
scroll.viewportBoundsProperty().addListener(new ChangeListener<Bounds>() {
@Override public void changed(ObservableValue<? extends Bounds> ov, Bounds oldBounds, Bounds bounds) {
centeredImageView.setPrefSize(
Math.max(imageView.prefWidth(bounds.getHeight()), bounds.getWidth()),
Math.max(imageView.prefHeight(bounds.getWidth()), bounds.getHeight())
);
}
});
scroll.setContent(centeredImageView);
return scroll;
}
}
@jewelsea
Copy link
Author

Primary feature is ability to capture a web page rendering up to 2000 pixels by 16000 pixels (i.e. larger than a screen resolution).

@Sergey80
Copy link

What about "headless" snapshoting? Like PhantomJS does?

@jewelsea
Copy link
Author

JavaFX can render in a headless mode using the Monocle glass windowing component. I do not know if this headless rendering using Monocle is compatible with WebView. You could ask further questions on the openjfx-dev mailing list for more information.

@Diarsid
Copy link

Diarsid commented Jan 27, 2017

It doesn't work. I've tried it just now but got NPE:

java.lang.NullPointerException
	at com.sun.javafx.webkit.prism.WCPageBackBufferImpl.validate(WCPageBackBufferImpl.java:97)
	at com.sun.webkit.WebPage.paint(WebPage.java:656)
	at com.sun.javafx.sg.prism.web.NGWebView.renderContent(NGWebView.java:96)
	at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2053)
	at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1945)
	at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:235)
	at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:576)
	at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2053)
	at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1945)
	at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:235)
	at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:576)
	at com.sun.javafx.sg.prism.NGNode.renderForClip(NGNode.java:2294)
	at com.sun.javafx.sg.prism.NGNode.renderRectClip(NGNode.java:2188)
	at com.sun.javafx.sg.prism.NGNode.renderClip(NGNode.java:2214)
	at com.sun.javafx.sg.prism.CacheFilter.impl_renderNodeToCache(CacheFilter.java:671)
	at com.sun.javafx.sg.prism.CacheFilter.render(CacheFilter.java:575)
	at com.sun.javafx.sg.prism.NGNode.renderCached(NGNode.java:2358)
	at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2044)
	at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1945)
	at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:235)
	at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:576)
	at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2053)
	at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1945)
	at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:235)
	at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:576)
	at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2053)
	at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1945)
	at com.sun.javafx.tk.quantum.ViewPainter.doPaint(ViewPainter.java:477)
	at com.sun.javafx.tk.quantum.ViewPainter.paintImpl(ViewPainter.java:323)
	at com.sun.javafx.tk.quantum.PresentingPainter.run(PresentingPainter.java:91)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
	at com.sun.javafx.tk.RenderJob.run(RenderJob.java:58)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:125)
	at java.lang.Thread.run(Thread.java:745)

@maxbrito500
Copy link

This worked good for me, just copy the whole file into your project and try it out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment