代码之家  ›  专栏  ›  技术社区  ›  tresf

WebView何时可以创建快照()?

  •  1
  • tresf  · 技术社区  · 5 年前

    WebView is ready when Worker.State.SUCCEEDED is reached Animation , Transition PauseTransition 等),则呈现空白页。

    这表明在WebView中发生了一个事件,它正在为捕获做准备,但是它是什么呢?

    over 7,000 code snippets on GitHub which use SwingFXUtils.fromFXImage 但他们中的大多数似乎要么与 网络视图 ,是交互式的(人类屏蔽了比赛条件)或使用任意转换(从100ms到2000ms)。

    我试过:

    • 倾听 changed(...) 网络视图 的尺寸(高度和宽度属性 DoubleProperty 工具 ObservableValue ,它可以监视这些东西)

      • 不可行。有时,该值似乎与绘制例程分开更改,从而导致部分内容。
    • runLater(...) 在FX应用程序线程上。

      • 许多技术都使用这种方法,但我自己的单元测试(以及其他开发人员的一些优秀反馈)解释说,事件通常已经在正确的线程上,而且这种调用是多余的。我能想到的最好的办法就是通过排队增加足够的延迟,这对一些人来说是有效的。
    • 网络视图

      • SUCCEEDED 尽管空白捕获被调用。DOM/JavaScript侦听器似乎没有帮助。
    • 过渡

      • 这种方法是有效的,如果延迟足够长,可以产生高达100%的单元测试,但是转换时间似乎是有限的 some future moment that we're just guessing 糟糕的设计。对于性能或任务关键型应用程序,这迫使程序员在速度和可靠性之间进行权衡,这两者对用户来说都是潜在的不良体验。

    什么时候打电话好 WebView.snapshot(...)

    用法:

    SnapshotRaceCondition.initialize();
    BufferedImage bufferedImage = SnapshotRaceCondition.capture("<html style='background-color: red;'><h1>TEST</h1></html>");
    /**
     * Notes:
     * - The color is to observe the otherwise non-obvious cropping that occurs
     *   with some techniques, such as `setPrefWidth`, `autosize`, etc.
     * - Call this function in a loop and then display/write `BufferedImage` to
     *   to see strange behavior on subsequent calls.
     * - Recommended, modify `<h1>TEST</h1` with a counter to see content from
     *   previous captures render much later.
     */
    

    代码段:

    import javafx.application.Application;
    import javafx.application.Platform;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.concurrent.Worker;
    import javafx.embed.swing.SwingFXUtils;
    import javafx.scene.Scene;
    import javafx.scene.SnapshotParameters;
    import javafx.scene.image.WritableImage;
    import javafx.scene.web.WebView;
    import javafx.stage.Stage;
    
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.util.concurrent.atomic.AtomicBoolean;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.logging.Logger;
    
    public class SnapshotRaceCondition extends Application  {
        private static final Logger log = Logger.getLogger(SnapshotRaceCondition.class.getName());
    
        // self reference
        private static SnapshotRaceCondition instance = null;
    
        // concurrent-safe containers for flags/exceptions/image data
        private static AtomicBoolean started  = new AtomicBoolean(false);
        private static AtomicBoolean finished  = new AtomicBoolean(true);
        private static AtomicReference<Throwable> thrown = new AtomicReference<>(null);
        private static AtomicReference<BufferedImage> capture = new AtomicReference<>(null);
    
        // main javafx objects
        private static WebView webView = null;
        private static Stage stage = null;
    
        // frequency for checking fx is started
        private static final int STARTUP_TIMEOUT= 10; // seconds
        private static final int STARTUP_SLEEP_INTERVAL = 250; // millis
    
        // frequency for checking capture has occured 
        private static final int CAPTURE_SLEEP_INTERVAL = 10; // millis
    
        /** Called by JavaFX thread */
        public SnapshotRaceCondition() {
            instance = this;
        }
    
        /** Starts JavaFX thread if not already running */
        public static synchronized void initialize() throws IOException {
            if (instance == null) {
                new Thread(() -> Application.launch(SnapshotRaceCondition.class)).start();
            }
    
            for(int i = 0; i < (STARTUP_TIMEOUT * 1000); i += STARTUP_SLEEP_INTERVAL) {
                if (started.get()) { break; }
    
                log.fine("Waiting for JavaFX...");
                try { Thread.sleep(STARTUP_SLEEP_INTERVAL); } catch(Exception ignore) {}
            }
    
            if (!started.get()) {
                throw new IOException("JavaFX did not start");
            }
        }
    
    
        @Override
        public void start(Stage primaryStage) {
            started.set(true);
            log.fine("Started JavaFX, creating WebView...");
            stage = primaryStage;
            primaryStage.setScene(new Scene(webView = new WebView()));
    
            // Add listener for SUCCEEDED
            Worker<Void> worker = webView.getEngine().getLoadWorker();
            worker.stateProperty().addListener(stateListener);
    
            // Prevents JavaFX from shutting down when hiding window, useful for calling capture(...) in succession
            Platform.setImplicitExit(false);
        }
    
        /** Listens for a SUCCEEDED state to activate image capture **/
        private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
            if (newState == Worker.State.SUCCEEDED) {
                WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);
    
                capture.set(SwingFXUtils.fromFXImage(snapshot, null));
                finished.set(true);
                stage.hide();
            }
        };
    
        /** Listen for failures **/
        private static ChangeListener<Throwable> exceptListener = new ChangeListener<Throwable>() {
            @Override
            public void changed(ObservableValue<? extends Throwable> obs, Throwable oldExc, Throwable newExc) {
                if (newExc != null) { thrown.set(newExc); }
            }
        };
    
        /** Loads the specified HTML, triggering stateListener above **/
        public static synchronized BufferedImage capture(final String html) throws Throwable {
            capture.set(null);
            thrown.set(null);
            finished.set(false);
    
            // run these actions on the JavaFX thread
            Platform.runLater(new Thread(() -> {
                try {
                    webView.getEngine().loadContent(html, "text/html");
                    stage.show(); // JDK-8087569: will not capture without showing stage
                    stage.toBack();
                }
                catch(Throwable t) {
                    thrown.set(t);
                }
            }));
    
            // wait for capture to complete by monitoring our own finished flag
            while(!finished.get() && thrown.get() == null) {
                log.fine("Waiting on capture...");
                try {
                    Thread.sleep(CAPTURE_SLEEP_INTERVAL);
                }
                catch(InterruptedException e) {
                    log.warning(e.getLocalizedMessage());
                }
            }
    
            if (thrown.get() != null) {
                throw thrown.get();
            }
    
            return capture.get();
        }
    }
    

    相关:

    0 回复  |  直到 5 年前
        1
  •  1
  •   VGR    5 年前

    loadContent load reload() 会补偿的。

    show() 在加载内容之前。由于内容是异步加载的,因此完全有可能在调用之后的语句之前加载 负载 饰面。

    然后,解决方法是将内容放在一个文件中,并调用WebEngines reload()

    Path htmlFile = Files.createTempFile("snapshot-", ".html");
    Files.writeString(htmlFile, html);
    
    WebEngine engine = myWebView.getEngine();
    engine.getLoadWorker().stateProperty().addListener(
        new ChangeListener<Worker.State>() {
            private boolean reloaded;
    
            @Override
            public void changed(ObservableValue<? extends Worker.State> obs,
                                Worker.State oldState,
                                Worker.State newState) {
                if (reloaded) {
                    Image image = myWebView.snapshot(null, null);
                    doStuffWithImage(image);
    
                    try {
                        Files.delete(htmlFile);
                    } catch (IOException e) {
                        log.log(Level.WARN, "Couldn't delete " + htmlFile, e);
                    }
                } else {
                    reloaded = true;
                    engine.reload();
                }
            }
        });
    
    
    engine.load(htmlFile.toUri().toString());
    

    但因为你在使用 static 对于所有内容,都必须添加一些字段:

    private static boolean reloaded;
    private static volatile Path htmlFile;
    

    /** Listens for a SUCCEEDED state to activate image capture **/
    private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
        if (newState == Worker.State.SUCCEEDED) {
            if (reloaded) {
                WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);
    
                capture.set(SwingFXUtils.fromFXImage(snapshot, null));
                finished.set(true);
                stage.hide();
    
                try {
                    Files.delete(htmlFile);
                } catch (IOException e) {
                    log.log(Level.WARN, "Couldn't delete " + htmlFile, e);
                }
            } else {
                reloaded = true;
                webView.getEngine().reload();
            }
        }
    };
    

    然后每次加载内容时都必须重置它:

    Path htmlFile = Files.createTempFile("snapshot-", ".html");
    Files.writeString(htmlFile, html);
    
    Platform.runLater(new Thread(() -> {
        try {
            reloaded = false;
            stage.show(); // JDK-8087569: will not capture without showing stage
            stage.toBack();
            webView.getEngine().load(htmlFile);
        }
        catch(Throwable t) {
            thrown.set(t);
        }
    }));
    

    注意,有更好的方法来执行多线程处理。您可以简单地使用 volatile 领域:

    private static volatile boolean started;
    private static volatile boolean finished = true;
    private static volatile Throwable thrown;
    private static volatile BufferedImage capture;
    

    (布尔字段默认为false,对象字段默认为null。与C程序不同,这是Java的硬保证;没有未初始化的内存。)

    与其在循环中轮询另一个线程中所做的更改,不如使用同步、锁或更高级别的类,如 CountDownLatch 它在内部使用这些东西:

    private static final CountDownLatch initialized = new CountDownLatch(1);
    private static volatile CountDownLatch finished;
    private static volatile BufferedImage capture;
    private static volatile Throwable thrown;
    private static boolean reloaded;
    
    private static volatile Path htmlFile;
    
    // main javafx objects
    private static WebView webView = null;
    private static Stage stage = null;
    
    private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
        if (newState == Worker.State.SUCCEEDED) {
            if (reloaded) {
                WritableImage snapshot = webView.snapshot(null, null);
                capture = SwingFXUtils.fromFXImage(snapshot, null);
                finished.countDown();
                stage.hide();
    
                try {
                    Files.delete(htmlFile);
                } catch (IOException e) {
                    log.log(Level.WARNING, "Could not delete " + htmlFile, e);
                }
            } else {
                reloaded = true;
                webView.getEngine().reload();
            }
        }
    };
    
    @Override
    public void start(Stage primaryStage) {
        log.fine("Started JavaFX, creating WebView...");
        stage = primaryStage;
        primaryStage.setScene(new Scene(webView = new WebView()));
    
        Worker<Void> worker = webView.getEngine().getLoadWorker();
        worker.stateProperty().addListener(stateListener);
    
        webView.getEngine().setOnError(e -> {
            thrown = e.getException();
        });
    
        // Prevents JavaFX from shutting down when hiding window, useful for calling capture(...) in succession
        Platform.setImplicitExit(false);
    
        initialized.countDown();
    }
    
    public static BufferedImage capture(String html)
    throws InterruptedException,
           IOException {
    
        htmlFile = Files.createTempFile("snapshot-", ".html");
        Files.writeString(htmlFile, html);
    
        if (initialized.getCount() > 0) {
            new Thread(() -> Application.launch(SnapshotRaceCondition2.class)).start();
            initialized.await();
        }
    
        finished = new CountDownLatch(1);
        thrown = null;
    
        Platform.runLater(() -> {
            reloaded = false;
            stage.show(); // JDK-8087569: will not capture without showing stage
            stage.toBack();
            webView.getEngine().load(htmlFile.toUri().toString());
        });
    
        finished.await();
    
        if (thrown != null) {
            throw new IOException(thrown);
        }
    
        return capture;
    }
    

    reloaded 未声明为volatile,因为它仅在JavaFX应用程序线程中访问。

        2
  •  0
  •   tresf    4 年前

    为了适应调整大小以及底层快照行为,我(我们)提出了以下工作解决方案。注意,这些测试运行了2000x(Windows、macOS和Linux),提供了100%成功的随机WebView大小。

    首先,我将引用一个JavaFX开发人员。这是引自一份私人(赞助的)错误报告:

    “我假设您在FX AppThread上启动了调整大小操作,并且在达到SUCCEEDED状态后完成。在这种情况下,在我看来,在那一刻,等待2个脉冲(不阻塞FX AppThread)应该给webkit实现足够的时间进行更改,除非这导致JavaFX中的某些维度被更改,这可能会再次导致webkit中的维度被更改。

    我正在考虑如何将这些信息输入JBS中的讨论,但我很肯定会有这样的答案:“只有当webcomponent稳定时,才应该拍摄快照”。所以为了预测这个答案,最好看看这个方法是否适合你。或者,如果它最终导致了其他问题,那么最好考虑一下这些问题,看看是否/如何在OpenJFX中修复它们。”

    1. 默认情况下,javafx8使用默认值 600 如果高度正好 0 . WebView setMinHeight(1) , setPrefHeight(1) 为了避免这个问题。这不在下面的代码中,但值得一提的是,任何人都可以将其应用到他们的项目中。
    2. 要防止快照空白错误,请利用快照回调,它还侦听脉冲。
    // without this runlater, the first capture is missed and all following captures are offset
    Platform.runLater(new Runnable() {
        public void run() {
            // start a new animation timer which waits for exactly two pulses
            new AnimationTimer() {
                int frames = 0;
    
                @Override
                public void handle(long l) {
                    // capture at exactly two frames
                    if (++frames == 2) {
                        System.out.println("Attempting image capture");
                        webView.snapshot(new Callback<SnapshotResult,Void>() {
                            @Override
                            public Void call(SnapshotResult snapshotResult) {
                                capture.set(SwingFXUtils.fromFXImage(snapshotResult.getImage(), null));
                                unlatch();
                                return null;
                            }
                        }, null, null);
    
                        //stop timer after snapshot
                        stop();
                    }
                }
            }.start();
        }
    });