2016年2月17日

ブラウザから別のソフトに画像をドラッグ&ドロップ

Windows におけるブラウザでの OLE Drag & Drop に関するメモ書き。

ブラウザで表示している img タグをドラッグアンドドロップで他のソフトに持ち込むことが出来たりすることがある。この仕組みでブラウザからファイルを受け付けたい場合、受け取り側では主に RegisterDragDrop で IDropTarget インターフェイスを登録しておき、呼ばれたら適宜必要な形式で抽出したりすることになる。

ただし、この周辺の挙動はブラウザごとにまちまちであるため、それぞれに対応が必要になる。
ここではケースごとに各ブラウザでどのように振る舞われているのかを簡単にまとめておく。

src に URL を指定した img タグの場合

いわゆる普通の img タグの場合。
  • Microsoft Internet Explorer 11.63.10586.0
    以下のフォーマットが利用できる。

    CF_TEXT
    CF_DIB
    CF_UNICODETEXT
    CF_HDROP
    RegisterClipboardFormat("HTML Format")
    RegisterClipboardFormat("DragContext")
    RegisterClipboardFormat("UniformResourceLocatorW")
    RegisterClipboardFormat("msSourceUrl")
    RegisterClipboardFormat("UntrustedDragDrop")
    RegisterClipboardFormat("DragImageBits")

    CF_TEXT / CF_UNICODETEXT / RegisterClipboardFormat("UniformResourceLocatorW") は画像の URL が入っている。
    RegisterClipboardFormat("msSourceUrl") は画像の URL ではなく、ドラッグ元のページの URL らしきものが格納される。
    RegisterClipboardFormat("UntrustedDragDrop") はこの辺に解説がある
    RegisterClipboardFormat("HTML Format") は HTML が画像タグの他にいくつかのタグがセットになった断片で格納されている。HTML の前にヘッダ情報がテキストで付いていて、恐らくこれを利用すると img タグのみを抜き出すこともできるのだと思われる。contentEditable なエレメントで利用される形式のような気がする。
    RegisterClipboardFormat("DragContext") は未調査。
    CF_DIB / RegisterClipboardFormat("DragImageBits") には未テストであるものの恐らくデコード済みのビットマップデータが取得可能だと思う。
    CF_HDROP からはデコードされていない画像へのパスが取得できる。

    ただし、IE には保護モードがあり、これが ON の場合フォーマットの列挙はできるが読み取ろうとすると API 呼び出しでエラーが起こる。これに関してはこの辺に解説があるようだけど細かい部分に関しては未調査。
  • Microsoft Edge 25.10586.0.0 EdgeHTML 13.10586
    以下のフォーマットが利用できる。

    CF_TEXT
    CF_DIB
    CF_UNICODETEXT
    CF_HDROP
    RegisterClipboardFormat("HTML Format")
    RegisterClipboardFormat("Preferred DropEffect")
    RegisterClipboardFormat("UniformResourceLocatorW")
    RegisterClipboardFormat("msSourceUrl")
    RegisterClipboardFormat("UntrustedDragDrop")
    RegisterClipboardFormat("msSourceTopLevelWindow")

    IE と概ね同じで、RegisterClipboardFormat("Preferred DropEffect") と RegisterClipboardFormat("msSourceTopLevelWindow") が増えて RegisterClipboardFormat("DragContext") がなくなった。
    格納されている内容も IE と同じで、かつ Edge には保護モードがない。
  • Google Chrome 48.0.2564.103
    以下のフォーマットが利用できる。

    CF_TEXT
    CF_UNICODETEXT
    RegisterClipboardFormat("FileContents")
    RegisterClipboardFormat("FileGroupDescriptorW")
    RegisterClipboardFormat("HTML Format")
    RegisterClipboardFormat("UniformResourceLocator")
    RegisterClipboardFormat("DragContext")
    RegisterClipboardFormat("UniformResourceLocatorW")
    RegisterClipboardFormat("text/html")
    RegisterClipboardFormat("DragImageBits")
    RegisterClipboardFormat("chromium/x-renderer-taint")
    RegisterClipboardFormat("text/x-moz-url")

    CF_TEXT / CF_UNICODETEXT / RegisterClipboardFormat("UniformResourceLocator") / RegisterClipboardFormat("UniformResourceLocatorW") / RegisterClipboardFormat("text/x-moz-url") は全て URL が取得できる。
    RegisterClipboardFormat("HTML Format") は IE と同じフォーマットのデータが取得できる。RegisterClipboardFormat("text/html") も似たようなデータだが、これは純粋に掴んだ画像タグがそのまま手に入る。
    とりあえず RegisterClipboardFormat("FileContents") を使うと画像の元データが手に入る。RegisterClipboardFormat("FileContents") のデータを読み取る時はここにあるように FORMATETC 構造体の LINDEX は -1 ではなく 0 から始まるインデックス番号にしなければいけないようだ。また、ファイル名として使うべき名前を RegisterClipboardFormat("FileGroupDescriptorW") から知ることができ、複数のファイルがドラッグされる場合は本来ここでの登場順とインデックス番号が対応するものと思われる。
  • Firefox 44.0.1
    以下のフォーマットが利用できる。

    CF_DIB
    CF_UNICODETEXT
    CF_HDROP
    CF_MAX
    RegisterClipboardFormat("FileContents")
    RegisterClipboardFormat("FileGroupDescriptorW")
    RegisterClipboardFormat("HTML Format")
    RegisterClipboardFormat("Preferred DropEffect")
    RegisterClipboardFormat("DragContext")
    RegisterClipboardFormat("UniformResourceLocatorW")
    RegisterClipboardFormat("text/html")
    RegisterClipboardFormat("DragImageBits")
    RegisterClipboardFormat("application/x-moz-file-promise-url")
    RegisterClipboardFormat("text/_moz_htmlinfo")
    RegisterClipboardFormat("application/x-moz-file-promise-dest-filename")
    RegisterClipboardFormat("text/_moz_htmlcontext")
    RegisterClipboardFormat("application/x-moz-nativeimage")
    RegisterClipboardFormat("text/x-moz-url")
    RegisterClipboardFormat("text/x-moz-url-data")
    RegisterClipboardFormat("text/x-moz-url-desc")
    RegisterClipboardFormat("text/uri-list")

    RegisterClipboardFormat("application/x-moz-nativeimage") は読み取り時にエラーが起きるので何が入っているのか確認することはできなかった。
    CF_UNICODETEXT / RegisterClipboardFormat("UniformResourceLocatorW") / RegisterClipboardFormat("application/x-moz-file-promise-url") / RegisterClipboardFormat("text/x-moz-url-data") / RegisterClipboardFormat("text/x-moz-url-desc") / RegisterClipboardFormat("text/uri-list") には大体画像の URL っぽいものが入っている。
    RegisterClipboardFormat("HTML Format") / RegisterClipboardFormat("text/html") は Chrome と同じデータ。
    RegisterClipboardFormat("text/_moz_htmlcontext") には HTML の断片。
    CF_DIB / CF_MAX / RegisterClipboardFormat("DragImageBits") はビットマップっぽいデータ。
    CF_HDROP は元データではなく、元データが BMP ファイルに変換されたもののパスが渡されてくるため、元データが欲しい場合にこれを優先的に処理してはいけない。
    未変換の画像データが欲しい場合はやはり Chrome と同じように RegisterClipboardFormat("FileContents") と RegisterClipboardFormat("FileGroupDescriptorW") を使うのが望ましい。
    ただし一つ注意点があって、Firefox では FORMATETC 構造体で TYMED_HGLOBAL を要求して GetData を呼んでも STGMEDIUM 構造体は TYMED_ISTREAM で返ってくる。そのため手抜きで戻ってきたものは全部 TYMED_HGLOBAL だと決め打ちして実装すると Firefox で上手く動かなくなると思われる。あと IStream::Clone は未実装らしい。
src に RFC 2397 Data URI scheme を使用した img タグの場合

最近だいぶ浸透しつつある data:image/png;base64,... みたいな奴。
  • Microsoft Internet Explorer 11.63.10586.0
    以下のフォーマットが利用できる。

    CF_TEXT
    CF_DIB
    CF_UNICODETEXT
    RegisterClipboardFormat("HTML Format")
    RegisterClipboardFormat("DragContext")
    RegisterClipboardFormat("msSourceUrl")
    RegisterClipboardFormat("DragImageBits")
    RegisterClipboardFormat("UntrustedDragDrop")

    URL を示す RegisterClipboardFormat("UniformResourceLocatorW") がない。
    色々なソフトで URL として受け付けることはこの時点で諦めそうな気配がする。
    ただし CF_TEXT / CF_UNICODETEXT には Data URI scheme 形式のテキストが入ってくるので、画像データを取得すること自体は普通に可能。
  • Microsoft Edge 25.10586.0.0 EdgeHTML 13.10586
    以下のフォーマットが利用できる。

    CF_TEXT
    CF_DIB
    CF_UNICODETEXT
    RegisterClipboardFormat("HTML Format")
    RegisterClipboardFormat("Preferred DropEffect")
    RegisterClipboardFormat("msSourceUrl")
    RegisterClipboardFormat("UntrustedDragDrop")
    RegisterClipboardFormat("msSourceTopLevelWindow")

    こちらも大体 IE と同じ。
  • Google Chrome 48.0.2564.103
    以下のフォーマットが利用できる。

    CF_TEXT
    CF_UNICODETEXT
    RegisterClipboardFormat("FileContents")
    RegisterClipboardFormat("FileGroupDescriptorW")
    RegisterClipboardFormat("HTML Format")
    RegisterClipboardFormat("UniformResourceLocator")
    RegisterClipboardFormat("DragContext")
    RegisterClipboardFormat("UniformResourceLocatorW")
    RegisterClipboardFormat("text/html")
    RegisterClipboardFormat("text/x-moz-url")
    RegisterClipboardFormat("DragImageBits")
    RegisterClipboardFormat("chromium/x-renderer-taint")

    Chrome は比較的普通のファイルと同じように扱えるよう配慮された挙動をする。
    画像データは RegisterClipboardFormat("FileContents") で普通に取り出せるし、RegisterClipboardFormat("FileGroupDescriptorW") には AQK0eAs+oHYKAAAAAElFTkSuQmCC.png のように Base64 エンコードされている画像データの最後の方に .png をつけたファイル名で格納されている。
    また、CF_TEXT / CF_UNICODETEXT / RegisterClipboardFormat("UniformResourceLocator") / RegisterClipboardFormat("UniformResourceLocatorW") / RegisterClipboardFormat("text/x-moz-url") などのように URL が格納されているものには Data URI scheme の画像データが格納されている。
  • Firefox 44.0.1
    以下のフォーマットが利用できる。

    CF_UNICODETEXT
    RegisterClipboardFormat("FileContents")
    RegisterClipboardFormat("FileGroupDescriptorW")
    RegisterClipboardFormat("HTML Format")
    RegisterClipboardFormat("DragContext")
    RegisterClipboardFormat("UniformResourceLocatorW")
    RegisterClipboardFormat("text/html")
    RegisterClipboardFormat("text/x-moz-url")
    RegisterClipboardFormat("text/x-moz-url-data")
    RegisterClipboardFormat("text/x-moz-url-desc")
    RegisterClipboardFormat("text/uri-list")
    RegisterClipboardFormat("text/_moz_htmlcontext")
    RegisterClipboardFormat("text/_moz_htmlinfo")
    RegisterClipboardFormat("DragImageBits")

    Firefox の RegisterClipboardFormat("FileContents") は画像ではなく、Data URI scheme へのインターネットショートカット(ファイル名は Untitled.URL)がデータ本体になっているため、これを抜き出してはいけない。
    CF_UNICODETEXT や URL が格納されている形式に関しては普通に Data URI scheme でデータが格納されているため、このデータをデコードすることで画像データを取得するのがいいと思われる。
結局どのようにデータを取得すべきか

ドロップ元のプログラムが何なのかを知らずに処理できなければいけないので、概ね以下の順序で取得するプログラムを書くのが取りあえずいいかなと思う。
なお TYMED_HGLOBAL でデータを要求しても TYMED_ISTREAM で返ってくることが実際にあったので、戻ってくる値が TYMED_HGLOBAL であることを想定して実装してはいけない。
  1. RegisterClipboardFormat("FileContents") があるなら読んでみて、GIF / JPEG / PNG / WEBP など対応可能な形式ならこれをデータとして受け付ける。形式の認識はファイルの先頭数バイトを見れば
    GIF: GIF87a または GIF89a
    JPEG: \xff\xd8\xff
    PNG: \x89PNG\x0d\x0a\x1a\x0a
    WEBP: RIFF????WEBP
    などなど、ある程度は判断できるはず。
    WebP は一般的な RIFF ヘッダなので ???? にはサイズ情報が入る。
    もしファイル名も必要な場合は RegisterClipboardFormat("FileGroupDescriptorW") を参照してファイル名を取得する。
    ブラウザ以外からもドロップを受け付けるつもりの場合は ANSI 版である RegisterClipboardFormat("FileGroupDescriptor") からのファイル名の取得も恐らく試みるべきだろう。
  2. CF_HDROP があるなら読んでみて、対応可能な形式ならこれをデータとして受け付ける。
    これに対応した時点で一般的な色々なソフトからのドロップにも対応できるようになる。
    なお、例えば 7zip で圧縮済みデータを閲覧中にファイルをドロップしようとした時など、DragEnter で CF_HDROP を渡してくるのにも関わらず、ファイルがまだ実在しないというパターンもある(DragEnter ではなく Drop の時にはファイルはある)。
    このような実装になっているのは大容量の圧縮ファイルのドラッグ時のレスポンスを良くするためだと思うが、実際お手本実装とも言える Windows 標準のエクスプローラはこの手のものでも正常に動作するので、対応する価値はあるように思う。
    この手のソフトから受け付けることも考慮する場合は拡張子からのファイルタイプ判断が必要になる。
  3. CF_UNICODETEXT または CF_TEXT があるなら読んでみて、内容が data: で始まる場合は Data URI scheme からのデータ抽出を試みる。Wikipedia にも少し書いてあるけど、仕様としては Base64 以外にパーセントエンコーディングが使用されている可能性がある。
    ただ画像データをエンコードする際、データの肥大化を考えるとまともなプログラムなら Base64 以外選択肢は実質的にはないと思えるので、Base64 のみ対応でもあまり問題ないかも知れない。
    これに対応すると副産物として当然ただのテキストの範囲選択ドロップでも Data URI scheme の形式なら画像として読み込めるようになる。
概ね上記の順番に従って実装したサンプルプログラム。
Lazarus 1.6RC2 で作ってある。

もう少しマイナーなエッジケースも存在するかもしれないが、知っている範囲では今のところ良好。
Clip to Evernote