WebViewがsnapshot()の準備ができるのはいつですか?


9

JavaFXのドキュメント状態WebView時に準備ができているWorker.State.SUCCEEDEDに達しているしばらくの間(すなわち待たない限り、しかしAnimationTransitionPauseTransition、など)を、空白のページが表示されます。

これは、キャプチャの準備ができているWebView内で発生するイベントがあることを示唆していますが、それは何ですか?

あります7,000以上のコードスニペットは、GitHubの上でその使用SwingFXUtils.fromFXImageが、それらのほとんどがどちらかに無関係と思われるWebView、インタラクティブ(ヒトマスクは競合状態)であるか、(100ミリ秒から2,000msまでの任意の場所)任意のトランジションを使用しています。

私はもう試した:

  • の寸法changed(...)内からリッスンしますWebView(高さと幅のプロパティDoublePropertyObservableValue、これらのものを監視できます)

    • 🚫実行できません。場合によっては、値がペイントルーチンとは別に変化して、部分的なコンテンツになることがあります。
  • runLater(...)FXアプリケーションスレッドで何でもすべてを盲目的に伝える。

    • techniques多くの手法でこれを使用していますが、私自身の単体テスト(および他の開発者からの素晴らしいフィードバック)は、イベントがすでに正しいスレッドにあることが多く、この呼び出しは冗長であることを説明しています。私が考えることができる最高のものは、それがいくつかのために働くキューイングを通しての遅延のちょうど十分な追加です。
  • DOMリスナー/トリガーまたはJavaScriptリスナー/トリガーを WebView

    • SUCCEEDED空白のキャプチャにもかかわらず、が呼び出されると、JavaScriptとDOMの両方が正しく読み込まれているようです。DOM / JavaScriptリスナーは役に立たないようです。
  • AnimationまたはTransitionを使用して、メインFXスレッドをブロックせずに効果的に「スリープ」します。

    • ⚠️このアプローチは機能し、遅延が十分に長い場合、ユニットテストの最大100%を生み出すことができますが、遷移時間は、私たちが推測していて設計が悪い将来の瞬間のようです。パフォーマンスの高いアプリケーションやミッションクリティカルなアプリケーションの場合、これはプログラマーにスピードと信頼性のどちらかをトレードオフすることを強制します。

いつ電話するのがいい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();
    }
}

関連:


Platform.runLaterは冗長ではありません。WebViewがレンダリングを完了するために必要な保留中のイベントがある可能性があります。Platform.runLaterは、私が最初に試すことです。
VGR

レースと単体テストは、イベントが保留中ではなく、別のスレッドで発生していることを示唆しています。 Platform.runLaterテストされ、それを修正していません。同意できない場合は、自分で試してみてください。私は間違っていて嬉しいです、それは問題を閉じます。
tresf

さらに、公式ドキュメントでは、SUCCEEDED状態(リスナーがFXスレッドで発動する状態)が適切な手法としてステージングされます。キューに入れられたイベントを表示する方法がある場合、私は試すことにうれしいでしょう。OracleフォーラムのコメントやWebView、設計上独自のスレッドで実行する必要があるいくつかのSOの質問を通じて、まばらな提案を見つけました。その仮定が間違っているなら、素晴らしい。任意の待機時間なしで問題を修正するための合理的な提案があれば、私はそれを受け入れます。
tresf

私は独自の非常に短いテストを作成し、ロードワーカーの状態リスナーでWebViewのスナップショットを正常に取得できました。しかし、あなたのプログラムは私に空白のページを与えます。私はまだ違いを理解しようとしています。
VGR

これは、loadContentメソッドを使用する場合、またはファイルURLをロードする場合にのみ発生するようです。
VGR

回答:


1

これはWebEngineのloadContentメソッドを使用するときに発生するバグのようです。を使用loadしてローカルファイルをロードする場合にも発生しますが、その場合はreload()を呼び出すことで補正されます。

また、スナップショットを撮るときにステージを表示する必要があるためshow()、コンテンツをロードする前にを呼び出す必要があります。コンテンツは非同期で読み込まれるため、呼び出しの後のステートメントloadまたはloadContent終了前のステートメントの前に読み込まれる可能性があります。

次に、回避策は、コンテンツをファイルに配置し、WebEngineのreload()メソッドを1回だけ呼び出すことです。コンテンツが2回目に読み込まれるときに、スナップショットをロードワーカーの状態プロパティのリスナーから正常に取得できます。

通常、これは簡単です。

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 JavaFXアプリケーションスレッドでのみアクセスされるため、揮発性と宣言されていません。


1
これは非常に優れた記事であり、特にスレッド化とvolatile変数に関するコードの改善点です。残念ながら、次の呼び出しWebEngine.reload()と待機SUCCEEDEDは機能しません。HTMLコンテンツにカウンターを配置する0, 0, 1, 3, 3, 50, 1, 2, 3, 4, 5、の代わりにが返されます。これは、根本的な競合状態を実際には修正しないことを示唆しています。
tresf

引用:「使用する方が良い[...] CountDownLatch」。この情報は簡単に見つけることができず、最初のFX起動でコードの速度とシンプルさが向上するため、賛成票を投じます。
tresf

0

基本的なスナップショットの動作と同様にサイズ変更に対応するために、私(私たちは)次の実用的なソリューションを考え出しました。これらのテストは2,000x(Windows、macOS、およびLinux)で実行され、ランダムなWebViewサイズで100%成功したことに注意してください。

まず、JavaFX開発者の1人を引用します。これはプライベート(後援)のバグレポートから引用されています。

「私はあなたがFX AppThreadでサイズ変更を開始し、SUCCEEDED状態に達した後に行われると想定しています。その場合、その瞬間に2パルス(FX AppThreadをブロックせずに)待機すると、変更を加えるのに十分な時間をwebkitに実装します。ただし、JavaFXで一部の次元が変更され、Webkit内で再び次元が変更される場合があります。

この情報をJBSのディスカッションにフィードする方法を考えていますが、「Webコンポーネントが安定している場合にのみスナップショットを取得する必要がある」という答えがあると確信しています。したがって、この答えを予測するには、このアプローチが効果的かどうかを確認することをお勧めします。または、他の問題が発生することが判明した場合は、これらの問題について検討し、OpenJFX自体でそれらを修正できるかどうか/どのように修正できるかを確認することをお勧めします。」

  1. デフォルトでは、JavaFX 8は600高さが正確にifのデフォルトを使用します0。この問題を回避WebViewするにはsetMinHeight(1)、コードの再利用でを使用する必要がありますsetPrefHeight(1)。これは以下のコードには含まれていませんが、プロジェクトに適合させる人には言及する価値があります。
  2. WebKitの準備に対応するために、アニメーションタイマーの内部から2つのパルスを正確に待ちます。
  3. スナップショットの空白のバグを防ぐには、スナップショットコールバックを利用します。これもパルスをリッスンします。
// 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();
    }
});
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.