JavaのGUIをベクトルデータとしてスクリーンショットを撮る
Mac OS X には、スクリーンショットの形式として PDF を選べる。 さらに驚きなのは、Tiger より前の Mac では、それがデフォルトのファイル形式だったことだ。[1] 実際にその PDF ファイルを見たことはないんだが、PDF とはいっても画像ファイルがあるだけでベクトルデータとしてスクリーンショットを撮っているという事はないと思う[要出典]。
それに発想を受けて、Java の API を見る限り、標準の機能のみで(=外部ライブラリを使わずに)、ベクトルデータとしてスクリーンショットを得られるんじゃないかと気づいたネタ。
Idea
- Java の GUI 部品の基底となるクラス
java.awt.Component
には、 部品を描画するメソッドpaint()
があり、引数で任意のグラフィックコンテクストを渡すことができる。 - Java の印刷APIのひとつにグラフィックコンテクストを使ったものがある。
- Java の印刷APIではプリンタではなく、PostScriptファイル出力を選ぶこともできる。
以上のAPIを組み合わせることで、Java の GUI 部品を PostScript としてベクトルデータとしてスクリーンショットをとれるのである。 PDF へは直接出力する機能を Java の API は有していないため PDF として得られないのが残念であるが、acrobat または ghostscript を使って PostScript から PDF に変換すればよい。
Java の印刷 API は歴史的事情でいくつか種類がある。 今回使うのはJava 印刷サービス[2]であるが、詳細は「ユーザズガイド」を参照されたし。
Code
大枠は下記の通りで、
outputFile
にcreatePrintable()
メソッド(後述)で得られた印刷ドキュメントをPostScript形式で書き込む。
final DocFlavor flavor = DocFlavor.SERVICE_FORMATTED.PRINTABLE;
StreamPrintServiceFactory[] serviceFactories;
PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
File outputFile = ...; // 保存先ファイル new File("….ps);
FileOutputStream fos = null;
try {
fos = new FileOutputStream(outputFile);
serviceFactories = StreamPrintServiceFactory
.lookupStreamPrintServiceFactories(flavor,
DocFlavor.BYTE_ARRAY.POSTSCRIPT.getMimeType());
if (serviceFactories != null && serviceFactories.length > 0) {
StreamPrintService service = serviceFactories[0]
.getPrintService(fos);
if (ServiceUI.printDialog(null, 0, 0,
new PrintService[] { service }, service, flavor, aset) != null) {
Doc doc = new SimpleDoc(createPrintable(), flavor, null);
DocPrintJob job = service.createPrintJob();
//job.addPrintJobListener(this); // お望みで(印刷状況が得られます)
job.print(doc, aset);
//job.removePrintJobListener(this); // お望みで
}
} else {
System.err.println("Print Service Not Found.");
}
} catch (Exception e1) {
// 例外処理のため略記
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e1) {
}
}
流れをざっと説明すると
- 出力のファイルストリームを開く
StreamPrintServiceFactory
に対して PostScript 対応のサービスを問い合わせてひとつ得る。- サービス情報を元にダイアログを表示する。(
ServiceUI
クラスの提供によって、印刷ダイアログが表示される。) createPrintable()
メソッド(後述)で得られた印刷ドキュメントdoc
を得る。- 印刷サービスからプリンタジョブのインスタンスを得る。
- プリンタジョブに対して印刷ドキュメント
doc
を印刷するよう要求する(印刷だが、実際には PostScript 出力)。
StreamPrintServiceFactory
クラスで印刷サービスを問い合わせているところで、
PostScript(DocFlavor.BYTE_ARRAY.POSTSCRIPT.getMimeType()
)を指定している。
手元の OpenJDK 1.6 ではここで必ずサービスが一つ入った配列が戻ってきた。
ここをDocFlavor.BYTE_ARRAY.PDF.getMimeType()
と変えたら、PDF で出力できそうだがサービスが見つからないとなってしまった。
(もっとも将来、あるいは Java 処理系の実装によってはできる環境があるかもしれないが)
次は、createPrintJob()
の中身を記載する。
private Printable createPrintable() {
Window[] windows = Window.getWindows();
ArrayList<Component> list = new ArrayList<Component>();
for (int i = 0; i < windows.length; i++) {
if (windows[i].isVisible()) {
list.add(windows[i]);
}
}
return new ComponentPrintableWrapper(list.toArray(new Component[list.size()]));
}
Window.getWindows()
で、Java プログラムから開いたすべてのウィンドウの配列が得られる。
これらのうち可視、すなわちisVisible()
が真となるウィンドウをリストアップしていく。
不可視状態のウィンドウを列挙していないのは、見えていないウィンドウはレイアウト(特にサイズ)が決まっていないことが多いため、描画することがそもそもできない、という理由のためである。
このウィンドウの配列をそのまま印刷サービスに流し込めればいいのだが、印刷用のインスタンスPrintable
の形にしなければならない。
であるので、ComponentPrintableWrapper
クラスというワッパーラッパーを作った。
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import javax.swing.RepaintManager;
class ComponentPrintableWrapper implements Printable {
private Component[] component = null;
public ComponentPrintableWrapper(Component[] component) {
this.component = component;
}
@Override
public int print(Graphics g, PageFormat format, int pageIndex)
throws PrinterException {
Graphics2D g2 = (Graphics2D) g;
if (pageIndex < component.length) {
Component paintComponent = component[pageIndex];
g2.translate(format.getImageableX(), format.getImageableY()); // 位置調整
RepaintManager.currentManager(paintComponent).setDoubleBufferingEnabled(false);
paintComponent.paint(g);
RepaintManager.currentManager(paintComponent).setDoubleBufferingEnabled(true);
return Printable.PAGE_EXISTS;
} else {
return Printable.NO_SUCH_PAGE;
}
}
}
プリンタジョブへの印刷指示job.print(doc, aset)
では、プリンタジョブから本クラスのprint()
メソッドが呼ばれる。
ページ(pageIndex
)が変わるごとにprint()
メソッドが呼ばれるので、このコードは1ページあたり1個のウィンドウが描き込まれる。
ここでRepaintManager.currentManager(paintComponent).setDoubleBufferingEnabled(false)
を呼んでいる箇所があるが、少し説明が必要であろう。
これは Swing 部品の場合はダブルバッファをしているため、コンポーネントの描画をしても内部バッファのラスタ画像が描かれてしまう。
そのため一旦ダブルバッファを無効にして直接描画してもらうようにしている。
(パフォーマンスに影響が出るため、処理後に元に戻している。)
逆に言うと、独自でダブルバッファ処理をしている場合は、そこのソースコードをいじることをしない限り、対処のしようがない。
Result
上のコードを実行している Java プログラムから SwingSet を呼び出してみるという芸当を使って、SwingSet の PostScript 出力をおこなってみた。

注意する点は、グラデーション部などは画像となっているが、境界線(border)は線とベクトルデータのままになっている。 テキストもベクトルデータのままだが、残念ながらアウトラインデータになっているのでテキスト改竄といったことを簡単にはできない。 とはいえ、inkscape の PostScript 読み込み機能(内部的には一旦PDFに変換されている)を使って読み込んだ後、変形などをして編集することが可能だ。

いくつか Look And Feel (いわゆるテーマのようなもの) を変えていくつかとってみたが、Nimbus といった半透明を使ったものや、GTK+ といった外部レンダリングエンジンを使ったものは画像として書き出されてしまう。 後者はしょうがないが、半透明などは PostScript で表現できないからそうなっているんだろうか。 (ただし、Adobe Acrobat / Distiller および Ghostscript には拡張命令の形で PDF 化の際に半透明にできる。[3]。) あと、ここには載せないが、Swing ではない AWT の GUI 部品は本手法を使ってベクトルデータを抜き出すことは出来なかった。


Conclusion
Java の GUI 部品を PostScript としてベクトルデータとしてスクリーンショットをとることは可能であった。 しかし、AWT の GUI 部品ではないことと、半透明を含まない単純な描画であることなど条件は厳しい。 また、ダブルバッファを有する場合それを無効にしなければならない。
半透明などをベクトルデータとして出力できない問題を解決するには、グラフィックコンテクストの描画命令から SVG などの出力を行うものが必要となるだろう[4]。 この技法によるベクトルデータ吸出しを防ぐ方法としてダブルバッファが有効であるだろう(とはいえ完全ではない)。
References
- Mac OS X: 画面を撮影するためのショートカット ↩
- Java 印刷サービス API ユーザーガイド1 - 入門 ↩
- Adobe Solutions Network, "Adobe Acrobat 7.0 pdfmark Reference Manual"(和文), 2004, pp.33-37 ↩
- SVG Generator: SVGGraphics2D, The Apache Software Foundation ↩