seraphyの日記

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

JAVAで透過画像(透過PNG)をクリップボードに設定する方法(Linux編)

前回、Windows環境で、透過画像をクリップボードにいれる方法について実験したので、
今回は、Linux環境(Ubuntu10.04 + sun-java-jdk6)での実験を行う。(※2012/5/5追記、Ubuntu12.04 + OpenJDK7で簡単に試した結果を追記しました。)

結論的にいうと、現在のLinux上のJavaSE6で透過画像をクリップボードにいれるためには、いくつかの注意点があり、Windowsと同じにやっては、うまく動かないことに注意すること。

ただ、その解決策が単純ではないので順を追って説明する。

まずは、何も考えずに透過画像をクリップボードに入れてみる。

前回と同様、JAVAで作られた以下のサンプルアプリケーションを動かして、
なにも考えずにクリップボードに透過画像をいれてみる。

下記はJAVAによるアプリケーションである。

すると、X-Window上のクリップボードに格納されている形式は、以下のようになる。

以下のクリップボードの中身をチェックするアプリケーションは、JAVAからでは確認の意味がないので、C++(GTK)による、ちょっとしたツールを自作したもので行っている。(ソースコードは末尾にある。)

PNG, JFIF(jpeg)、加えてgifがあるようである。
PNGがあるということは、Gimpで透過画像を受け取れる可能性は、濃厚である。

Gimpの場合はPNGを受け取れる。

やはり、うまく取れる。

Linuxで画像編集といえば、Gimpであろうから、これで受け取れれば問題の大方は片づいたも同然であろう。

GIF, JPEGといった形式は不味いことになっている。

では、その他の形式はどうであろうか?

Gimpには「形式を指定して貼付」というコマンドは無さそうなので、
自作ツールでクリップボードの中身を取り出してみる。*1

JPEGの場合

(↑IEだと画像が壊れて「×」になる。FirefoxChromeだと色化けした画像が見られるだろう。)
透過画像を無理やり変換するのは、やはり不味いのであろう。今回もまた、JPEGは酷いことになっている。

私はLinuxをメインのデスクトップ環境としているわけではないので、
この「image/jpeg」「JFIF」を受け取れるアプリケーションがあるのか分からないが、
受け取った場合には、がっかりすること間違いない。

透過をもてないのだから、かわりに背景色で補正する処置が必要であろう。

GIFの場合

私の記憶によれば、GIFは透過情報を持てたような気がするが、インデックスカラーであるのも関わらず透過情報が無くなっている。

ImageIO#write()でGIFを指定する場合、TYPE_INT_ARGB(32bit)はインデックスカラーでないので上手く変換できなかったりするのであろうか。(※2011/4/11追記 ImageIO.write()でTYPE_INT_ARGBの画像をGIF形式を指定して出力した場合、透過情報は欠落することを確認した。)

まあ、GIFは私にとってどうでもいい形式なので、深くは追求しまい。

これも背景色を補正する処置が必要であろう。

ビットマップは...?

ビットマップはクリップボードに格納されていないので、そもそも使うことはできない。

WindowsBMPも、X-WindowのXPM、XBMも、どちらもない。

一見、JPEG, GIF, PNGの3つがあれば充分に思われるかもしれない。

だが、調べてみると、そうでもないのである。

それについては後述する。

とりあえず、透過情報を消してみる。

前回同様に、透過情報を消してしまえば、いずれの形式でも「見られる」画像にはなるだろう。

やり方も、結果も、前回と変わらない。

もちろん、このままではPNG形式でも透過情報が無くなってしまうことも変わりない。


PNGだけは、透過画像としてクリップボードに格納する。

PNGだけは、透過画像としてクリップボードに格納したいわけだが、これも、前回と同じ方法である。

ただし、ネイティブ形式名として、どのような名前を選ぶべきか明確ではない。

そこで、まず、標準で、どのようなマッピングがなされているのか、調べてみる。

/usr/lib/jvm/java-6-sun/jre/lib/flavormap.properties (sun-java6-jdk on ubuntu10.04)
UTF8_STRING=text/plain;charset=UTF-8;eoln="\n";terminators=0
TEXT=text/plain;eoln="\n";terminators=0
STRING=text/plain;charset=iso8859-1;eoln="\n";terminators=0
FILE_NAME=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

画像に関係するMIME「image/x-java-image」を値とするのは、PNG, JFIFだけであるし、
先ほど見た「image/gif, image/png, image/jpeg」やらを、誰が設定しているのかよく分からない。
(すくなくとも、このflavormap.propertiesに記載されていない。)


もちろん、flavormap.propertiesにない形式でも実行時に設定できるし、
仮にネイティブ形式を設定しなければ、DataFlavorのMIME名をもとに勝手に名前をつけてクリップボードに格納する、という動きになっているので*2


おそらくは、JAVA実行環境側で「image/x-java-image」のMIMEを変換するときに、「image/gif, image/png, image/jpeg」の各形式も同時に設定しているのであろう。
Windowsのように自動的にGnome Desktopが補完しているとは思えない。)

つまり、今回は、flavormap.propertiesは、まったく当てにできない感じである。


次に、ネイティブアプリケーションである「Gimp2」が透過画像をクリップボードにコピーしたときに、
どんな形式で格納しているのか、見てみる。


すると、以下のような結果が得られた。
(ちなみに、Firefoxの画像のコピーや、アクセサリの「スクリーンキャプチャ」を使っても同じ結果となった。)

Gimp2/Firefox等で画像をコピーした場合
MULTIPLE
TARGETS
TIMESTAMP
image/bmp
image/jpeg
image/png
image/tiff
image/x-MS-bmp
image/x-bmp
image/x-ico
image/x-icon
image/x-win-bitmap

JAVAで画像をコピーすると設定されていた「PNG」「JFIF」といった項目は無く、画像はすべてMIMEでの表現となっている。


このあたりの「クリップボードマネージャ」の仕様は、以下のURLあたりにあるようだ。

http://freedesktop.org/wiki/Specifications

これによると、LinuxGnome Desktop/KDE全般の決まりごととしてコンテンツはMIMEで表すことになっているようである。
(これは、クリップボードドラッグアンドドロップ、それにファイルの関連付けにも関わってくるようである。)

http://freedesktop.org/wiki/Specifications/shared-mime-info-spec

よって、PNG画像については、「image/png」とすることが一般的だと見てよいだろう。


ということは、普通は「PNG」「JFIF」という大文字表記のものは、Linux環境のネイティブアプリでは使われていないと思われる。(JAVAが、これらを出力しているのは、たとえば、Windows環境用に作られたJAVAアプリ間の互換性のためなのかもしれない。)


また、確証はないしドキュメントにも、はっきりとは書かれていないが、GDKのPixbuf*3
では、gif, tiff, jpeg, png, bmpの各形式をサポートしているので、GTKgtk_clipboard_set_image APIあたりを使ってPixbufをクリップボードに格納すると、
一括してimage/gif, image/tiff, image/jpeg, image/png, image/bmpの全形式を設定してくれているのではないか、と推測する。
(なので、特別なことをしなければ全部同じ形式を出力しているのではないかな、と推測する。)

PNGのネイティブ形式として「image/png」を明示する。

以上のことから、透過画像をPNGとしてクリップボードに設定するには、

前回のWindowsの時と同様に、PNG画像に対するネイティブ形式を「image/png」と明示し、
DataFlavorを「image/x-java-image」ではなく「image/png」の独自のものに切り替えれば良い。

/**
 * システムフレーバーマップで、ネイティブフレーバー「image/png」にDataFlavor「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", "image/png", "image/x-png"});
    }
}

前回のWindowsでの対策との違いは、「image/png」「image/x-png」という文字列が増えただけである。


なお、もともと「PNG」「image/png」「image/x-png」の各ネイティブ形式は、DataFlavor.imageFlavor(image/x-java-image)である場合に自動的に変換されて出力されるもののため、これを明示的に指定して上書きしないと、標準の変換で出力されてしまうことになる。

そのため、「image/x-png」や「PNG」といった形式も、あえてネイティブ形式として指定する必要がある。


これで改めて実行してみると、こうなる。

JFIF
PNG
image/gif
image/jpeg
image/png
image/x-png

特に形式が増えた分けではないが、「PNG」「image/png」「image/x-png」のいずれかで画像を取得すれば、透過情報の活きたPNG画像として、それ以外では背景色を設定した非透過画像として取得できるようになる。


ここまで対応すれば、ほぼ実用的になった、と思えるのだが、しかし、実はまだ足りない。

実はビットマップも必要だった!!

Linux上でJAVAからクリップボードに画像をコピーすると、ビットマップ形式が無いのは前述のとおり。

Linuxだから当たり前か、なんて思ったら、実は、当たり前じゃなかったのである。


Gimp2も、Firefoxも、アクセサリーにある「スクリーンショット」も、OpenOffice3.2も、実はビットマップを出力している。

Firefox、Gimp2の例は既に見たので、ここでは、OpenOffice3.2の例を見てみる。

OpenOffice3.2の画像のクリップボードコピー

正体不明の独自形式が並ぶ中で、「image/bmp」がいる。

pngjpegを差し置いて、bmpだけは居るのである。


つまり、bmpを出さないJAVAのほうが当たり前じゃなかったのである。


さらに、ここに重要な点が有る。


OpenOffice3.2は、「image/png」「image/jpeg」「image/gif」を出力しないだけでなく、認識さえもしていないようなのである。
(まぁ、出力してなければ当然かもしれないけど。)

だからJAVA側で「image/bmp」を出力していない以上、OpenOffice3では何も画像を貼り付けられないことになる。


OpenOffice以外のLinux上のアプリケーションで「image/bmp」を必要とするアプリがあるのかは分らないが、GTKを使うアプリケーションは皆、出力している状況を考えると、JAVAでも出力しておくほうが無難であることは間違いないだろう。

image/bmpも出力する。

ビットマップ形式でも出力されるようにするには、PNGの場合と同じように、新しいDataFlavorを作り、MIMEを「image/bmp」にして、
そのMIMEに対するネイティブ形式の文字列をSystemFlavorMapに登録してやれば良い。

幸い、前回、「BMPを自力でなんとかする実験」をしているので、ほとんど同じものがつかえる。

/**
 * BMP + PNG + 標準
 * @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);
        }
    }

    /**
     * システムフレーバーマップで、「image/bmp」を割り当てる.<br>
     */
    public static void setupFlavorMapping() {
        ImageTransferablePNG.setupFlavorMapping();

        FlavorMap fm = SystemFlavorMap.getDefaultFlavorMap();
        if (fm instanceof SystemFlavorMap) {
            SystemFlavorMap systemFlavorMap = (SystemFlavorMap) fm;
            systemFlavorMap.setNativesForFlavor(BMP_FLAVOR, new String[] {
                    "image/bmp", "image/x-bmp", "image/x-win-bitmap", "image/x-MS-bmp"});
        }
    }
    
    @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();
            }

            byte[] data = bos.toByteArray();
            return new ByteArrayInputStream(data);
        }
        // BMP以外は親に任せる。
        return super.getTransferData(flavor);
    }
    
    @Override
    public DataFlavor[] getTransferDataFlavors() {
        ArrayList<DataFlavor> flavors = new ArrayList<DataFlavor>();
        flavors.addAll(Arrays.asList(super.getTransferDataFlavors()));
        flavors.add(BMP_FLAVOR);
        return flavors.toArray(new DataFlavor[flavors.size()]);
    }
}

ネイティブ形式として、「image/bmp」の他に、「image/x-bmp」「image/x-win-bitmap」
「image/x-MS-bmp」と節操なく大量に指定しているのは、Gimp2らと同じにしてみたためである。

どうして、こんなに沢山の別名があるのか、というと、bmp形式に対する正式なメディアタイプが存在しないためかもしれない。*4


OpenOffice3.2が認識するのは「image/bmp」のようであるから、ようやくOpenOffice3.2にも画像をはりつけられるようになったはずである。

これで「image/png」でなら透過画像のまま、「image/bmp」「image/jpeg」「image/gif」としてならば非透過画像として受け取れるようになった。

ここまでやれば、一般的なアプリケーションとの間で画像のクリップボードによる受け渡しが、そこそこ実用的になったのではないかと思う。

でも、OpenJDKは…。

だが、しかし、まだ残念なことがある。


Fedora14付属のOpenJDKで実行した場合、画像をクリップボードに格納することができないのである。


これは標準の方法でもダメであるし、上記のいろんな対策をしたとしても全く効果がない。

自作ツールでクリップボードに何が格納されているのか確認してみたら、「何も格納していない」ということが判明した。

つまり、何も機能していないのである。(クリップボードを空にさえしていない。)


sun-javaも、openjdkも、どちらもJDK6で、どちらも64ビット版であるから、Javaのバージョンとか、32/64ビット間の問題とかではないだろう。

(ここまで全く何も動かないということは、バグというよりは意図的な何かを感じないこともない。何かの事情がありそうな気がする。
詳しく調べていないが、ここはネイティブコードが関係する部分なので、ライセンスとか、そうゆう問題があるのかもしれない。)

[seraphy@fedore14 ~]$ java -version
java version "1.6.0_24"
Java(TM) SE Runtime Environment (build 1.6.0_24-b07)
Java HotSpot(TM) 64-Bit Server VM (build 19.1-b02, mixed mode)
[seraphy@fedore14 ~]$ sudo alternatives --config java
[sudo] password for seraphy:

3 プログラムがあり 'java' を提供します。

  選択       コマンド
-----------------------------------------------
   1           /usr/lib/jvm/jre-1.5.0-gcj/bin/java
   2           /usr/lib/jvm/jre-1.6.0-openjdk.x86_64/bin/java
*+ 3           /usr/java/default/bin/java

Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2
[seraphy@fedore14 ~]$ java -version
java version "1.6.0_20"
OpenJDK Runtime Environment (IcedTea6 1.9.7) (fedora-52.1.9.7.fc14-x86_64)
OpenJDK 64-Bit Server VM (build 19.0-b09, mixed mode)
[seraphy@fedore14 ~]$


これは、もうopenjdk側で改善してくれるのを待つほか無い状況だと思われる。

※ 2012/5/5追記 Ubuntu12.04LTS + OpenJDK7 で試したところ、上記のsun-java6での対応方法をとったものをOpenJDK7で実行したところ、LibreOffice, GIMPともに画像を受け取れるようになっており、この問題はOpenJDK7では解決されているのではないかと思われる。

結論

Linux環境におけるクリップボードの扱いは、いくつかの点で問題がある。

  • 仕様書に書かれているflavormap.propertiesが、ほとんど意味をなしていないように見える。
  • 他のアプリケーションと異なり、image/bmpを出力していない。
  • 他のアプリケーションと異なり、image/gifを出力しているが、透過画像はうまく扱えていない。
  • openjdk6では、そもそも機能しない。(※2012/5/5追記 openjdk7なら大丈夫のようである。)

しかし、

  1. クリップボードに渡す場合に透過画像には背景色をつける。
  2. ただし、image/png, image/x-pngのネイティブフレーバに対して明示的にPNG画像を渡す。
  3. 加えて、image/bmp, image/x-bmp, image/x-MS-bmp, image/x-win-bitmapのネイティブフレーバに対して明示的にBMP画像を渡す。
  4. アプリケーションはsun-java6-jdkで動作させる。

という手順と対策を行うことで、Linuxのデスクトップ環境でも、透過画像のクリップボードへの格納が、
そこそこ実用的になるのではないかと思われる。


※ もっとも、不具合っぽいものや改善されそうなところが多々あるので、今後のバージョンやJDK7あたりで、
また、ガラリと状況が変る可能性はあると思われる。

今回の実験ソース

JAVAソース

前回とほぼ同じで、差分は本文中にあるので省略。

Linux環境のクリップボードの形式調査用(GTK/C++)

GTK使いではないので、間違い多々あるかもしれないが、とりあえず検証には使えた。
http://developer.gnome.org/gtk/2.24/ あたりのマニュアルを参照しながら作成したものである。

#include <stdlib.h>
#include <stdio.h>

#include <list>
#include <string>
#include <sstream>

#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

typedef std::basic_string<gchar> gstring;
typedef std::list<gstring> gstring_list;
typedef std::basic_stringstream<gchar> gstringstream;

/**
 * メインフレーム
 * @author seraphy
 */
class MainFrame
{
private:

  /**
   * Gtkウィンドウ
   */
  GtkWidget *window;

  /**
   * テキストエリア
   */
  GtkWidget *textarea;

  /**
   * クリップボード形式名入力ボックス
   */
  GtkWidget *formatnamebox;

  /**
   * アクセラレータ
   */
  GtkAccelGroup *accel_group;

public:

  MainFrame()
  {
    // ウィンドウの構築
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    // タイトルとサイズの設定
    gtk_window_set_title(GTK_WINDOW(window), "EnumClipFormat");
    gtk_window_set_default_size(GTK_WINDOW(window), 250, 200);

    // ウィンドウの閉じるイベントのハンドル
    g_signal_connect(G_OBJECT(window),
        "delete_event",
        G_CALLBACK(gtk_main_quit), // 直接、gtk_main_quitを呼ぶ
        NULL);

    // アクセラレータの初期化
    accel_group = gtk_accel_group_new();
    gtk_window_add_accel_group(GTK_WINDOW(window), accel_group);

    // コンポーネントの構築
    initComponent();
  }

  /**
   * メインフレームを表示する
   */
  void show()
  {
    gtk_widget_show_all(window);
  }

private:

  /**
   * コンポーネントの初期化
   */
  void initComponent()
  {
    // メニューとコンテンツの基本レイアウト
    GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
    gtk_container_add(GTK_CONTAINER(window), vbox);

    // メニューの構築
    GtkWidget *menubar = gtk_menu_bar_new();
    gtk_box_pack_start(GTK_BOX(vbox), menubar, FALSE, FALSE, 0);

    GtkWidget *menu = gtk_menu_new();


    GtkWidget *menu_chk_cb = gtk_menu_item_new_with_mnemonic(
        "クリップボードのチェック(_V)");
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_chk_cb);

    gtk_widget_add_accelerator(menu_chk_cb,
        "activate", accel_group, GDK_v, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);

    g_signal_connect(G_OBJECT(menu_chk_cb),
        "activate", G_CALLBACK(chk_cb_event), this);


    GtkWidget *menu_chk_primary = gtk_menu_item_new_with_mnemonic(
        "プライマリセレクションのチェック(_P)");
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_chk_primary);

    gtk_widget_add_accelerator(menu_chk_primary,
        "activate", accel_group, GDK_p, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);

    g_signal_connect(G_OBJECT(menu_chk_primary),
        "activate", G_CALLBACK(chk_primary_event), this);


    gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());

    GtkWidget *menu_save = gtk_menu_item_new_with_mnemonic("クリップボードの保存(_S)");
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_save);

    gtk_widget_add_accelerator(menu_save,
        "activate", accel_group, GDK_s, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);

    g_signal_connect(G_OBJECT(menu_save),
        "activate", G_CALLBACK(save_event), this);

    gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());


    GtkWidget *menu_exit = gtk_menu_item_new_with_mnemonic("終了(_Q)");
    /* menu_exit = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT, accel_group); */
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_exit);

    gtk_widget_add_accelerator(menu_exit,
         "activate", accel_group, GDK_q, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);

    g_signal_connect(G_OBJECT(menu_exit),
         "activate", G_CALLBACK(gtk_main_quit), NULL); // 直接、gtk_main_quitを呼ぶ

    GtkWidget *menu_item = gtk_menu_item_new_with_mnemonic("コマンド(_C)");

    gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), menu);

    gtk_menu_shell_append(GTK_MENU_SHELL(menubar), menu_item);


    // テキストエリア
    textarea = gtk_text_view_new();

    // スクロールペイン
    GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

    gtk_container_add(GTK_CONTAINER(scroll), textarea);

    gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 0);

    // テキストボックス
    GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);

    formatnamebox = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(hbox), formatnamebox, TRUE, TRUE, 0);

    GtkWidget *save_button = gtk_button_new_with_label("クリップボードを保存");
    gtk_box_pack_start(GTK_BOX(hbox), save_button, FALSE, FALSE, 0);

    gtk_signal_connect(GTK_OBJECT(save_button),
        "clicked", G_CALLBACK(save_event), this);
  }

  /**
   * クリップボードの形式の取得
   */
  void enum_target(GtkClipboard *clipboard)
  {
    GdkAtom *targets = NULL;
    gint n_targets = 0;

    if (gtk_clipboard_wait_for_targets(clipboard, &targets, &n_targets) != TRUE) {
      return;
    }

    // 通知されたクリップボード形式の一覧から
    // 形式名を列挙する
    gstring_list fmtList;

    for (int idx = 0; idx < n_targets; idx++)
    {
      gstring fmtName = gdk_atom_name(targets[idx]);
      fmtList.push_back(fmtName);
    }

    // ソート
    fmtList.sort();

    // テキストバッファに格納する
    gstringstream stm;

    for (gstring_list::const_iterator ite = fmtList.begin();
          ite != fmtList.end(); ++ite)
      stm << *ite << std::endl;

    // テキストビューに書き込む
    GtkTextBuffer *textbuf =
      gtk_text_view_get_buffer(GTK_TEXT_VIEW(textarea));

    gtk_text_buffer_set_text(textbuf, stm.str().c_str(), -1);

    g_free(targets);
  }

  /**
   * クリップボードのチェックメニューハンドラ
   */
  static void chk_cb_event(GtkWidget *widget, gpointer data)
  {
    MainFrame *me = reinterpret_cast<MainFrame*>(data);

    GtkClipboard* clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
    me->enum_target(clipboard);
  }

  /**
   * プライマリセレクションのチェックメニューハンドラ
   */
  static void chk_primary_event(GtkWidget *widget, gpointer data)
  {
    MainFrame *me = reinterpret_cast<MainFrame*>(data);

    GtkClipboard* clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
    me->enum_target(clipboard);
  }

  /**
   * 保存メニューハンドラ
   */
  static void save_event(GtkWidget *widget, gpointer data)
  {
    MainFrame *me = reinterpret_cast<MainFrame*>(data);

    const gchar *text = gtk_entry_get_text(GTK_ENTRY(me->formatnamebox));
    if (text == NULL || g_utf8_strlen(text, -1) == 0) {
      me->show_message("フォーマット名を入力してください。");
      return;
    }

    // クリップボードから該当データの取り出し
    GdkAtom atom = gdk_atom_intern(text, FALSE);

    GtkClipboard* clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
    GtkSelectionData *seldata = gtk_clipboard_wait_for_contents(clipboard, atom);
    if (seldata == NULL) {
      me->show_message("データはありません。");
      return;
    }

    // 保存先ファイル名の選択
    GtkWidget *savedlg = gtk_file_chooser_dialog_new("Save File",
         GTK_WINDOW(me->window), GTK_FILE_CHOOSER_ACTION_SAVE, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
         GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL);

    gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER (savedlg), TRUE);
    gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(savedlg), "clipboard.dat");

    if (gtk_dialog_run(GTK_DIALOG(savedlg)) == GTK_RESPONSE_ACCEPT) {
      gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER (savedlg));

      // 該当形式のデータをファイルに保存
      gint siz = gtk_selection_data_get_length(seldata);
      const guchar *binary = gtk_selection_data_get_data(seldata);

      FILE *fp = fopen(filename, "wb");
      fwrite(binary, 1, siz, fp);
      fclose(fp);

      g_free (filename);
    }
    gtk_widget_destroy(savedlg);

    gtk_selection_data_free(seldata);
  }

  /**
   * メッセージボックスの表示
   */
  void show_message(const gchar *message)
  {
    GtkWidget *msgdlg = gtk_message_dialog_new(
        GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE,
        "%s", message);
    gtk_dialog_run(GTK_DIALOG(msgdlg));
    gtk_widget_destroy(msgdlg);
  }
};

/**
 * エントリポイント
 */
int main(int argc, char *argv[])
{
  gtk_set_locale();
  gtk_init(&argc, &argv);

  MainFrame mainFrame;
  mainFrame.show();

  gtk_main();

  return 0;
}

makefile

compile:
        g++ -Wall -g enumclipformat.cc -o enumclipformat `pkg-config gtk+-2.0 --cflags --libs`

このC++(GTK)のソースコードgithubにも残しました。*5
https://github.com/seraphy/enumclipformat

*1:というか、慣れないGTK+2で、この自作ツールを作ることに、今回の時間の大部分が使われる結果となった。

*2:それが仕様であるかは、明確なドキュメントはみつけられなかったが

*3:http://developer.gnome.org/gdk-pixbuf/2.22/

*4:今後定義されるか、すでに定義されている可能性も否定できないが、どのみち互換性のために同じような処理が必要になるであろう。

*5:※2012/5/5追記、GTK3対応版のブランチも作ってみました。