スキップしてメイン コンテンツに移動

Java: Capture Web Page

Recently I was investigating how to capture web page as image.
I recently realized that there is a nicer nice component for navigating web page in Java FX.
  • javafx.scene.web.WebView
  • javafx.embed.swing.JFXPanel
I referred this stackoverflow post and wrote a Java class for capturing web page and save it as image.

Please note that you should include jfxrt.jar, which was in JDK1.7.0_XX/jre/lib, should be added to your classpath.

Code

Here is an actual code. Please feel free to use it...
I am very happy if you give me some feedback for this code.
package com.dukesoftware.javafx;

import static com.dukesoftware.utils.io.IOUtils.getExtension;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.concurrent.Worker.State;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;

import javax.imageio.ImageIO;
import javax.swing.JFrame;

import com.dukesoftware.utils.awt.AwtUtils;
import com.dukesoftware.utils.io.IOUtils;
import com.dukesoftware.utils.swing.jframe.JFrameFactory;
import com.google.common.collect.ImmutableMap;

public class WebCapture extends JFXPanel{
    
    private static final String PATH = "path";
    private static final String URL = "url";
    private static final String COMMAND = "command";

    private static final String SHUTDOWN = "shutdown";
    
    public static void main(String[] args) throws InterruptedException {
       WebCapture capture = new WebCapture(1024, 768, 1000);

       // you can see the WebView behavior if you want
       // JFrame frame = JFrameFactory.createExitJFrame(capture, "", 1024, 768);
       // frame.pack();
       // frame.setVisible(true);

       capture
       .load("http://www.google.com", "c:/temp/google.png")
       .load("http://www.yahoo.com", "c:/temp/yahoo.png")
       .load("http://www.bing.com", "c:/temp/bing.png")
       .shutdownAfterAllRequestsProcessed();
    }
    
    private WebView webView;
    
    private final long waitMills;
    private final LinkedBlockingQueue<Map<String, String>> queue = new LinkedBlockingQueue<>();
    private volatile Map<String, String> current = null;
    
    public WebCapture(int width, int height, long waitMills){
        setSize(width, height);
        this.waitMills = waitMills;
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                initWebViewFX();
            }
        });
    }

    // This method is invoked on JavaFX thread
    private void initWebViewFX() {
        if(webView != null) {
            throw new IllegalStateException();
        }
        webView = new WebView();
        webView.getStyleClass().add(".scroll-bar{ -fx-scale-x: 0;-fx-scale-y: 0;-fx-scale-z: 0; }");
        System.out.println(webView.getStylesheets());
        
        // hide webview scrollbars whenever they appear.
//        hideScrollBar();
        
        // get web engine
        final WebEngine engine = webView.getEngine();
        
        // add listener for capture image when loading page is completed
        engine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {

            @Override
            public void changed(ObservableValue<? extends State> observable,
                    State oldValue, State newValue) {
                if (State.SUCCEEDED == newValue) {
                    Executors.newSingleThreadExecutor().execute(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                // need this sleep because sometimes the capturing is started before the rendering is done 
                                // if anyone knows better solution, please tell me!!
                                Thread.sleep(waitMills);
                                BufferedImage image = AwtUtils.captureView(WebCapture.this);
                                String path = current.get(PATH);
                                ImageIO.write(image, IOUtils.getExtension(path), new File(path));

                            } catch (IOException e) {
                                e.printStackTrace();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            fireNextOnJavaFXThread();

                        }
                    });
                }
                else if(State.FAILED == newValue){
                    fireNextOnJavaFXThread();
                }
            }
        });
        
        setScene(new Scene(webView, getWidth(), getHeight()));
    }


    private void fireNext() {
        if(queue.isEmpty()){
            current = null;
            return;
        }
        try {
            current = queue.take();
            if(current.get(COMMAND).equals(SHUTDOWN)){
                System.out.println("all done");
                System.exit(0);
            }
            else{
                System.out.println("loading " + current.get(URL));
                webView.getEngine().load(current.get(URL));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void hideScrollBar() {
        webView.getChildrenUnmodifiable().addListener(new ListChangeListener<Node>() {
          @Override public void onChanged(Change<? extends Node> change) {
            Set<Node> deadSeaScrolls = webView.lookupAll(".scroll-bar");
            for (Node scroll : deadSeaScrolls) {
              scroll.setVisible(false);
            }
          }
        });
    }
    
    
    public WebCapture load(final String url, final String path) throws InterruptedException {
        queue.put(ImmutableMap.of(COMMAND, "load", URL, url, PATH, path));
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                if(current == null){
                    fireNext();
                }
            }
        });
        return this;
    }

    private void shutdownAfterAllRequestsProcessed() throws InterruptedException {
        queue.put(ImmutableMap.of(COMMAND, SHUTDOWN));
        if(queue.isEmpty()){
            Platform.exit();
            // sorry sorry anti-pattern but for standalone program this should be fine...
            System.exit(0);
        }
    }

    private void fireNextOnJavaFXThread() {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                fireNext();
            }
        });
    }

    public final static String getExtension(final String fname) {
        int dotNum = fname.lastIndexOf('.');
        if (dotNum < 0) {
            return "";
        } else {
            return fname.substring(dotNum + 1);
        }
    }

}

Here is some trivial utility methods used in above web page capture class.
    // AwtUtils
    public static BufferedImage captureView(Component component) throws IOException {
        BufferedImage image = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics graphics = image.createGraphics();
        component.paint(graphics);
        graphics.dispose();
        image.flush();
        return image;
    }

    // JFrameFactory
    public static JFrame createExitJFrame(Component component, String title, int width, int height){
        JFrame frame = new JFrame(title);
        frame.getContentPane().add(component, BorderLayout.CENTER);
        frame.setPreferredSize(new Dimension(width, height));
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        return frame;
    }

    // IOUtils
    public final static String getExtension(final String fname) {
        int dotNum = fname.lastIndexOf('.');
        if (dotNum < 0) {
            return "";
        } else {
            return fname.substring(dotNum + 1);
        }
    }


Pending Task

  • Hide scroll bar
  • I read this stackoverflow post and calling setVisible(false) method.
    it works very well but only one thing I would not prefer is white space corresponding to an original scroll bar place rendered.
    Looks the best way to avoid this is defining custom css for the scroll bar.
  • Is there any better way to trigger capturing page?
  • Sometimes capturing is started before rendering is fully done.
    So I added silly sleep call before capturing the web page.

コメント

Unknown さんの投稿…
Cool
Thank's
Naima
Unknown さんの投稿…
Heloo,
do you get solution for Thread.sleep(waitMills); ??
Thank's

このブログの人気の投稿

Eclipseでコードカバレッジのハイライトを削除する方法

Eclipseには便利なコードカバレッジ表示機能が搭載されていますが、コード内に緑、赤、黄の色付けがされて煩く感じるときもあると思います。 1度カバレッジの色付けが出てしまった後に消す方法の紹介です(方法は簡単)。 下記のキャプチャの青いマーカーで示した「Remove All Sessions」のボタンを押せばすべて消えます。

「特定の文字から始まらない文字列」にマッチする正規表現

「特定の文字から始まらない文字列」 にマッチする正規表現の例です。  以下の例では、Aから始まらない文字列にマッチする正規表現を示しています。 ^(?!A).*$ 私も正規表現の組み方で四苦八苦することがあります。以下の書籍は実践的に様々な正規表現のパターンを例示してくれているので、重宝しています。

ダイソーで買った200円のドライバーセットでHDDを分解

HDDの処分 最近は個人情報の問題もあって、HDDを処分する前にちゃんとデータの消去を気にすることも多くなってきました。消去方法としては大きく分けて下記の3つがあります。 データ消去ソフトでフォーマット HDD内部のプラッタを物理破壊 データ消去を行ってくれる専門の業者や家電量販店(Sofmapやビックカメラで実施していると思います。費用発生。)に持ち込み。 データ消去ソフトでのフォーマットは簡単ですが、欠点として「フォーマットに時間がかかる」「セクタ破損などで中途半端に壊れたディスクのフォーマットができない」などがあります。 またHDD内部のプラッタの物理破壊については、HDDを分解するために、通常のプラスやマイナスドライバーではなく、星形ネジに対応したトルクスドライバーが必要とのこともあって、少し面倒です。 筆者は今回、今後もHDDの廃棄をするだろうなあと思い、思い切って自分で分解して廃棄することにチャレンジしてみました。(家電量販店に持って行くよりも安くできないかというどケチ丸出しですw) HDDの星形ネジ こんなやつです。ちなみに写真はSeagateのST2000DL003というHDDで撮影しました。 トルクスドライバー というわけで、分解のために Amazonでトルクスドライバー を探しました。 調べると T8のもだと使えそう とのことで、いろいろと物色。 セットのものとか T8一本で立派なやつとか 色々あったのですが、HDD壊すだけで800円かぁ(←どケチ)、と思って購入を躊躇。 ネット上で調べると100円ショップのダイソーでも、トルクスドライバーを販売しているとの情報をキャッチ!近所のダイソーに行って、探したところ星形のヘッド交換に対応した精密ドライバーセットがありました。 プラスが10種類、マイナスが8種類、六角が6種類、星形が6種類(今回ほしかったもの)のセットで、何とお値段税抜き200円!、税抜き200円!と安かったので、ダメもとで購入しました。 結論から言うと 買って大正解 でした。 ダイソーの精密ドライバーセット こんな商品です! 星形対応のヘッドを装着するとこんな感じ。ドライバーのグリップもゴムで滑らない様になっていて使いやす

SQLで特定の文字を組み合わせたランダムな文字列を生成

簡易的な方法として「指定した文字列からランダムに1文字選ぶ」を必要な文字の長さ分concat関数でつなげれば実現できます。 1文字ずつ文字を選ぶので、あまり性能もよくない上、セキュリティ的な観点からのランダム性も担保されていないので、あくまで開発中に必要になった時に使う程度が無難だと思います。 下記に英数字大文字小文字を含んだランダムな3文字の文字列を生成するクエリを示します。 # RAND関数で指定した文字列からランダムに1文字選択。 # 下記の例の62の部分はa~z、A~Z、1~9の文字数の合計値を入れた結果 SELECT CONCAT( SUBSTRING('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', FLOOR(RAND() * 62 + 1), 1), SUBSTRING('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', FLOOR(RAND() * 62 + 1), 1), SUBSTRING('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', FLOOR(RAND() * 62 + 1), 1) ) AS random_string;

PHPの配列(array)のメモリ使用量の考察

はじめに 最近PHP上に大量のデータをメモリ上に展開していたのですが、配列(array)の形式(連想配列 or 単純な配列)や配列の要素のデータ構造(数字、配列、文字列など)で大きくメモリ使用量に差が出てくることに気づき、簡単なプログラムを組んで調べてみました。 あくまで筆者の環境での結果なので、細かい数値は参考程度に見てください。 測定環境と方法 OS: Windows 10 PHP 7.4.5 (php-7.4.5-nts-Win32-vc15-x64) 配列に要素を追加するプログラムを書いて、PHPのmemory_get_usage(true)関数を使って実メモリ使用量を計測しました。 計測結果 No. 方式 1MB当たり作成できる 要素数 プログラム 補足 1 キーも値も整数の配列 (整数IDを想定) 28571 // 2,000,000 / 70MB $row = []; for($i = 0; $i < 2000000; $i++) { $row[] = $i; } No.2~6でテストしたプログラム中の要素数は200,000。これだけ一桁多い! 2 キーが文字列、値が整数の連想配列 8333 // 200,000 / 24MB $row = []; for($i = 0; $i < 200000; $i++) { $row[$i.'_key_string'] = $i; } キーの文字列が長い方がメモリ使用量多くなる。 3 キーが整数、値が連想配列の配列 DBから取得してきたデータを想定 2325 // 200,000 / 86MB $row = []; for($i = 0; $i < 200000; $i++) { row[] = ['id' => $i]; } 4 キーが整数、値が連想配列の配列(配列に複数の値を保持) DBから取得してきたデータを想定 2127 // 200,000 /

ADODB.streamオブジェクトを使って文字列とByte配列を相互変換(Excel VBA)

ADODB.streamオブジェクトを使って文字列をByte配列に変換するコードのサンプルです。 ExcelVBAでADODB.streamを使う際には、 1. ExcelのMicrosoft Visual Basic エディタのメニューバーから「ツール->参照設定」とたどる。 2. 表示されたダイアログからMicrosoft ActiveX Data Objectsにチェックを入れる。 という手順が必要です。 文字列からByte配列へ Private Function ADOS_EncodeStringToByte(ByVal cset As String, ByRef strUni As String) As Byte() On Error GoTo e Dim objStm As ADODB.stream: Set objStm = New ADODB.stream objStm.Mode = adModeReadWrite objStm.Open objStm.Type = adTypeText objStm.Charset = cset objStm.WriteText strUni objStm.Position = 0 objStm.Type = adTypeBinary Select Case UCase(cset) Case "UNICODE", "UTF-16" objStm.Position = 2 Case "UTF-8" objStm.Position = 3 End Select ADOS_EncodeStringToByte = objStm.Read() objStm.Close Set objStm = Nothing Exit Function e: Debug.Print "Error occurred while encoding characters" & Err.Description If objStm Is No

MySQL: SELECTの結果をUNIONして ORDER BYする際の最適化方法

SELECTの結果をUNIONして ORDER BY する際には下記の点に注意する必要があります。 無駄なメモリ消費 ソートにINDEXが利かない (≒CPU負荷増大) 対応策 可能であればPush-down Condition (各サブクエリ内でORDER BY, LIMIT, OFFSETを適用してからUNION, ORDER BYを実行する)を利用することで、 パフォーマンスを改善できる場合があります。 下記に例を示します。 もともとのクエリ SELECT tmp.* FROM ( SELECT tableA.column1, tableA.column2 FROM tableA WHERE (条件) UNION ALL SELECT tableB.column1, tableB.column2 FROM tableB WHERE (条件) ) AS tmp ORDER BY tmp.column1, tmp.column2 LIMIT 100, 20 Push-down Conditionを用いて書き直したクエリ SELECT tmp.* FROM ( SELECT tableA.column1, tableA.column2 FROM tableA WHERE (条件) ORDER BY tableA.column1, tableA.column2 LIMIT 30 # <- 10 (offset) + 20 (limit) UNION ALL SELECT tableB.column1, tableB.column2 FROM tableB WHERE (条件) ORDER BY tableB.column1, tableB.column2 LIMIT 30 # <- 10 (offset) + 20 (limit) ) AS tmp ORDER BY tmp.column1, tmp.column2 LIMIT 10, 20 ただしこのPush-down Conditionの手法も下記の場合は、効果が半減しますので注意が必要です。 OFFSETの値が大きい場合は、結局全結果セットUNIONと変わらない サブクエリ内のソートで、INDEXが効かない場合

Visual Studio 2010 SP1のアンインストール

Visual Studio 2013に乗り換えるためにVisual Studio 2010をアンインストールしようとしたところで問題発生。。。 先にVisual Studio 2010本体をアンインストールした後、Visual Studio 2010 SP1をアンインストールできなくて困っていました。 Google先生で調べたところ、以下の情報が見つかり、書かれていた通り実施したところ無事Visual Studio 2010 SP1のアンインストールに成功しました。 How to uninstall/remove Visual Studio SP1 アンインストール手順は以下の通りです。 http://www.microsoft.com/en-gb/download/details.aspx?id=23691 からMicrosoft Visual Studio 2010 Service Pack 1 (Installer)をダウンロード VS10sp1-KB983509.exeというファイル名でダウンロードされる(はず)。 コマンドプロンプトから以下のコマンドを実行 (以下の例は、c:\tempにVS10sp1-KB983509.exeがある場合) c:\temp\VS10sp1-KB983509.exe /uninstall /force ダイアログが立ち上がるので、アンインストールを選択して次へ進めばOK!

MySQLのSQL_CALC_FOUND_ROWS使用上の注意

MySQLのSQL_CALC_FOUND_ROWSを使用する際の無駄なメモリ消費に注意 ページング機能の実装などのために、ヒットした全件数を取得する必要のある場合があるかと思います。 その場合、下記のようにSQL_CALC_FOUND_ROWSを使うと、検索結果に加えて、そのクエリの全ヒット件数が取得できます。 SELECT SQL_CALC_FOUND_ROWS table.column1, table.column2 ...(途中省略)... FROM table WHERE (条件) しかし、SQL_CALC_FOUND_ROWSを使うと、「絞り込んだ結果にヒットする全べての行の結果のセットを作成する」という大きな欠点があります。 これは、LIMIT, OFFSETを指定していても実行されてしまうので、 SELECTで指定するカラム数やデータ量が多い SELECTの結果返ってくる行数が多い 場合、無駄に領域(≒メモリ)を消費してしまうので注意が必要です。 例えば、100万行検索対象としてヒットするクエリの場合、仮にLIMIT 20と指定して最初の20行を取得するようにクエリを書いても、その100万行分の結果セットが作成されてしまうということになります。 対応策 根本的な対応策としては、SQL_CALC_FOUND_ROWSを使わない(結果行数の取得はCOUNTを用いた別クエリで取得する、結果行数をあらかじめサマリーテーブルに保持しておく)ことですが、 SQL_CALC_FOUND_ROWSをどうしても使う必要がある場合は、 SELECT SQL_CALC_FOUND_ROWS primary_id のように最低限のカラムを指定して結果行セットを取得 (LIMIT OFFSET指定前提) 取得したprimary_idを使って必要なデータを取得 して、SQL_CALC_FOUND_ROWSで使用する領域をできるだけ減らすことで、対応するしかないと思います。 世の中ではLIMIT, OFFSETは使わない方がよいとよく書かれていますが、 SQL_CALC_FOUND_ROWSは、書いてしまえばどんなときも検索にヒットする全結果行セットを作成するので、同じくらい使用する際には注意が必要です。

PHPでファイルを指定した行数ごとに分割

ファイルを指定した行数ごとに分割するためには、Linuxのsplitコマンドを使えば簡単に実現できます。 PHPではexec関数にsplitコマンドを渡して実行すればよいですが、下記の弱点があります。 Linuxのコマンドに依存 (PHPの場合はほとんどLinux環境で動作させることが普通なのでそこまで問題にならないかも知れません)。 exec関数は慎重に引数を渡さないと、OSコマンドインジェクション脆弱性を引き起こす可能性がある。 そこで、今回はPHPでファイルを指定した行数ごとに分割するプログラムを書いてみました。 <?php class FileSplitter { private $lines; private $fileCount; public function split($filePath, $linesPerFile, $outputDir) { $this->fileCount = 0; $this->lines = null; $file = new \SplFileObject($filePath); $lineCount = 0; try{ while (!$file->eof()) { if($lineCount % $linesPerFile === 0) { $this->writeToFile($this->generateOutputFilePath($outputDir, $file)); } $this->lines[] = $file->fgets(); $lineCount++; } $this->writeToFile($this->generateOutpu