LoginSignup
6
2

More than 3 years have passed since last update.

Unityでもクリップボードの画像を使いたい【Windows編】

Posted at

このように思って調べてみたのですが、2020年7月時点でUnityは、クリップボードの文字列しか扱うことができません。

Scripting API: GUIUtility.systemCopyBuffer - Unity

この機能自体は、ゲーム内でフレンドコードをクリップボードにコピーするために使われているそうです。
しかし、私のように「クリップボードの画像も使いたい!」という人が必ずどこかにいると思うので、今回この記事を投稿します。

成果物はこちらのリポジトリです。
https://github.com/umetaman/UnityNativeClipboard

また、一言で「画像」と言ってもたくさんの形式があるので、今回は各チャンネル8ビットで3チャンネルから4チャンネルの画像を対象にします。JPEGとPNG-32です。

環境

  • Unity2019.4.3f1(LTS)
  • Windows 10 Pro(2004)
  • Visual Studio 2019

はじめに

UnityのC#ではサポートされていない機能を使うために、NativePluginを使用します。

ネイティブプラグイン - Unity マニュアル

簡単に説明すると、

  • C, C++, Objective-Cで書かれたコードをC#から呼び出すことができる。
  • OSのAPIを呼び出すコードや、既存のC/C++のライブラリを再利用できる。

というものです。

C#でもOSのAPIを呼ぶことはできますが、構造体の定義やマーシャリングなど、いろいろと面倒くさいので、今回はC++でWin32APIを使ったメソッドを定義して、それをC#から呼び出すという方法で実装します。

大まかな流れ

  1. C++でWindowsのクリップボードから画像の情報を取り出す。
  2. C#で画像のビットマップを保存する領域を確保して、C++にアドレスを渡す。
  3. C++でC#から渡されたアドレスにビットマップをコピーする。
  4. C#でコピーされたビットマップをもとに、UnityのTexture2Dを作成する。

画像のデータをコピーするのにC++とC#の間を2往復していますが、後ほど述べるWindowsのBitmapの構造上、こうするしかありませんでした。

C++(NativePlugin)

まず、Visual StudioでDLL(ダイナミックリンクライブラリ)のプロジェクトを作成してください。
ウェブ上には様々な解説がありますが、最初はMicrosoftの解説を読んだほうがいいと思います。

チュートリアル: 独自のダイナミック リンク ライブラリを作成して使用する (C++)

GetClipboardData

Win32APIではいくつかの段階を踏むことで、クリップボードのデータを取り出すことができます。例えば、画像であれば、次の通りです。

  1. クリップボードを開く。
  2. 欲しいフォーマットのデータがあるか確認する。
  3. フォーマットを指定して、データのハンドルを受け取る。
  4. ハンドルを欲しいデータへのポインタとしてキャストする。
  5. コピーしたり、処理したりする。
  6. クリップボードを閉じる。

Windowsにおいて標準でサポートされているフォーマットについては、こちらを参照してください。

Clipboard Formats - Win32 apps | Microsoft Docs

今回は、DIB(Device Independent Bitmap, デバイスに依存しないビットマップ)が欲しかったので、CF_DIBを指定しています。

bool isOpened = OpenClipboard(NULL);

if (isOpened) {
    if (IsClipboardFormatAvailable(CF_DIB)) {
        HANDLE hClipboardData = GetClipboardData(CF_DIB);
        LPVOID clipboardDataPtr = GlobalLock(hClipboardData);

        BITMAPINFO* bitmapInfoPtr = static_cast<BITMAPINFO*>(clipboardDataPtr);
       // つづく
    }
}

BITMAPINFOというのは、ビットマップ画像について、様々な情報が詰まっている構造体です。ここから画像の大きさ、ビットの大きさ、ピクセルのデータを取り出します。

BITMAPINFO (wingdi.h) - Win32 apps | Microsoft Docs
BITMAPINFOHEADER (wingdi.h) - Win32 apps | Microsoft Docs

BITMAPINFO* bitmapInfoPtr = static_cast<BITMAPINFO*>(clipboardDataPtr);

int width = bitmapInfoPtr->bmiHeader.biWidth;
int height = bitmapInfoPtr->bmiHeader.biHeight;
int bitsPerPixel = bitmapInfoPtr->bmiHeader.biBitCount;

ビットマップの取り出し

それでは、いよいよ画像のピクセルを取り出していきます。いきなりですが、こちらの画像をご覧ください。WindowsにおけるBitmap画像ファイルの構造を示したものです。

Windows bitmap

引用:Windows bitmap - Wikipedia
(「Wikipediaかよ…」と思った人もいると思いますが、ぶっちゃけこれがいちばんわかりやすかったです。)

この画像を見ると、どうやらビットマップのヘッダーの直後に画像のピクセルが配置されているみたいなので、先ほど取り出したBITMAPINFOのbmiHeaderのポインタをシフトして、ピクセルデータまで移動します。

bitmap_dst.png

BITMAPINFOHEADERのポインタから、unsigned char型のポインタにキャストして、BITMAPINFOHEADERの大きさ分シフトします。
さらに、画像によっては画像に使う色を定義したカラーテーブルが含まれている場合もあるので、さらにその分シフトします。オプション的な機能らしいので、カラーテーブルをわざわざ持っているJPEGやPNGは少ないそうです。今回は、24ビットと32ビットのみの対応ですが、1, 4, 8ビットの画像には必ず入っているそうです。

参考:BMPファイルのフォーマット

// BITMAPINFOHEADERのbiSizeは、BITMAPINFOHEADERの大きさを示します。
unsigned char* pixelData = (unsigned char*)(bitmapInfoPtr)+bitmapInfoPtr->bmiHeader.biSize;

// カラーテーブルがあるときはその分シフトする
if (bitmapInfoPtr->bmiHeader.biCompression == BI_BITFIELDS) {
    pixelData += bitmapInfoPtr->bmiHeader.biClrUsed;
}

ようやく、ビットマップまでたどり着いたので、いよいよC#から渡されたバッファにコピーしていきます。そこで、またまたこちらの画像をご覧ください。ピクセルデータの部分を拡大したものです。
padding.png

Windows Bitmapのピクセルデータは、必ず1ライン(横幅, Width)が4バイトの倍数になるように確保されています。画像の大きさが足りない場合は、0で埋められます。
思わぬ落とし穴ですね。私はこれに気づくのに2日かかりました。

よって、コピーするときは、これを考慮して1ラインずつコピーすることにします。

int bytesPerPixel = bitmapInfoPtr->bmiHeader.biBitCount / 8;    // 1ピクセル当たりのバイト数
int bytesPerLine = width * bytePerPixel;    // 1ライン当たりのバイト数
bytesPerLine += bytePerLine % 4 == 0 ? 0 : 4 - bytesPerLine % 4;    // 4の倍数になるように増やしてあげる

unsigned char* dst = buffer;    // C#から渡されたバッファという想定
unsigned char* src = pixelData  // Windows Bitmapのピクセルの先頭のポインタ

// 1ラインずつコピーする
for (int h = 0; h < height; h++) {
    memcpy(
        dst + (width * h * bytesPerPixel),
        src + (h * bytesPerLine),
        width * bytesPerPixel
    );
}

コードは部分的にしか示しませんでしたが、これでC++側の実装は完了です。DLLをビルドしてUnityのプロジェクトにインポートしましょう。

C#(Unity)

NativePluginで定義したメソッドを静的クラスのメソッドとして定義します。DllImportAttributeでプラグインの名前を指定します。

DllImportAttribute クラス (System.Runtime.InteropServices) | Microsoft Docs

using System;
using System.Runtime.InteropServices;
using UnityEngine;

public static class NativePlugin
{
    // クリップボードに画像があるか
    [DllImport("Clipboard")]
    private static extern bool hasClipboardImage();
    // クリップボードの画像の大きさと、1ピクセル当たりのビット数
    [DllImport("Clipboard")]
    private static extern void getClipboardImageSize(ref int width, ref int height, ref int bitsPerPixel);
    // Bufferに画像のピクセルを書き込む
    [DllImport("Clipboard")]
    private static extern bool getClipboardImage(IntPtr buffer);
}

定義の仕方には、いろいろな流儀がありますが、私はNativePluginのメソッドはprivateにして、別にpublicなメソッドを定義して間接的に呼び出すようにしています。

バッファの確保

getClipboardImageSize(ref int width, ref int height, ref int bitsPerPixel)getClipboardImage(IntPtr buffer)というように、大きさの取得とデータの取得を分けているのは、C#側で事前に大きさを知ってバッファを確保するためです。
width, height, bitsPerPixelにref(参照渡しのキーワード)を付けているのは、NativePluginから一気に値を返してもらうためです。

int width = 0;
int height = 0;
int bitsPerPixel = 0;
getClipboardImageSize(ref width, ref height, ref bitsPerPixel);

得た値を元に、バッファを確保します。今回は1ピクセルが24ビットと32ビットで、それぞれ1チャンネルが8ビット(1バイト)なので、byte配列をバッファとします。

また、C#では、ガベージコレクションによってメモリのアドレスが変わってしまうことがあるため、ハンドルを割り当てて、位置を固定してあげないといけません。「NativePluginにコピーしてもらうまで、じっとしてなさい!」と命令してあげます。コピーが終わったら、ちゃんとFree()で解放してあげます。

GCHandle 構造体 (System.Runtime.InteropServices) | Microsoft Docs

// チャンネル数
int channel = bitsPerPixel / 8;

// C#側の領域を用意する
byte[] buffer = new byte[width * height * channel];

// GCによって移動しないように固定する。必ず開放する。
GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
// 確保したバッファのアドレス
IntPtr bufferPtr = handle.AddrOfPinnedObject();

// クリップボードからコピーする
bool successCopy = false;
successCopy = getClipboardImage(bufferPtr);

// 解放
handle.Free();

これでようやくC#側に持ってくることができました。

Texture2Dの作成

あとは、クリップボードから持ってきたピクセルの配列を使って、Texture2Dを作成しましょう。
UnityのTexture2Dでは、ピクセルの配列から画像を作成することを想定していたのか、LoadRawTextureDataという素敵なメソッドが用意されているので、それを使います。TextureFormat.BGRA32と指定すれば、そのまま流し込めます。残念ながらTextureFormat.BGR24という形式はUnityにはないようなので、Color32[]でアルファチャンネルの値を255で埋めてごまかします。

Texture2D-LoadRawTextureData - Unity スクリプトリファレンス

Texture2D texture = new Texture2D(width, height, TextureFormat.BGRA32, false);

// BGRA
if(channel == 4)
{
    texture.LoadRawTextureData(buffer);
}
// BGR
else if (channel == 3)
{
    Color32[] pixels = new Color32[width * height];
    for (int i = 0; i < pixels.Length; i++)
    {
        pixels[i].b = buffer[channel * i];
        pixels[i].g = buffer[channel * i + 1];
        pixels[i].r = buffer[channel * i + 2];
        pixels[i].a = (byte)255;
    }

    texture.SetPixels32(pixels);
}

texture.Apply();

これでTexture2Dに変換できたので、Unityのオブジェクトにセットしてみましょう!
次のGIFは、クリップボードの画像をuGUIのImageに反映したものです。

unity_clipboard_result.gif

突っ込まれそうなポイント

なぜ、CF_DIBなのか。

Device Independent Bitmapという名前に惹かれました。CF_BITMAPでも同様のことができると思います。

使いどころは?

この機能を必要としている人たちもいます。UnityはVJなんかにも使われているので、クリップボードの画像を使うのもいいのではないでしょうか。Visual Effect Graphにも持っていけます。

アセットストアで売れる?

今回はすべてのフォーマットの画像をサポートしきれないなと思ったので、コードを公開しました。バグの修正やサポート形式の追加をお待ちしています。

まとめ

今回は、Windowsのクリップボードに保存されている画像をUnityで使うために、NativePluginでWin32APIを呼び出して、画像をコピーするという方法を取りました。
OSの中に保持されているデータを読み取っているので、これを応用すれば、ウィンドウをキャプチャしてUnityで再生するみたいなこともできそうです。

記事中のコードは、説明のためにかなり内容を省略しているので、自分のプロジェクトに組み込みたい方は、GitHubのリポジトリを参照してください。
https://github.com/umetaman/UnityNativeClipboard

近々、macOS向けの記事も書く予定です。こちらもかなりハマるポイントがありました。お楽しみに~

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2