- PR -

HTMLをGIFファイルに保存

投稿者投稿内容
田中
ベテラン
会議室デビュー日: 2002/05/08
投稿数: 54
投稿日時: 2006-09-24 16:29
HTMLをGIFファイルに保存

田中と申します。


http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=6751&forum=12&start=8&15
を参考にして、サーバー側でHTMLをイメージファイルに落とすプログラムを作成したのですが、作成されるイメージが安定せず困っています。
一回目の実行では空白しか表示されず、2回目の実行でイメージが出力されるという具合です。

今は少しでも安定させようと1秒のウエイトを入れていますが、描画が完了したタイミングでイメージを取得するにはどうしたら良いのでしょうか?

宜しくお願いします。

一応ソースを添付します。

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.filechooser.FileFilter;

public class H2J1 {

public static void main(String[] args) {
JFrame frame;
JEditorPane html;

String url=args[0];
String filename = args[1];

// HTML を表示するコンポーネント
html = new JEditorPane();
try {
// プロトコルが指定されていない場合は http:// を追加する
if (url.indexOf("://") == -1) {
url = "http://" + url;
}

html.setPage(url);
} catch (IOException ex) {
ex.printStackTrace();
}

// html = new JEditorPane("text/html","");
// html.setText("あああ1<br>いいい<br>ううう<br>");


frame = new JFrame("HTML Image");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(html);
frame.setSize (300,150);

frame.setVisible(true);

// 出力用イメージの生成
Dimension dim = html.getSize();
BufferedImage image = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);

// イメージからグラフィックコンテキストを取得
Graphics g = image.getGraphics();

// JEditorPane をイメージに書き込む
// paintComponent は proteced なので使用できない
html.paint(g);

// ちょっと待つ(タイミングが分からない)
try {
Thread.sleep(1000); // 1秒間停止
} catch (InterruptedException e) {
System.out.println(e);
}

boolean result = false;
try {
// イメージの出力 Image I/O を使用
result = ImageIO.write(image, "jpeg", new File(filename));
} catch (Exception e) {
e.printStackTrace();
result = false;
}

// 使い終わったグラフィックコンテキストを開放
g.dispose();

System.exit(0);
}
}

unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2006-09-24 17:38
引用:

田中さんの書き込み (2006-09-24 16:29) より:
http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=6751&forum=12&start=8&15
を参考にして、サーバー側でHTMLをイメージファイルに落とすプログラムを作成したのですが、作成されるイメージが安定せず困っています。


引用:

田中さんの書き込み (2006-09-24 16:29) より:
// JEditorPane をイメージに書き込む
// paintComponent は proteced なので使用できない
html.paint(g);


参照元記事がすでにそうなのだと思いますが、paint メソッド(や paintComponent メソッド)を直接呼んでいる時点で、そのプログラムは正しくないと思います。paint メソッドがそういう用途のためのメソッドではないからです。そのため、タイミングに依存してしまっているのでしょう。
でも、かといって、paint メソッドを呼ばずにやろうとすると、どうすれば良いのかが分かりませんね。どうやればいいんでしょう?

--
unibon {B73D0144-CD2A-11DA-8E06-0050DA15BC86}
nagise
ぬし
会議室デビュー日: 2006/05/19
投稿数: 1141
投稿日時: 2006-09-25 09:07
swingのEDT(イベントディスパッチスレッド)を利用して
待機すればあるいはうまく描画終了を捕らえられるかもしれません。

javax.swing.SwingUtilities.invokeAndWait()
メソッドについて調べてみてください。
きっとswingのスレッド処理関連の記事がいくつか見つかることでしょう。

# JFrame.EXIT_ON_CLOSEを使っているのがどうにも気色悪いなぁ。
びしばし
大ベテラン
会議室デビュー日: 2002/03/13
投稿数: 181
投稿日時: 2006-09-25 11:54
お手軽に考えるなら、paintComponent()をオーバーライドしてしまうとか。
コード:

html = new JEditorPane() {
protected void paintComponent(Graphics g){
super.paintComponent(g);
hoge(g); // こんな感じに。
}
};



[ メッセージ編集済み 編集者: びしばし 編集日時 2006-09-25 11:55 ]
nagise
ぬし
会議室デビュー日: 2006/05/19
投稿数: 1141
投稿日時: 2006-09-25 15:38
簡単だろうと高をくくってサンプルコード書いてみたんですが、うまくいかず。
意外に落とし穴は深そうでした。

とりあえず、JavaのThreadについてのある程度の知識と
swingはEDT(イベントディスパッチスレッド)と呼ばれる
制御スレッドからのみアクセスするシングルスレッドモデルを
採用しているということを理解していれば、
描画イベントを正しいタイミングで取得することが可能でしょう。

しかし、落とし穴としてJEditorPaneが実際に処理する際に
用いているEditorKitの実装HTMLEditorKitの非同期ロードがあります。
http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/javax/swing/text/html/HTMLEditorKit.html
クラスドキュメントの「非同期ロード」を参照してください。
要するにHTMLの読み込みをスレッド立てて並列処理しているんですよね。

この設定をはずさないと、JEditorPaneがHTMLの読み込みが終わったタイミングが
わからないため、描画途中の画像を取得してしまいます。
ドキュメントには

「createDefaultDocument メソッドをオーバーライドすれば、プロパティを変更できます。」

と記述されていますが、設定すべきプロパティの具体的な説明がない。

こちらは作成されるHTMLDocumentのAPIドキュメントですが
http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/javax/swing/text/html/HTMLDocument.html

「このドキュメントは、追加読み込みをサポートしています。
TokenThreshold プロパティにより、どれくらいの量の解析がバッファに格納されると
ドキュメントの要素構造の更新を行うかを制御します。」

との記述があるのですが、「TokenThreshold プロパティ」に具体的に
どんな値を入れるべきなのか記述がないのですね…。
そもそも本当にTokenThresholdプロパティの設定なのだか。

とりあえず、問題は2点で、

1.swingの描画イベントを適切にフックする方法について
2.HTMLEditorKitの非同期ロードを抑制する方法について

だと思われます。
squeak
会議室デビュー日: 2005/05/31
投稿数: 6
投稿日時: 2006-09-25 16:04
こんな感じ。

html = new JEditorPane();
html.setEditorKit(new MyEditorKit());



class MyEditorKit extends HTMLEditorKit{
public Document createDefaultDocument() {
HTMLDocument doc=(HTMLDocument)super.createDefaultDocument();
doc.setAsynchronousLoadPriority(-1);
return doc;
}
}
nagise
ぬし
会議室デビュー日: 2006/05/19
投稿数: 1141
投稿日時: 2006-09-26 08:59
引用:

squeakさんの書き込み (2006-09-25 16:04) より:
こんな感じ。

html = new JEditorPane();
html.setEditorKit(new MyEditorKit());



class MyEditorKit extends HTMLEditorKit{
public Document createDefaultDocument() {
HTMLDocument doc=(HTMLDocument)super.createDefaultDocument();
doc.setAsynchronousLoadPriority(-1);
return doc;
}
}



おかげで惜しいところまではできたのですが…。
結局、HTMLDocumentが読み込みを完了したことをフックしないと
いけないという所で詰まってしまいました。
余裕のあるときにソース見てみるか…。
nagise
ぬし
会議室デビュー日: 2006/05/19
投稿数: 1141
投稿日時: 2006-09-27 12:49
結構難儀なコードなりました。

JEditorPane.setPage()が最後に"page"というプロパティの変更の
イベントを発していたのでそいつで同期してみました。
EDTとの絡みもあってスレッドを制御するのが難しい…。
私の環境(Windows版 JDK5)ではとりあえず動作していますが
別種のVMでうまく動くのか不安は残ってます。

コード:
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;

import javax.imageio.ImageIO;
import javax.swing.Action;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.ViewFactory;

/**
 * HTMLのレンダリングイメージをjpegで保存するサンプル
 * @author nagise
 */
public class HtmlCapture {
	public static void main(String[] args) throws Exception {
		String url = args[0];
		final String filename = args[1];
		if (url.indexOf("://") == -1) {
			url = "http://" + url;
		}
		HtmlCapture capture = new HtmlCapture(320,150, new URL(url));
		BufferedImage image = capture.getImage();
		ImageIO.write(image, "jpeg", new File(filename + System.currentTimeMillis() + ".jpeg"));
	}

	private URL url;
	private int width;
	private int height;

	HtmlCapture(int width, int height, URL url) {
		this.width = width;
		this.height = height;
		this.url = url;
	}

	public BufferedImage getImage() throws InterruptedException, IOException {
		JEditorPaneEx editorPane = new JEditorPaneEx();
		editorPane.setEditable(false);
		Listener listener = new Listener(editorPane);
		editorPane.addPropertyChangeListener(listener);
		editorPane.setPage(this.url);
		editorPane.setPreferredSize(new Dimension(this.width, this.height));
		final JFrame frame = new JFrame();
		frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
		frame.getContentPane().add(editorPane);

		SwingUtilities.invokeLater(new Runnable(){
			public void run() {
				frame.setVisible(true);
				frame.pack();
			}
		});
		try {
			listener.joinThread();
			return editorPane.getImage();
		} finally {
			SwingUtilities.invokeLater(new Runnable(){
				public void run() {
					frame.dispose();
				}
			});
		}
	}

	/**
	 * 画像をキャプチャするためのJEditorPaneの拡張
	 */
	public static class JEditorPaneEx extends JEditorPane {
		private Object lock = new Object();
		private boolean captureFlag;
		private BufferedImage captureImage;

		/** キャプチャのフラグ設定 */
		public void captureImage() {
			synchronized (this.lock) {
				this.captureFlag = true;
				this.repaint();
			}
		}
		/** キャプチャのフラグが立った時点で描画をジャックする */
		public void paint(Graphics g) {
			if (this.captureFlag) {
				this.captureFlag = false;
				Graphics g2 = null;
				BufferedImage image = new BufferedImage(
						this.getWidth(), this.getHeight(),
						BufferedImage.TYPE_INT_RGB);
				g2 = image.getGraphics();
				super.paint(g2);
				synchronized (this.lock) {
					this.captureImage = image;
					this.lock.notifyAll();
				}
				g.drawImage(image, 0, 0, null);
			} else {
				super.paint(g);
			}
		}
		public BufferedImage getImage() {
			synchronized (this.lock) {
				try {
					if (this.captureImage == null) {
						this.lock.wait();
					}
					return this.captureImage;
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				} finally {
					this.captureImage = null;
				}
			}
		}
		/** 非同期ロードをしないように設定 */
		protected EditorKit createDefaultEditorKit() {
			EditorKit kit = super.createDefaultEditorKit();
			AsynchronousProxyEditorKit ret = new AsynchronousProxyEditorKit(kit);
			return ret;
		}
	}
	/**
	 * JEditorPaneExが描画を行った際のプロパティ変更イベントを拾って
	 * キャプチャ可能となるタイミングまでwaitをかける
	 */
	public class Listener implements PropertyChangeListener, Runnable {
		private JEditorPaneEx editorPane;
		private Thread thread;
		private Object lock = new Object();
		public Listener(JEditorPaneEx editorPane) {
			this.editorPane = editorPane;
		}
		public void propertyChange(PropertyChangeEvent evt) {
			if ("page".equals(evt.getPropertyName())) {
				SwingUtilities.invokeLater(new Runnable(){
					public void run() {
						synchronized (Listener.this.lock) {
							Listener.this.thread = new Thread(Listener.this);
							Listener.this.thread.setDaemon(true);
							Listener.this.thread.start();
							Listener.this.lock.notifyAll();
						}
					}
				});
			}
		}
		public void run() {
			this.editorPane.captureImage();
		}
		public void joinThread() throws InterruptedException {
			synchronized (this.lock) {
				if (this.thread == null) {
					this.lock.wait();
				}
				this.thread.join();
			}
		}
	}
	/**
	 * EditorKitのcreateDefaultDocument()をオーバーライドして
	 * AbstractDocument.setAsynchronousLoadPriority(-1)を設定し
	 * Documentの非同期ロードを抑制する
	 */
	public static class AsynchronousProxyEditorKit extends EditorKit {
		private EditorKit target;
		AsynchronousProxyEditorKit(EditorKit target) {
			this.target = target;
		}
		public Document createDefaultDocument() {
			AbstractDocument doc = (AbstractDocument) this.target.createDefaultDocument();
			doc.setAsynchronousLoadPriority(-1);
			return doc;
		}
		public String getContentType() {
			return target.getContentType();
		}
		public ViewFactory getViewFactory() {
			return target.getViewFactory();
		}
		public Action[] getActions() {
			return target.getActions();
		}
		public Caret createCaret() {
			return target.createCaret();
		}
		public void read(InputStream arg0, Document arg1, int arg2) throws IOException, BadLocationException {
			target.read(arg0, arg1, arg2);
		}
		public void write(OutputStream arg0, Document arg1, int arg2, int arg3) throws IOException, BadLocationException {
			target.write(arg0, arg1, arg2, arg3);
		}
		public void read(Reader arg0, Document arg1, int arg2) throws IOException, BadLocationException {
			target.read(arg0, arg1, arg2);
		}
		public void write(Writer arg0, Document arg1, int arg2, int arg3) throws IOException, BadLocationException {
			target.write(arg0, arg1, arg2, arg3);
		}
	}
}

スキルアップ/キャリアアップ(JOB@IT)