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.