SDL: Added support for clipboard data on Windows

From 443868143ce3d925f575c66e2218edcd8bc884d7 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 4 Jul 2023 11:09:43 -0700
Subject: [PATCH] Added support for clipboard data on Windows

The only supported image format is image/bmp
---
 src/video/windows/SDL_windowsclipboard.c | 228 ++++++++++++++++++++---
 src/video/windows/SDL_windowsclipboard.h |   3 +
 src/video/windows/SDL_windowsvideo.c     |   3 +
 3 files changed, 204 insertions(+), 30 deletions(-)

diff --git a/src/video/windows/SDL_windowsclipboard.c b/src/video/windows/SDL_windowsclipboard.c
index 7af109c44fa9..2ecb081f8207 100644
--- a/src/video/windows/SDL_windowsclipboard.c
+++ b/src/video/windows/SDL_windowsclipboard.c
@@ -31,17 +31,190 @@
 #else
 #define TEXT_FORMAT CF_TEXT
 #endif
+#define IMAGE_FORMAT CF_DIB
+#define IMAGE_MIME_TYPE "image/bmp"
 
-/* Get any application owned window handle for clipboard association */
-static HWND GetWindowHandle(SDL_VideoDevice *_this)
+/* Assume we can directly read and write BMP fields without byte swapping */
+SDL_COMPILE_TIME_ASSERT(verify_byte_order, SDL_BYTEORDER == SDL_LIL_ENDIAN);
+
+static const char bmp_magic[2] = { 'B', 'M' };
+
+static BOOL WIN_OpenClipboard(SDL_VideoDevice *_this)
+{
+    /* Retry to open the clipboard in case another application has it open */
+    const int MAX_ATTEMPTS = 3;
+    int attempt;
+    HWND hwnd = NULL;
+
+    if (_this->windows) {
+        hwnd = _this->windows->driverdata->hwnd;
+    }
+    for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
+        if (OpenClipboard(hwnd)) {
+            return TRUE;
+        }
+        SDL_Delay(10);
+    }
+    return FALSE;
+}
+
+static void WIN_CloseClipboard(void)
+{
+    CloseClipboard();
+}
+
+static HANDLE WIN_ConvertBMPtoDIB(const void *bmp, size_t bmp_size)
+{
+    HANDLE hMem = NULL;
+
+    if (bmp && bmp_size > sizeof(BITMAPFILEHEADER) && SDL_memcmp(bmp, bmp_magic, sizeof(bmp_magic)) == 0) {
+        BITMAPFILEHEADER *pbfh = (BITMAPFILEHEADER *)bmp;
+        BITMAPINFOHEADER *pbih = (BITMAPINFOHEADER *)((Uint8 *)bmp + sizeof(BITMAPFILEHEADER));
+        size_t bih_size = pbih->biSize + pbih->biClrUsed * sizeof(RGBQUAD);
+        size_t pixels_size = pbih->biSizeImage;
+
+        if (pbfh->bfOffBits >= (sizeof(BITMAPFILEHEADER) + bih_size) &&
+            (pbfh->bfOffBits + pixels_size) <= bmp_size) {
+            const Uint8 *pixels = (const Uint8 *)bmp + pbfh->bfOffBits;
+            size_t dib_size = bih_size + pixels_size;
+            hMem = GlobalAlloc(GMEM_MOVEABLE, dib_size);
+            if (hMem) {
+                LPVOID dst = GlobalLock(hMem);
+                if (dst) {
+                    SDL_memcpy(dst, pbih, bih_size);
+                    SDL_memcpy((Uint8 *)dst + bih_size, pixels, pixels_size);
+                    GlobalUnlock(hMem);
+                } else {
+                    WIN_SetError("GlobalLock()");
+                    GlobalFree(hMem);
+                    hMem = NULL;
+                }
+            } else {
+                SDL_OutOfMemory();
+            }
+        } else {
+            SDL_SetError("Invalid BMP data");
+        }
+    } else {
+        SDL_SetError("Invalid BMP data");
+    }
+    return hMem;
+}
+
+static void *WIN_ConvertDIBtoBMP(HANDLE hMem, size_t *size)
+{
+    void *bmp = NULL;
+    size_t mem_size = GlobalSize(hMem);
+
+    if (mem_size > sizeof(BITMAPINFOHEADER)) {
+        LPVOID dib = GlobalLock(hMem);
+        if (dib) {
+            BITMAPINFOHEADER *pbih = (BITMAPINFOHEADER *)dib;
+            size_t bih_size = pbih->biSize + pbih->biClrUsed * sizeof(RGBQUAD);
+            size_t dib_size = bih_size + pbih->biSizeImage;
+            if (dib_size <= mem_size) {
+                size_t bmp_size = sizeof(BITMAPFILEHEADER) + dib_size;
+                bmp = SDL_malloc(bmp_size);
+                if (bmp) {
+                    BITMAPFILEHEADER *pbfh = (BITMAPFILEHEADER *)bmp;
+                    pbfh->bfType = 0x4d42; /* bmp_magic */
+                    pbfh->bfSize = (DWORD)bmp_size;
+                    pbfh->bfReserved1 = 0;
+                    pbfh->bfReserved2 = 0;
+                    pbfh->bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + bih_size);
+                    SDL_memcpy((Uint8 *)bmp + sizeof(BITMAPFILEHEADER), dib, dib_size);
+                    *size = bmp_size;
+                } else {
+                    SDL_OutOfMemory();
+                }
+            } else {
+                SDL_SetError("Invalid BMP data");
+            }
+            GlobalUnlock(hMem);
+        } else {
+            WIN_SetError("GlobalLock()");
+        }
+    } else {
+        SDL_SetError("Invalid BMP data");
+    }
+    return bmp;
+}
+
+static int WIN_SetClipboardImage(SDL_VideoDevice *_this)
+{
+    SDL_VideoData *data = _this->driverdata;
+    int result = 0;
+
+    if (WIN_OpenClipboard(_this)) {
+        HANDLE hMem;
+        size_t clipboard_data_size;
+        const void *clipboard_data;
+
+        clipboard_data = _this->clipboard_callback(_this->clipboard_userdata, IMAGE_MIME_TYPE, &clipboard_data_size);
+        hMem = WIN_ConvertBMPtoDIB(clipboard_data, clipboard_data_size);
+        if (hMem) {
+            /* Save the image to the clipboard */
+            EmptyClipboard();
+            if (!SetClipboardData(IMAGE_FORMAT, hMem)) {
+                result = WIN_SetError("Couldn't set clipboard data");
+            }
+            data->clipboard_count = GetClipboardSequenceNumber();
+        } else {
+            /* WIN_ConvertBMPtoDIB() set the error */
+            result = -1;
+        }
+
+        WIN_CloseClipboard();
+    } else {
+        result = WIN_SetError("Couldn't open clipboard");
+    }
+    return result;
+}
+
+int WIN_SetClipboardData(SDL_VideoDevice *_this)
+{
+    size_t i;
+    int result = 0;
+
+    /* Right now only BMP data is supported for interchange with other applications */
+    for (i = 0; i < _this->num_clipboard_mime_types; ++i) {
+        if (SDL_strcmp(_this->clipboard_mime_types[i], IMAGE_MIME_TYPE) == 0) {
+            result = WIN_SetClipboardImage(_this);
+        }
+    }
+    return result;
+}
+
+void *WIN_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, size_t *size)
 {
-    SDL_Window *window;
+    void *data = NULL;
+
+    if (SDL_strcmp(mime_type, IMAGE_MIME_TYPE) == 0) {
+        if (IsClipboardFormatAvailable(IMAGE_FORMAT)) {
+            if (WIN_OpenClipboard(_this)) {
+                HANDLE hMem;
+
+                hMem = GetClipboardData(IMAGE_FORMAT);
+                if (hMem) {
+                    data = WIN_ConvertDIBtoBMP(hMem, size);
+                } else {
+                    WIN_SetError("Couldn't get clipboard data");
+                }
+                WIN_CloseClipboard();
+            }
+        }
+    }
+    return data;
+}
 
-    window = _this->windows;
-    if (window) {
-        return window->driverdata->hwnd;
+SDL_bool WIN_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type)
+{
+    if (SDL_strcmp(mime_type, IMAGE_MIME_TYPE) == 0) {
+        if (IsClipboardFormatAvailable(IMAGE_FORMAT)) {
+            return SDL_TRUE;
+        }
     }
-    return NULL;
+    return SDL_FALSE;
 }
 
 int WIN_SetClipboardText(SDL_VideoDevice *_this, const char *text)
@@ -49,7 +222,7 @@ int WIN_SetClipboardText(SDL_VideoDevice *_this, const char *text)
     SDL_VideoData *data = _this->driverdata;
     int result = 0;
 
-    if (OpenClipboard(GetWindowHandle(_this))) {
+    if (WIN_OpenClipboard(_this)) {
         HANDLE hMem;
         LPTSTR tstr;
         SIZE_T i, size;
@@ -90,10 +263,12 @@ int WIN_SetClipboardText(SDL_VideoDevice *_this, const char *text)
                 result = WIN_SetError("Couldn't set clipboard data");
             }
             data->clipboard_count = GetClipboardSequenceNumber();
+        } else {
+            result = SDL_OutOfMemory();
         }
         SDL_free(tstr);
 
-        CloseClipboard();
+        WIN_CloseClipboard();
     } else {
         result = WIN_SetError("Couldn't open clipboard");
     }
@@ -105,27 +280,23 @@ char *WIN_GetClipboardText(SDL_VideoDevice *_this)
     char *text = NULL;
 
     if (IsClipboardFormatAvailable(TEXT_FORMAT)) {
-        /* Retry to open the clipboard in case another application has it open */
-        const int MAX_ATTEMPTS = 3;
-        int attempt;
-
-        for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
-            if (OpenClipboard(GetWindowHandle(_this))) {
-                HANDLE hMem;
-                LPTSTR tstr;
+        if (WIN_OpenClipboard(_this)) {
+            HANDLE hMem;
+            LPTSTR tstr;
 
-                hMem = GetClipboardData(TEXT_FORMAT);
-                if (hMem) {
-                    tstr = (LPTSTR)GlobalLock(hMem);
+            hMem = GetClipboardData(TEXT_FORMAT);
+            if (hMem) {
+                tstr = (LPTSTR)GlobalLock(hMem);
+                if (tstr) {
                     text = WIN_StringToUTF8(tstr);
                     GlobalUnlock(hMem);
                 } else {
-                    WIN_SetError("Couldn't get clipboard data");
+                    WIN_SetError("Couldn't lock clipboard data");
                 }
-                CloseClipboard();
-                break;
+            } else {
+                WIN_SetError("Couldn't get clipboard data");
             }
-            SDL_Delay(10);
+            WIN_CloseClipboard();
         }
     }
     if (text == NULL) {
@@ -136,13 +307,10 @@ char *WIN_GetClipboardText(SDL_VideoDevice *_this)
 
 SDL_bool WIN_HasClipboardText(SDL_VideoDevice *_this)
 {
-    SDL_bool result = SDL_FALSE;
-    char *text = WIN_GetClipboardText(_this);
-    if (text) {
-        result = text[0] != '\0' ? SDL_TRUE : SDL_FALSE;
-        SDL_free(text);
+    if (IsClipboardFormatAvailable(TEXT_FORMAT)) {
+        return SDL_TRUE;
     }
-    return result;
+    return SDL_FALSE;
 }
 
 void WIN_CheckClipboardUpdate(struct SDL_VideoData *data)
diff --git a/src/video/windows/SDL_windowsclipboard.h b/src/video/windows/SDL_windowsclipboard.h
index fd1cc52290f7..beb068dae3e8 100644
--- a/src/video/windows/SDL_windowsclipboard.h
+++ b/src/video/windows/SDL_windowsclipboard.h
@@ -26,6 +26,9 @@
 /* Forward declaration */
 struct SDL_VideoData;
 
+int WIN_SetClipboardData(SDL_VideoDevice *_this);
+void *WIN_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, size_t *size);
+SDL_bool WIN_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type);
 extern int WIN_SetClipboardText(SDL_VideoDevice *_this, const char *text);
 extern char *WIN_GetClipboardText(SDL_VideoDevice *_this);
 extern SDL_bool WIN_HasClipboardText(SDL_VideoDevice *_this);
diff --git a/src/video/windows/SDL_windowsvideo.c b/src/video/windows/SDL_windowsvideo.c
index b1617385b6b2..d8669bdc10eb 100644
--- a/src/video/windows/SDL_windowsvideo.c
+++ b/src/video/windows/SDL_windowsvideo.c
@@ -257,6 +257,9 @@ static SDL_VideoDevice *WIN_CreateDevice(void)
     device->ClearComposition = WIN_ClearComposition;
     device->IsTextInputShown = WIN_IsTextInputShown;
 
+    device->SetClipboardData = WIN_SetClipboardData;
+    device->GetClipboardData = WIN_GetClipboardData;
+    device->HasClipboardData = WIN_HasClipboardData;
     device->SetClipboardText = WIN_SetClipboardText;
     device->GetClipboardText = WIN_GetClipboardText;
     device->HasClipboardText = WIN_HasClipboardText;