【Unity】Texture2Dとクリップボード

# TL;DR

- `Win32API`を使ってクリップボードとTexture2D間で画像データを読み書きする
    - クリップボード->`Texture2D`
    - `Texture2D`->クリップボード

# 動機

適当に遊んでる時に画面のキャプチャを画像ファイルではなくクリップボードに書き込みたかった(DiscordやVSCode等に貼る為)

# 実装

画像の読み込みについては[引用先](https://qiita.com/umetaman/items/03753c005554b8e810ca)にて詳細に説明されている為割愛します

```clipboard.cpp
#include <Windows.h>
#define DLL_API extern "C" __declspec(dllexport)

DLL_API bool setClipboardImage(unsigned char* data, int width, int height) {
	bool result = false;
	if (OpenClipboard(NULL)) {
		int size = width * height * 4;

		HGLOBAL hg = GlobalAlloc(GMEM_MOVEABLE, sizeof(BITMAPINFOHEADER) + size);
		BITMAPINFO* pInfo = (BITMAPINFO*)GlobalLock(hg);

		pInfo->bmiHeader.biSize = sizeof(BITMAPINFO);
		pInfo->bmiHeader.biWidth = width;
		pInfo->bmiHeader.biHeight = height;
		pInfo->bmiHeader.biPlanes = 1;
		pInfo->bmiHeader.biBitCount = 32;
		pInfo->bmiHeader.biCompression = BI_RGB;
		pInfo->bmiHeader.biSizeImage = width * height * 4;
		pInfo->bmiHeader.biXPelsPerMeter = 0;
		pInfo->bmiHeader.biYPelsPerMeter = 0;
		pInfo->bmiHeader.biClrUsed = 0;
		pInfo->bmiHeader.biClrImportant = 0;

		memcpy(pInfo->bmiColors, data, size);
		GlobalUnlock(hg);

		EmptyClipboard();
		SetClipboardData(CF_DIB, hg);
		result = true;
	}
	CloseClipboard();
	return result;
}
```

```Clipboard.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
//using UniHumanoid;
using UnityEngine;
using UnityEngine.UIElements;

public static class Clipboard {
    #region DLL
    [DllImport("libymtrplus")] private static extern bool hasClipboardImage();
    [DllImport("libymtrplus")] private static extern void getClipboardImageSize(ref int width, ref int height, ref int bitsPerPixel);
    [DllImport("libymtrplus")] private static extern bool getClipboardImage(IntPtr buffer);
    [DllImport("libymtrplus")] private static extern bool setClipboardImage(IntPtr data, int width, int height);
    #endregion

    public static bool GetClipboardImage(out Texture2D tex) {
        //  Get Info
        int width = 0, height = 0, bitsPerPixel = 0;
        getClipboardImageSize(ref width, ref height, ref bitsPerPixel);

        //  Check size
        if (width * height < 1) {
            throw new Exception("[Clipboard] Invalid image size");
        }
        int ch = bitsPerPixel / 8;
        if (ch != 3 && ch != 4) {
            throw new Exception("[Clipboard] Invalid image ch size");
        }

        //  Get Image
        byte[] buffer = new byte[width * height * ch];
        GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        IntPtr pBuffer = handle.AddrOfPinnedObject();
        bool success = getClipboardImage(pBuffer);
        handle.Free();

        //  Create texture
        if (success) {
            tex = new Texture2D(width, height, TextureFormat.BGRA32, false);
            if (ch == 4) {
                tex.LoadRawTextureData(buffer);
            } else if (ch == 3) {
                Color32[] cols = new Color32[width * height];
                for (int i = 0; i < cols.Length; i++) {
                    cols[i].b = buffer[ch * i + 0];
                    cols[i].g = buffer[ch * i + 1];
                    cols[i].r = buffer[ch * i + 2];
                    cols[i].a = 0xff;
                }
                tex.SetPixels32(cols);
                cols = null;
            }
            tex.Apply();
        } else {
            tex = null;
        }
        return success;
    }
    public static bool SetClipboardImage(Texture2D tex) {
        //  Prepare Data
        byte[] btex = tex.GetRawTextureData();
        for (int i = 0; i < btex.Length; i += 4) {
            //  RGBA -> BGRA
            byte tmp = btex[i + 0];
            btex[i + 0] = btex[i + 2];
            btex[i + 2] = tmp;
        }
        ScreenCapture.CaptureScreenshotAsTexture();
        //  Set Data
        IntPtr pData = Marshal.AllocHGlobal(btex.Length);
        Marshal.Copy(btex, 0, pData, btex.Length);
        return setClipboardImage(pData, tex.width, tex.height);
    }
}
```

```Capture.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.InputSystem;

public class TestCapture : MonoBehaviour {
    [DllImport("libymtrplus")] private static extern bool hasClipboardImage();
    [DllImport("libymtrplus")] private static extern void getClipboardImageSize(ref int width, ref int height, ref int bitsPerPixel);
    [DllImport("libymtrplus")] private static extern bool getClipboardImage(IntPtr buffer);
    [DllImport("libymtrplus")] private static extern bool setClipboardImage(IntPtr data, int width, int height);

    void Update() {
        if (Keyboard.current.pKey.wasPressedThisFrame) {
            StartCoroutine(UpdateInput());
        }
    }
    IEnumerator UpdateInput() {
        yield return new WaitForEndOfFrame();

        //  Capture
        var tex = new Texture2D(Screen.width, Screen.height);
        tex.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
        tex.Apply();

        //  Set clipboard
        Clipboard.SetClipboardImage(tex);
    }
}
```

# 実行結果

- Unity Editor
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500080/1bdef284-edb4-b44d-26ab-9411f9a155e7.png)

- 取得画像(クリップボード内容)
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500080/b4e53019-556a-fb38-7629-f2649526ec64.png)


# つまづいた点

## `BITMAPINFOHEADER`

`BITMAPINFOHEADER`の後ろに画像データの配列を置いてあげればよさそうだったので、`clipboard.cpp`にて下記のように初期化
```cpp
BITMAPINFOHEADER* pData = (BITMAPINFOHEADER*)GlobalLock(hg);
```
`hg`に確保しているサイズは同じなので問題ないかと思ったが左下に黒い画素が表示され、uvスクロールを行ったような画像となった

`BITMAPINFOHEADER*`→`BITMAPINFO*`にて解決

## `ScreenCapture.CaptureScreenshotAsTexture()`

```cs
Texture2D tex = ScreenCapture.CaptureScreenshotAsTexture();
```

Gameウィンドウの表示状態に依存しているようで、`Free Aspect`の際にはそれほど気にならないが余白部が画像データに含まれており、クロップされたような画像となる

詳細な原因は未調査

# Reference

- [Unityでもクリップボードの画像を使いたい【Windows編】 - Qiita](https://qiita.com/umetaman/items/03753c005554b8e810ca)
    - [umetaman/UnityNativeClipboard](https://github.com/umetaman/UnityNativeClipboard)
- [Unityで画面のスクリーンショットを撮る(Application.CaptureScreenshotじゃない方法) - Qiita](https://qiita.com/tempura/items/e8f4bbb4419407916d12)

- [GetClipboardData 関数 (winuser.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getclipboarddata)
- [SetClipboardData 関数 (winuser.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-setclipboarddata)