WebView는 언제 snapshot ()을 준비합니까?


9

A가 있다는 자바 FX 문서 상태 WebView때 준비가 Worker.State.SUCCEEDED도달 한 동안 (즉, 대기하지 않는 한, 그러나 Animation, Transition, PauseTransition, 등), 빈 페이지가 렌더링됩니다.

이것은 WebView 내부에서 캡처 준비를하는 이벤트가 있음을 시사하지만, 무엇입니까?

GitHubSwingFXUtils.fromFXImage 에는 7,000 개가 넘는 코드 스 니펫이 있지만 대부분은 관련이 없으며 WebView대화 형 (인간 마스크 경쟁 조건) 또는 임의의 전환 (100ms ~ 2,000ms)을 사용합니다.

난 노력 했어:

  • 차원 changed(...)내에서 청취 WebView(높이 및 너비 속성 DoubleProperty구현 ObservableValue,이를 모니터링 할 수 있음)

    • 🚫 생존하지 않습니다. 때로는 값이 페인트 루틴과 별도로 변경되어 부분 내용물이 생성되는 것처럼 보입니다.
  • runLater(...)FX Application Thread에서 맹목적으로 모든 것을 말하십시오 .

    • 🚫 많은 기술들이 이것을 사용하지만, 내 자신의 단위 테스트 (및 다른 개발자들의 훌륭한 피드백)는 이벤트가 이미 올바른 스레드에 있고이 호출이 중복된다고 설명합니다. 내가 생각할 수있는 최선은 큐잉을 통해 지연 될 수 있다는 것입니다.
  • 에 DOM 리스너 / 트리거 또는 JavaScript 리스너 / 트리거 추가 WebView

    • 🚫 SUCCEEDED빈 캡처에도 불구하고 JavaScript를 호출하면 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 스레드에서 발생 하는 상태가 적절한 기술입니다. 대기중인 이벤트를 표시하는 방법이 있다면 시도해 볼 수 있습니다. 오라클 포럼에 대한 의견과 WebView디자인에 의해 자체 스레드에서 실행되어야하는 SO 질문에 대한 희소 한 제안을 찾았 으므로 테스트 후 며칠 동안 에너지에 집중하고 있습니다. 그 가정이 틀렸다면 위대합니다. 나는 임의의 대기 시간없이 문제를 해결하는 합리적인 제안을 할 수 있습니다.
— tresf

나는 매우 간단한 테스트를 작성했으며로드 워커의 상태 리스너에서 WebView의 스냅 샷을 성공적으로 얻을 수있었습니다. 그러나 귀하의 프로그램은 빈 페이지를 제공합니다. 나는 여전히 차이점을 이해하려고 노력하고 있습니다.
— VGR

loadContent메소드를 사용하거나 파일 URL을로드 할 때만 발생 합니다.
— VGR

답변:


1

이것은 WebEngine의 loadContent메소드를 사용할 때 발생하는 버그 인 것 같습니다 . load로컬 파일을로드하는 데 사용할 때도 발생 하지만이 경우 reload () 를 호출 하면이를 보상합니다.

또한 스냅 샷을 만들 때 스테이지가 표시되어야 show()하므로 내용을로드하기 전에 호출 해야합니다. 내용은 비동기식으로로드되므로 호출 load또는 loadContent종료 후 명령문이로드되기 전에 내용이로드 될 수 있습니다 .

따라서 해결 방법은 컨텐츠를 파일에 배치하고 WebEngine의 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 JavaFX 응용 프로그램 스레드에서만 액세스되므로 휘발성으로 선언되지 않습니다.


1
이것은 스레딩과 volatile변수를 둘러싼 코드 개선, 특히 훌륭한 쓰기 입니다. 불행히도, 전화 WebEngine.reload()하고 후속 전화 를 기다리는 SUCCEEDED것은 효과가 없습니다. HTML 콘텐츠에 카운터를 배치하면 0, 0, 1, 3, 3, 5대신 0, 1, 2, 3, 4, 5기본 경쟁 조건을 수정하지 않는다는 제안 대신 : 가 나타납니다.
— tresf

인용 : "더 나은 사용 [...] CountDownLatch". 이 정보는 찾기가 쉽지 않았으며 초기 FX 시작으로 코드의 속도와 단순성을 돕습니다.
— tresf

0

기본 스냅 샷 동작뿐만 아니라 크기 조정을 수용하기 위해 다음 작업 솔루션을 제안했습니다. 이 테스트는 2,000x (Windows, macOS 및 Linux)에서 100 % 성공한 임의의 WebView 크기를 제공하여 실행되었습니다.

먼저 JavaFX 개발자 중 하나를 인용하겠습니다. 이는 비공개 (스폰서) 버그 보고서에서 인용 한 것입니다.

"FX AppThread에서 크기 조정을 시작하고 SUCCEEDED 상태에 도달 한 후에 완료된 것으로 가정합니다.이 경우 FX FX 스레드를 차단하지 않고 2 개의 펄스를 기다리는 것은 JavaFX에서 일부 치수가 변경되어 웹킷 내에서 치수가 다시 변경 될 수있는 경우를 제외하고는 웹킷 구현이 변경하기에 충분한 시간입니다.

JBS 토론에이 정보를 제공하는 방법에 대해 생각하고 있지만 "웹 구성 요소가 안정적 일 때만 스냅 샷을 작성해야합니다"라는 대답이있을 것입니다. 따라서이 답변을 예상하려면이 방법이 적합한 지 확인하는 것이 좋습니다. 또는 다른 문제를 일으키는 것으로 판명되면 이러한 문제에 대해 생각하고 OpenJFX 자체에서 문제를 해결할 수 있는지 여부를 확인하는 것이 좋습니다. "

  1. 기본적으로 JavaFX 8은 600높이가 정확히 인 경우 기본값을 사용합니다 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.