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

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...

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: 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 # ただしこのPush-down Conditionの手法も下記の場合は、効果が半減しますので注意が必要です。 OFFSETの値が大きい場合は、結局全結果セットUNIONと変わらない サブクエリ内のソートで、INDEXが効かない場合

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...

MySQLでGROUP_CONCATしたフィールドに対して疑似的にLIMITを実現する方法

MySQLでGROUP_CONCATしたフィールドに対して疑似的にLIMITを実現するには、GROUP_CONCATで生成された文字列に対して、SUBSTRING_INDEXを使って文字列を切り出す方法が簡単です。 # 下記はid, codeをカラムに持つテーブルで、codeカラムでGROUP BYして、codeごとにidをlimitで取得する例です。 SELECT code ,SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY id DESC), ',', :limit) # :limitの部分に取得したい件数を指定。 FROM table GROUP BY code 長所は、下記のように条件を指定して、LIMIT句で取得件数を指定したクエリを何度も発行する必要がないところです。 特に、一回あたりクエリの発行コストが高い場合は、GROUP_CONCATを使って一度に取得したほうが最終的な実行時間をかなり節約することができます。 SELECT id FROM table WHERE code = 'A' ORDER BY id DESC LIMIT :limit; ただし、短所も多いので、使用する際は、これらの短所について十分に考慮したうえで使ってください。 GROUP_CONCATで生成された文字列に対して、SUBSTRING_INDEXを使って文字列を切り出すという文字列処理なので、無駄が多い。 特にGROUP_CONCATで生成された元の文字列が長い場合。 GROUP_CONCATの区切り文字が、GROUP_CONCATされる元の文字列に含まれていると正しくLIMITされない。 例:GROUP_CONCATされる元の文字列にカンマが含まれているのに、カンマを区切り文字で指定している場合。 GROUP_CONCATの最大文字数制限を超えた場合は、機能しない。 MySQLのGROUP_CONCATの最大文字数制限は、「SHOW VARIABLES LIKE '%group_concat%';」で調べられます。デフォルト値は1024のようです。 ...