seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

JAVAで透過画像(透過PNG)をクリップボードに格納する方法(Windows編)

Windows上のJAVA実行環境で、透過でない画像をクリップボードに設定するのは簡単である。

単に、Transferableを実装したクラスでBufferedImageなど、java.awt.Image派生オブジェクトを返せば、それだけでOKである。

たとえば、こんな感じに。

/**
 * クリップボードに現在の画像をコピーする.<br>
 */
protected void onCopy() {
	Toolkit tk = Toolkit.getDefaultToolkit();
	final BufferedImage image = imagePanel.getImage();
	if (image == null) {
		tk.beep();
		return;
	}
	Clipboard cb = tk.getSystemClipboard();
	cb.setContents(new Transferable() {
		@Override
		public boolean isDataFlavorSupported(DataFlavor flavor) {
			return flavor.equals(DataFlavor.imageFlavor);
		}
		@Override
		public DataFlavor[] getTransferDataFlavors() {
			return new DataFlavor[] {DataFlavor.imageFlavor};
		}
		@Override
		public Object getTransferData(DataFlavor flavor)
				throws UnsupportedFlavorException, IOException {
			if (flavor.equals(DataFlavor.imageFlavor)) {
				return image;
			}
			throw new UnsupportedFlavorException(flavor);
		}
	}, null);
}

しかし、透過画像の場合は簡単ではないのである。

何も考えないで透過画像をクリップボードに入れると…。

JAVAWindowsとの間のクリップボードを状態を確かめるために、以下のようなサンプルアプリケーションを作ってみた。

この画像はアルファチャンネルをもつTYPE_INT_ARGB形式のBufferedImageに格納されている。

これを描画した画面で、灰色に見えるのは透過部分である。(背景色が見えている。)


これを、何も考えずにクリップボードに格納してみる。

/**
 * クリップボードに画像を格納するためのTransferable抽象実装.<br>
 * @author seraphy
 */
abstract class ImageTransferable implements Transferable {

	private BufferedImage image;
	
	public BufferedImage getImage() {
		return image;
	}
	
	public void setImage(BufferedImage image) {
		this.image = image;
	}

	@Override
	public boolean isDataFlavorSupported(DataFlavor flavor) {
		for (DataFlavor supportedFlavor : getTransferDataFlavors()) {
			if (supportedFlavor.equals(flavor)) {
				return true;
			}
		}
		return false;
	}
}

/**
 * 標準の画像転送のまま.<br>
 * @author seraphy
 */
class ImageTransferableStd extends ImageTransferable {
	
	@Override
	public Object getTransferData(DataFlavor flavor)
			throws UnsupportedFlavorException, IOException {
		System.out.println("ImageTransferableStd: " + flavor);
		if (flavor.equals(DataFlavor.imageFlavor)) {
			return getImage();
		}
		throw new UnsupportedFlavorException(flavor);
	}
	
	@Override
	public DataFlavor[] getTransferDataFlavors() {
		return new DataFlavor[] {DataFlavor.imageFlavor};
	}
}


すると、以下のような結果になる。(JRE6 on Windows7の場合)

左から、PNG, ビットマップ、JPEGの順に、「形式を指定して貼り付け」をしたものである。

ここで、クリップボードに格納されている形式を列挙してみよう。

     2: CF_BITMAP
     3: CF_METAFILEPICT
     8: CF_DIB
    14: CF_ENHMETAFILE
    17: CF_DIBV5
 49860: PNG
 49861: JFIF

これらはJAVA実行環境が、「image/x-java-image」(DataFlavor.imageFlavorで示すMIME)に対するデフォルトのマッピングとして、イメージを変換して格納してくれているものである。


PNGJFIF(JPEG File Interchange Format: JPEGファイル形式)もついているのが有り難い。


PNG形式は、その番号で見るとおりWindowsの事前定義フォーマットではないが、Microsoft Office 2010や、Gimp2, Fireworksといったアプリケーションは、この形式を認識してくれる。*1

前述の例で、PNG画像として貼り付けした場合は透過情報が認識されているのは、このためである。


もちろん、画像を受け取れる全てのアプリケーションが「PNG」形式を認識してくれるわけではない。

一般的にはCF_BITMAPか、CF_DIBあたりで受け取るであろう。


なので、ここで問題なのは、ビットマップ(CF_BITMAP, CF_DIB, CF_DIBV5)で背景色が真っ黒になっていることである。

どうやら、これは比較的有名な事象のようである。

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4720930


WindowsJREの実装では、CF_DIBとしてビットマップを構築するときにARGB情報のうち、RGBだけ使って0で初期化されたキャンバスに展開しているらしく、結果としてアルファで透過されている部分は黒くなってしまうらしい。

そして、出力しているのはCF_DIBだけで、透過情報をもつDIBV5形式での出力はなされていないようである。


では、なぜ、CF_DIBV5形式や、CF_BITMAP形式があるのか、というと、恐らく、Windows側で、
CF_BITMAP, CF_DIB, CF_DIBV5の各形式は、いずれか1つを設定してクリップボードをクローズすると、他の形式を補完してくれる、という動きになっているからであろう。*2

http://msdn.microsoft.com/en-us/library/ms649013%28v=vs.85%29.aspx


JPEG形式でもおかしなことになっているのは、アルファチャネルをもつTYPE_INT_ARGB形式を渡しちゃダメ、ってことなんであろうか。とりあえず、不味い状況なのは分る。

とりあえず、透過情報を消してコピーする

解決方法としては、単に透過情報を無くしてしまえば良い、ということで、単純に背景色を設定してアルファチャネルを無くしてみる。

/**
 * 透過情報なしの画像転送.<br>
 * @author seraphy
 */
class ImageTransferableOpaque extends ImageTransferable {
	
	@Override
	public Object getTransferData(DataFlavor flavor)
			throws UnsupportedFlavorException, IOException {
		System.out.println("ImageTransferableOpaque: " + flavor);
		if (flavor.equals(DataFlavor.imageFlavor)) {
			return convertNoneTransparent(getImage());
		}
		throw new UnsupportedFlavorException(flavor);
	}
	
	protected BufferedImage convertNoneTransparent(BufferedImage org) {
		int w = org.getWidth();
		int h = org.getHeight();
		BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_3BYTE_BGR);
		Graphics2D g = image.createGraphics();
		try {
			g.setBackground(Color.WHITE);
			g.clearRect(0, 0, w, h);
			g.drawImage(org, 0, 0, w, h, 0, 0, w, h, null);

		} finally {
			g.dispose();
		}
		return image;
	}
	
	@Override
	public DataFlavor[] getTransferDataFlavors() {
		return new DataFlavor[] {DataFlavor.imageFlavor};
	}
}

そうすれば、以下のようになる。

PNGでもビットマップでもJPEGでも、画像は、きちんと貼り付けられている。


PNGでも透過情報がなくなってしまったのは残念であるが、これが、一般的なアプリケーションに渡す上では必要最低限の処置といえるだろう。

PNG形式だけは透過画像をコピーできるようにしておく。

コピーはできるようになったが、ここでの議題は、いかにして透過画像をコピーするか、ということである。


前述で見たとおり、DataFlavor.imageFlavorが示す「image/x-java-image」形式でクリップボードに格納すると、CF_DIB, CF_DIBV5, JFIF, PNG(とメタファイルとか)などの形式が同時に作成されるが、

この中で透過情報をもてる「PNG」だけは何とか生かすことはできないであろうか?



それには、まず、JAVA実行環境が、どのようにMIME形式のデータフレーバをWindowsのネイティブフォーマットと関連づけているのか、その仕組みを調べてみる必要がある。


まず、ネイティブフォーマットと、MIMEを関連づけているのは 「$JRE/lib/flavormap.properties」というファイルである。
(もちろん、これはネイティブ形式とJAVA内部で環境の独立したMIMEマッピングとを決めるためのファイルなので、Mac OS XLinuxでは、また違った設定となる。)

lib/flavormap.properties (JRE6 on Windows7の場合)
UNICODE\ TEXT=text/plain;charset=utf-16le;eoln="\r\n";terminators=2
TEXT=text/plain;eoln="\r\n";terminators=1
HTML\ Format=text/html;charset=utf-8;eoln="\r\n";terminators=1
Rich\ Text\ Format=text/rtf
HDROP=application/x-java-file-list;class=java.util.List
PNG=image/x-java-image;class=java.awt.Image
JFIF=image/x-java-image;class=java.awt.Image
DIB=image/x-java-image;class=java.awt.Image
ENHMETAFILE=image/x-java-image;class=java.awt.Image
METAFILEPICT=image/x-java-image;class=java.awt.Image
LOCALE=application/x-java-text-encoding;class="[B"
UniformResourceLocator=application/x-java-url;class=java.net.URL
UniformResourceLocator=text/uri-list;eoln="\r\n";terminators=1
UniformResourceLocator=text/plain;eoln="\r\n";terminators=1

この中で画像に関係するのは、「image/x-java-image」の値をもつものである。

キーとしてはPNG, JFIF, DIB, ENHMETAFILE, METAFILEPICTが、それに該当する。


これがシステムデフォルトのマッピングとなり、Transferable#getTransferDataFlavors() で返すDataFlavorが「image/x-java-image」である場合、
これらのネイティブ形式としてWindowsクリップボードに格納される、というわけである。


この中の「PNG」形式に着目し、それを「image/png」という独自のフレーバーに設定し、独自にPNG形式を返すように実装すれば、PNGとして透過画像を貼り付けられそうである。


ネイティブ形式のマッピングはSystemFlavorMapクラスで実行時に変更することができる。

/**
 * 透過ありと透過情報なしの混合の画像転送.<br>
 * @author seraphy
 */
class ImageTransferablePNG extends ImageTransferableOpaque {
	
	/**
	 * PNGフレーバー.<br>
	 */
	public static final DataFlavor PNG_FLAVOR;
	
	static {
		try {
			PNG_FLAVOR = new DataFlavor("image/png");

		} catch (ClassNotFoundException ex) {
			throw new RuntimeException(ex);
		}
	}

	/**
	 * システムフレーバーマップで、ネイティブフレーバー「PNG」を、「image/png」に割り当てる.<br>
	 */
	public static void setupFlavorMapping() {
		FlavorMap fm = SystemFlavorMap.getDefaultFlavorMap();
		if (fm instanceof SystemFlavorMap) {
			SystemFlavorMap systemFlavorMap = (SystemFlavorMap) fm;
			systemFlavorMap.setNativesForFlavor(PNG_FLAVOR, new String[] {"PNG"});
		}
	}
	
	@Override
	public Object getTransferData(DataFlavor flavor)
			throws UnsupportedFlavorException, IOException {
		System.out.println("ImageTransferablePNG: " + flavor);
		if (flavor.equals(PNG_FLAVOR)) {
			// PNG形式の場合、PNGのファイル形式に変換したバイナリデータをクリップボードに格納する.
			BufferedImage image = getImage();
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			try {
				ImageIO.write(image, "png", bos);
			} finally {
				bos.close();
			}
			return new ByteArrayInputStream(bos.toByteArray());
			
		} else if (flavor.equals(DataFlavor.imageFlavor)) {
			// 通常の画像コピーの場合
			return convertNoneTransparent(getImage());
		}
		throw new UnsupportedFlavorException(flavor);
	}
	
	@Override
	public DataFlavor[] getTransferDataFlavors() {
		return new DataFlavor[] {PNG_FLAVOR, DataFlavor.imageFlavor};
	}
}

独自の形式を返す場合、Transferable#getTransferData() メソッドの戻り値は InputStream派生クラスとする。

これで自由にバイナリデータをクリップボードに格納できるようになる。


今回は、ImageIO.write()を使って、TYPE_INT_ARGBの透過画像をPNG形式のファイルデータに変換し、それをクリップボードに詰めている。


SystemFlavorMap#setNativesForFlavor() でネイティブ形式として指定する文字列は、内部的には、RegisterClipboardFormat APIを呼び出して、その形式を登録しているようである。

なので、flavormap.propertiesに無い、アプリケーション独自のフォーマット(たとえば「PNG2」とか)を指定しても機能するし、Windows側で、それを受け取ろうと思えば受け取ることができるようになる。


こうすると、以下のような結果になる。

とりあえず、PNGを受け取れるのであれば透過画像として貼り付けられるようになり、透過情報をもてない旧式のビットマップや、そもそも透過情報がないJPEGでは透過なし画像として貼り付けることができるようになる。

ここまでやれば、Microsoft Office 2010やGimp2, Fireworksといった著名なアプリケーションでは透過情報を渡すことができるようになり、実用的には、ほぼ問題なくなるかと思われる。

(PNG形式をサポートしていないアプリケーションは、そもそもCF_DIBやCF_BITMAPといった、透過情報を扱えない形式でしか認識しないものがほとんどではないかと思う。)*3

がんばってCF_DIBも自前で処理してみる実験。

これは余興となり何らメリットもないことであるが、

JAVA実行環境側の「image/x-java-image」によるクリップボード形式への格納をバイパスして、CF_DIB形式も自前で何とかしてみる。


/**
 * BMPを自前でなんとかする実験.<br>
 * @author seraphy
 */
class ImageTransferableBMP extends ImageTransferablePNG {
	
	/**
	 * BMPフレーバー.<br>
	 */
	public static final DataFlavor BMP_FLAVOR;
	
	static {
		try {
			BMP_FLAVOR = new DataFlavor("image/bmp");

		} catch (ClassNotFoundException ex) {
			throw new RuntimeException(ex);
		}
	}

	/**
	 * システムフレーバーマップで、ネイティブフレーバー「DIB」を、「image/bmp」に割り当てる.<br>
	 */
	public static void setupFlavorMapping() {
		FlavorMap fm = SystemFlavorMap.getDefaultFlavorMap();
		if (fm instanceof SystemFlavorMap) {
			SystemFlavorMap systemFlavorMap = (SystemFlavorMap) fm;
			systemFlavorMap.setNativesForFlavor(BMP_FLAVOR, new String[] {"DIB"});
		}
	}
	
	@Override
	public Object getTransferData(DataFlavor flavor)
			throws UnsupportedFlavorException, IOException {
		System.out.println("ImageTransferableBMP: " + flavor);
		if (flavor.equals(BMP_FLAVOR)) {
			BufferedImage org =  convertNoneTransparent(getImage());
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			try {
				// BMP形式で出力
				ImageIO.write(org, "bmp", bos);

			} finally {
				bos.close();
			}

// ※ ためしにGimp2で書き出したDIBV5形式のBMPファイルから読み込ませてるテスト.
//			FileInputStream fis = new FileInputStream("test.dibv5");
//			try {
//				byte[] buf = new byte[4096];
//				for (;;) {
//					int rd = fis.read(buf);
//					if (rd <= 0) {
//						break;
//					}
//					bos.write(buf, 0, rd);
//				}
//			} finally {
//				fis.close();
//			}

			byte[] data = bos.toByteArray();
			final int offset = 14; // BMPヘッダはクリップボードには不要.
			return new ByteArrayInputStream(data, offset, data.length - offset);
		}
		throw new UnsupportedFlavorException(flavor);
	}
	
	@Override
	public DataFlavor[] getTransferDataFlavors() {
		return new DataFlavor[] {BMP_FLAVOR}; // BMPフレーバーのみ
	}
}

単に、DIBというネイティブ形式を「image/bmp」というフレーバに割り当てて、Transferable#getTransferData() でBMP形式(ただし、BMPヘッダを除く)を返すだけである。
(ImageIOのbmp出力は、いわゆる普通のbmp形式なので、DIBV5の透過情報をもつ形式はサポートしていない。)

実験のため、サポートしているフレーバーとして「image/bmp」以外を外してある。


それを実行すると、クリップボードに格納されたフォーマットは以下のようになる。

     2: CF_BITMAP
     8: CF_DIB
    17: CF_DIBV5

getTransferDataFlavors()で「image/x-java-image」のサポートを切っているので、クリップボードに格納されたのは「image/bmp」のデータだけである。

ネイティブ形式として「DIB」を指定したので、恐らく、「CF_DIB」だけがクリップボードに格納され、CF_DIBV5とCF_BITMAPはWindowsが補完したものであろう、と推測される。


ためしにGimp2で生成したDIBV5形式のBMPを返せるようにしてみたが、背景色が真っ黒となり透過情報は認識しなかった。

CF_DIBとして転送されているのであれば、そもそもCF_DIBには透過情報はもてないのであるから、これは当然の結果であろう。


可能性として、ネイティブ形式として「DIB」ではなく、「DIBV5」のようなものが指定できるのであれば、CF_DIBV5形式の透過情報を転送することは可能かもしれない。

だが、CF_DIBV5は事前定義された形式であり、RegisterClipboardFormat APIのように名前で定義するものではないため、
SystemFlavorMap#setNativesForFlavor() で、うまく指定する方法がみつからなかった。


ここが解決できれば、透過画像のコピーが、より上手くゆくような気もするのだが、残念ながら、その方法をみつけることはできなかった。

結論

Windows環境において、JAVAから透過画像をコピーする場合は、

  1. まず、Windowsで実行しているのであれば、クリップボードにコピーする前に画像はアルファチャンネルを外して背景色をつけておく。
  2. ただし、PNG形式だけは透過画像のままPNGファィル形式としてクリップボードに設定する。

という手順を踏むことで、そこそこ実用的になるものと思われる。


※ なお、Mac OS Xでは、このような手間は必要なくて、普通に透過画像は当たり前にコピーできる。なにも考えなくてよろしい。

Linuxの場合は、また別の問題があり、いろいろややこしい状況になっているが、これについては次の機会に。


今回の実験ソース一式

JAVAソース。
package jp.seraphyware.clipboardimagetest;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.FlavorMap;
import java.awt.datatransfer.SystemFlavorMap;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

/**
 * メインフレーム.
 * @author seraphy
 */
public class MainFrame extends JFrame {

	private static final long serialVersionUID = 1L;
	
	private ImagePanel imagePanel = new ImagePanel();

	public MainFrame() {
		setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		addWindowListener(new WindowAdapter() {
			public void windowClosing(java.awt.event.WindowEvent e) {
				onClose();
			};
		});

		initComponent();
		
		setSize(300, 300);
		setLocationByPlatform(true);
	}
	
	private void initComponent() {
		setTitle("ClipboardImageTest");
		
		Container contentPane = getContentPane();
		contentPane.setLayout(new BorderLayout());
		contentPane.add(imagePanel, BorderLayout.CENTER);
		
		JMenuBar menubar = new JMenuBar();
		setJMenuBar(menubar);
		
		JMenu mnuFile = new JMenu("File");
		mnuFile.setMnemonic('F');
		menubar.add(mnuFile);
		
		
		JMenuItem mnuNew = new JMenuItem("New");
		mnuNew.setMnemonic('N');
		mnuNew.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				onNew();
			}
		});
		mnuFile.add(mnuNew);
		
		JMenuItem mnuOpen = new JMenuItem("Open");
		mnuOpen.setMnemonic('O');
		mnuOpen.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				onOpen();
			}
		});
		mnuFile.add(mnuOpen);

		JMenuItem mnuExit = new JMenuItem("Exit");
		mnuExit.setMnemonic('X');
		mnuExit.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				onClose();
			}
		});
		mnuFile.add(mnuExit);
		
		JMenu mnuEdit = new JMenu("Edit");
		mnuEdit.setMnemonic('E');
		menubar.add(mnuEdit);
		
		JMenuItem mnuCopy = new JMenuItem("copy (standard)");
		mnuCopy.setMnemonic('C');
		mnuCopy.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				onCopy(new ImageTransferableStd());
			}
		});
		mnuEdit.add(mnuCopy);

		JMenuItem mnuCopy2 = new JMenuItem("copy (opaque)");
		mnuCopy2.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				onCopy(new ImageTransferableOpaque());
			}
		});
		mnuEdit.add(mnuCopy2);

		JMenuItem mnuCopy3 = new JMenuItem("copy (PNG)");
		mnuCopy3.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				// システムフレーバーのPNGを独自のものに書き換える.
				ImageTransferablePNG.setupFlavorMapping();
				onCopy(new ImageTransferablePNG());
			}
		});
		mnuEdit.add(mnuCopy3);

		JMenuItem mnuCopy4 = new JMenuItem("copy (BMP)");
		mnuCopy4.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				// システムフレーバーのDIBを独自のものに書き換える.
				ImageTransferableBMP.setupFlavorMapping();
				onCopy(new ImageTransferableBMP());
			}
		});
		mnuEdit.add(mnuCopy4);
	}
	
	protected void onClose() {
		dispose();
	}
	
	protected void onNew() {
		imagePanel.setImage(null);
	}
	
	protected void onOpen() {
		JFileChooser chooser = new JFileChooser();
		if (JFileChooser.APPROVE_OPTION != chooser.showOpenDialog(this)) {
			return;
		}
		File selectedFile = chooser.getSelectedFile();

		try {
			BufferedImage img = ImageIO.read(selectedFile);
			int w = img.getWidth();
			int h = img.getHeight();
			BufferedImage argb = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
			Graphics2D g = argb.createGraphics();
			try {
				g.drawImage(img, 0, 0, w, h, 0, 0, w, h, null);
			} finally {
				g.dispose();
			}
			
			imagePanel.setImage(argb);
			
		} catch (IOException ex) {
			JOptionPane.showMessageDialog(this, ex.getMessage());
		}
	}

	/**
	 * クリップボードに現在の画像をコピーする.<br>
	 */
	protected void onCopy(ImageTransferable transferable) {
		Toolkit tk = Toolkit.getDefaultToolkit();
		BufferedImage image = imagePanel.getImage();
		if (image == null) {
			tk.beep();
			return;
		}
		Clipboard cb = tk.getSystemClipboard();
		transferable.setImage(image);
		cb.setContents(transferable, null);
	}
	
	/**
	 * エントリポイント.<br>
	 * @param args 未使用
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				try {
					UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

				} catch (Exception ex) {
					ex.printStackTrace();
				}
				new MainFrame().setVisible(true);
			}
		});
	}
	
}

/**
 * クリップボードに画像を格納するためのTransferable抽象実装.<br>
 * @author seraphy
 */
abstract class ImageTransferable implements Transferable {

	private BufferedImage image;
	
	public BufferedImage getImage() {
		return image;
	}
	
	public void setImage(BufferedImage image) {
		this.image = image;
	}

	@Override
	public boolean isDataFlavorSupported(DataFlavor flavor) {
		for (DataFlavor supportedFlavor : getTransferDataFlavors()) {
			if (supportedFlavor.equals(flavor)) {
				return true;
			}
		}
		return false;
	}
}

/**
 * 標準の画像転送のまま.<br>
 * @author seraphy
 */
class ImageTransferableStd extends ImageTransferable {
	
	@Override
	public Object getTransferData(DataFlavor flavor)
			throws UnsupportedFlavorException, IOException {
		System.out.println("ImageTransferableStd: " + flavor);
		if (flavor.equals(DataFlavor.imageFlavor)) {
			return getImage();
		}
		throw new UnsupportedFlavorException(flavor);
	}
	
	@Override
	public DataFlavor[] getTransferDataFlavors() {
		return new DataFlavor[] {DataFlavor.imageFlavor};
	}
}

/**
 * 透過情報なしの画像転送.<br>
 * @author seraphy
 */
class ImageTransferableOpaque extends ImageTransferable {
	
	@Override
	public Object getTransferData(DataFlavor flavor)
			throws UnsupportedFlavorException, IOException {
		System.out.println("ImageTransferableOpaque: " + flavor);
		if (flavor.equals(DataFlavor.imageFlavor)) {
			return convertNoneTransparent(getImage());
		}
		throw new UnsupportedFlavorException(flavor);
	}
	
	protected BufferedImage convertNoneTransparent(BufferedImage org) {
		int w = org.getWidth();
		int h = org.getHeight();
		BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_3BYTE_BGR);
		Graphics2D g = image.createGraphics();
		try {
			g.setBackground(Color.WHITE);
			g.clearRect(0, 0, w, h);
			g.drawImage(org, 0, 0, w, h, 0, 0, w, h, null);

		} finally {
			g.dispose();
		}
		return image;
	}
	
	@Override
	public DataFlavor[] getTransferDataFlavors() {
		return new DataFlavor[] {DataFlavor.imageFlavor};
	}
}

/**
 * 透過ありと透過情報なしの混合の画像転送.<br>
 * @author seraphy
 */
class ImageTransferablePNG extends ImageTransferableOpaque {
	
	/**
	 * PNGフレーバー.<br>
	 */
	public static final DataFlavor PNG_FLAVOR;
	
	static {
		try {
			PNG_FLAVOR = new DataFlavor("image/png");

		} catch (ClassNotFoundException ex) {
			throw new RuntimeException(ex);
		}
	}

	/**
	 * システムフレーバーマップで、ネイティブフレーバー「PNG」を、「image/png」に割り当てる.<br>
	 */
	public static void setupFlavorMapping() {
		FlavorMap fm = SystemFlavorMap.getDefaultFlavorMap();
		if (fm instanceof SystemFlavorMap) {
			SystemFlavorMap systemFlavorMap = (SystemFlavorMap) fm;
			systemFlavorMap.setNativesForFlavor(PNG_FLAVOR, new String[] {"PNG"});
		}
	}
	
	@Override
	public Object getTransferData(DataFlavor flavor)
			throws UnsupportedFlavorException, IOException {
		System.out.println("ImageTransferablePNG: " + flavor);
		if (flavor.equals(PNG_FLAVOR)) {
			// PNG形式の場合、PNGのファイル形式に変換したバイナリデータをクリップボードに格納する.
			BufferedImage image = getImage();
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			try {
				ImageIO.write(image, "png", bos);
			} finally {
				bos.close();
			}
			return new ByteArrayInputStream(bos.toByteArray());
			
		} else if (flavor.equals(DataFlavor.imageFlavor)) {
			// 通常の画像コピーの場合
			return convertNoneTransparent(getImage());
		}
		throw new UnsupportedFlavorException(flavor);
	}
	
	@Override
	public DataFlavor[] getTransferDataFlavors() {
		return new DataFlavor[] {PNG_FLAVOR, DataFlavor.imageFlavor};
	}
}

/**
 * BMPを自前でなんとかする実験.<br>
 * @author seraphy
 */
class ImageTransferableBMP extends ImageTransferablePNG {
	
	/**
	 * BMPフレーバー.<br>
	 */
	public static final DataFlavor BMP_FLAVOR;
	
	static {
		try {
			BMP_FLAVOR = new DataFlavor("image/bmp");

		} catch (ClassNotFoundException ex) {
			throw new RuntimeException(ex);
		}
	}

	/**
	 * システムフレーバーマップで、ネイティブフレーバー「DIB」を、「image/bmp」に割り当てる.<br>
	 */
	public static void setupFlavorMapping() {
		FlavorMap fm = SystemFlavorMap.getDefaultFlavorMap();
		if (fm instanceof SystemFlavorMap) {
			SystemFlavorMap systemFlavorMap = (SystemFlavorMap) fm;
			systemFlavorMap.setNativesForFlavor(BMP_FLAVOR, new String[] {"DIB"});
		}
	}
	
	@Override
	public Object getTransferData(DataFlavor flavor)
			throws UnsupportedFlavorException, IOException {
		System.out.println("ImageTransferableBMP: " + flavor);
		if (flavor.equals(BMP_FLAVOR)) {
			BufferedImage org =  convertNoneTransparent(getImage());
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			try {
				// BMP形式で出力
				ImageIO.write(org, "bmp", bos);

			} finally {
				bos.close();
			}

// ※ ためしにGimp2で書き出したDIBV5形式のBMPファイルから読み込ませてるテスト.
//			FileInputStream fis = new FileInputStream("test.dibv5");
//			try {
//				byte[] buf = new byte[4096];
//				for (;;) {
//					int rd = fis.read(buf);
//					if (rd <= 0) {
//						break;
//					}
//					bos.write(buf, 0, rd);
//				}
//			} finally {
//				fis.close();
//			}

			byte[] data = bos.toByteArray();
			final int offset = 14; // BMPヘッダはクリップボードには不要.
			return new ByteArrayInputStream(data, offset, data.length - offset);
		}
		throw new UnsupportedFlavorException(flavor);
	}
	
	@Override
	public DataFlavor[] getTransferDataFlavors() {
		return new DataFlavor[] {BMP_FLAVOR}; // BMPフレーバーのみ
	}
}

/**
 * イメージを表示するパネル.<br> 
 * @author seraphy
 */
class ImagePanel extends JPanel {

	private static final long serialVersionUID = 1L;

	private BufferedImage image;
	
	public BufferedImage getImage() {
		return image;
	}
	
	public void setImage(BufferedImage image) {
		this.image = image;
		repaint();
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		if (image != null) {
			int w = image.getWidth();
			int h = image.getHeight();
			g.drawImage(image, 0, 0, w, h, 0, 0, w, h, null);
		}
	}
}
Windowsクリップボードフォーマットの調査用(C++)。
// EnumClipFormat.cpp
//

#include "stdafx.h"
#include <string>
#include <sstream>
#include <iomanip>
#include <list>

int _tmain(int argc, _TCHAR* argv[])
{
	using namespace std;

	if ( !OpenClipboard(NULL)) {
		return 1;
	}

	typedef basic_string<TCHAR> tstring;
	typedef basic_stringstream<TCHAR> tsringstream;

	std::list<tstring> results;

	UINT fmt = 0;
	do {
		fmt = EnumClipboardFormats(fmt);
		if (fmt) {
			tsringstream str;

			str << setw(6) << right << fmt;
			str << left << _T(": ");

			TCHAR szName[256] = {0};
			if (GetClipboardFormatName(fmt, szName, 256)) {
				str << szName;
			} else {
				tstring fmtName;
				switch (fmt) {
					case 1: fmtName = _T("CF_TEXT"); break;
					case 2: fmtName = _T("CF_BITMAP"); break;
					case 3: fmtName = _T("CF_METAFILEPICT"); break;
					case 4: fmtName = _T("CF_SYLK"); break;
					case 5: fmtName = _T("CF_DIF"); break;
					case 6: fmtName = _T("CF_TIFF"); break;
					case 7: fmtName = _T("CF_OEMTEXT"); break;
					case 8: fmtName = _T("CF_DIB"); break;
					case 9: fmtName = _T("CF_PALETTE"); break;
					case 10: fmtName = _T("CF_PENDATA"); break;
					case 11: fmtName = _T("CF_RIFF"); break;
					case 12: fmtName = _T("CF_WAVE"); break;
					case 13: fmtName = _T("CF_UNICODETEXT"); break;
					case 14: fmtName = _T("CF_ENHMETAFILE"); break;
					case 15: fmtName = _T("CF_HDROP"); break;
					case 16: fmtName = _T("CF_LOCALE"); break;
					case 17: fmtName = _T("CF_DIBV5"); break;
					default: fmtName = _T("unknown");
				}
				str << fmtName;
			}

			results.push_back(str.str());

		}
	} while (fmt != 0);

	CloseClipboard();

	results.sort();

	for (list<tstring>::const_iterator ite = results.begin();
		ite != results.end(); ++ite) {
		_tprintf(_T("%s\n"), (*ite).c_str());
	}

	return 0;
}

*1:番号が大きいのは、RegisterClipboardFormat APIで名前からATOMを生成しているためであろう。これらは名前で判定されるので番号は実行環境によっては異なるかもしれない。

*2:CF_METAFILEPICT, CF_ENHMETAFILEは、CF_DIBと同様にJAVAが自前で出力しており、Windowsが補完したものではない。

*3:残念なことに、Photoshop Elements 9では透過情報は、そもそもサポートしていないようである。逆にPSE9側で透過画像をコピーしても、クリップボードフォーマットにはPNGなどの形式は見あたらず、他のアプリにコピーしても透過情報はないことから、PSEでは、そもそも透過画像のクリップボードをサポートしていないのだと思う。