UI: Allow Clipboard Copy/Paste Images #105833

Merged
Harley Acheson merged 2 commits from Harley/blender:ClipboardImage into main 2023-04-14 03:48:28 +02:00
13 changed files with 567 additions and 0 deletions

View File

@ -911,6 +911,27 @@ extern char *GHOST_getClipboard(bool selection);
*/
extern void GHOST_putClipboard(const char *buffer, bool selection);
/**
* Returns GHOST_kSuccess if the clipboard contains an image.
*/
extern GHOST_TSuccess GHOST_hasClipboardImage(void);
/**
* Get image data from the Clipboard
* \param r_width: the returned image width in pixels.
* \param r_height: the returned image height in pixels.
* \return pointer uint array in RGBA byte order. Caller must free.
*/
extern uint *GHOST_getClipboardImage(int *r_width, int *r_height);
/**
* Put image data to the Clipboard
* \param rgba: uint array in RGBA byte order.
* \param width: the image width in pixels.
* \param height: the image height in pixels.
*/
extern GHOST_TSuccess GHOST_putClipboardImage(uint *rgba, int width, int height);
/**
* Set the Console State
* \param action: console state

View File

@ -471,6 +471,27 @@ class GHOST_ISystem {
*/
virtual void putClipboard(const char *buffer, bool selection) const = 0;
/**
* Returns GHOST_kSuccess if the clipboard contains an image.
*/
virtual GHOST_TSuccess hasClipboardImage(void) const = 0;
/**
* Get image data from the Clipboard
* \param r_width: the returned image width in pixels.
* \param r_height: the returned image height in pixels.
* \return pointer uint array in RGBA byte order. Caller must free.
*/
virtual uint *getClipboardImage(int *r_width, int *r_height) const = 0;
/**
* Put image data to the Clipboard
* \param rgba: uint array in RGBA byte order.
* \param width: the image width in pixels.
* \param height: the image height in pixels.
*/
virtual GHOST_TSuccess putClipboardImage(uint *rgba, int width, int height) const = 0;
/***************************************************************************************
* System Message Box.
***************************************************************************************/

View File

@ -883,6 +883,24 @@ void GHOST_putClipboard(const char *buffer, bool selection)
system->putClipboard(buffer, selection);
}
GHOST_TSuccess GHOST_hasClipboardImage(void)
{
GHOST_ISystem *system = GHOST_ISystem::getSystem();
return system->hasClipboardImage();
}
uint *GHOST_getClipboardImage(int *r_width, int *r_height)
{
GHOST_ISystem *system = GHOST_ISystem::getSystem();
return system->getClipboardImage(r_width, r_height);
}
GHOST_TSuccess GHOST_putClipboardImage(uint *rgba, int width, int height)
{
GHOST_ISystem *system = GHOST_ISystem::getSystem();
return system->putClipboardImage(rgba, width, height);
}
bool GHOST_setConsoleWindowState(GHOST_TConsoleWindowState action)
{
GHOST_ISystem *system = GHOST_ISystem::getSystem();

View File

@ -42,6 +42,23 @@ GHOST_System::~GHOST_System()
exit();
}
GHOST_TSuccess GHOST_System::hasClipboardImage(void) const
{
return GHOST_kFailure;
}
uint *GHOST_System::getClipboardImage(int * /*r_width*/, int * /*r_height*/) const
{
return nullptr;
}
GHOST_TSuccess GHOST_System::putClipboardImage(uint * /*rgba*/,
int /*width*/,
int /*height*/) const
{
return GHOST_kFailure;
}
uint64_t GHOST_System::getMilliSeconds() const
{
return std::chrono::duration_cast<std::chrono::milliseconds>(

View File

@ -330,6 +330,27 @@ class GHOST_System : public GHOST_ISystem {
*/
virtual void putClipboard(const char *buffer, bool selection) const = 0;
/**
* Returns GHOST_kSuccess if the clipboard contains an image.
*/
GHOST_TSuccess hasClipboardImage(void) const;
/**
* Get image data from the Clipboard
* \param r_width: the returned image width in pixels.
* \param r_height: the returned image height in pixels.
* \return pointer uint array in RGBA byte order. Caller must free.
*/
uint *getClipboardImage(int *r_width, int *r_height) const;
/**
* Put image data to the Clipboard
* \param rgba: uint array in RGBA byte order.
* \param width: the image width in pixels.
* \param height: the image height in pixels.
*/
GHOST_TSuccess putClipboardImage(uint *rgba, int width, int height) const;
/**
* Show a system message box
* \param title: The title of the message box.

View File

@ -26,6 +26,9 @@
#include "utf_winfunc.h"
#include "utfconv.h"
#include "IMB_imbuf.h"
#include "IMB_imbuf_types.h"
#include "GHOST_DisplayManagerWin32.hh"
#include "GHOST_EventButton.hh"
#include "GHOST_EventCursor.hh"
@ -2305,6 +2308,257 @@ void GHOST_SystemWin32::putClipboard(const char *buffer, bool selection) const
}
}
GHOST_TSuccess GHOST_SystemWin32::hasClipboardImage(void) const
{
if (IsClipboardFormatAvailable(CF_DIBV5) ||
IsClipboardFormatAvailable(RegisterClipboardFormat("PNG"))) {
return GHOST_kSuccess;
}
return GHOST_kFailure;
}
static uint *getClipboardImageDibV5(int *r_width, int *r_height)
{
HANDLE hGlobal = GetClipboardData(CF_DIBV5);
if (hGlobal == nullptr) {
return nullptr;
}
BITMAPV5HEADER *bitmapV5Header = (BITMAPV5HEADER *)GlobalLock(hGlobal);
if (bitmapV5Header == nullptr) {
return nullptr;
}
int offset = bitmapV5Header->bV5Size + bitmapV5Header->bV5ClrUsed * sizeof(RGBQUAD);
if (bitmapV5Header->bV5Compression == BI_BITFIELDS) {
Harley marked this conversation as resolved Outdated

picky use braces with all statements.

*picky* use braces with all statements.
offset += 12;
}
BYTE *buffer = (BYTE *)bitmapV5Header + offset;
int bitcount = bitmapV5Header->bV5BitCount;
int width = bitmapV5Header->bV5Width;
int height = bitmapV5Header->bV5Height;
*r_width = width;
*r_height = height;
DWORD ColorMasks[4];
ColorMasks[0] = bitmapV5Header->bV5RedMask ? bitmapV5Header->bV5RedMask : 0xff;
ColorMasks[1] = bitmapV5Header->bV5GreenMask ? bitmapV5Header->bV5GreenMask : 0xff00;
ColorMasks[2] = bitmapV5Header->bV5BlueMask ? bitmapV5Header->bV5BlueMask : 0xff0000;
ColorMasks[3] = bitmapV5Header->bV5AlphaMask ? bitmapV5Header->bV5AlphaMask : 0xff000000;
/* Bit shifts needed for the ColorMasks. */
DWORD ColorShifts[4];
for (int i = 0; i < 4; i++) {
_BitScanForward(&ColorShifts[i], ColorMasks[i]);
}
uchar *source = (uchar *)buffer;
uint *rgba = (uint *)malloc(width * height * 4);
uint8_t *target = (uint8_t *)rgba;
if (bitmapV5Header->bV5Compression == BI_BITFIELDS && bitcount == 32) {
for (int h = 0; h < height; h++) {
for (int w = 0; w < width; w++, target += 4, source += 4) {
DWORD *pix = (DWORD *)source;
target[0] = uint8_t((*pix & ColorMasks[0]) >> ColorShifts[0]);
target[1] = uint8_t((*pix & ColorMasks[1]) >> ColorShifts[1]);
target[2] = uint8_t((*pix & ColorMasks[2]) >> ColorShifts[2]);
target[3] = uint8_t((*pix & ColorMasks[3]) >> ColorShifts[3]);
}
}
}
else if (bitmapV5Header->bV5Compression == BI_RGB && bitcount == 32) {
for (int h = 0; h < height; h++) {
for (int w = 0; w < width; w++, target += 4, source += 4) {
RGBQUAD *quad = (RGBQUAD *)source;
target[0] = uint8_t(quad->rgbRed);
target[1] = uint8_t(quad->rgbGreen);
target[2] = uint8_t(quad->rgbBlue);
target[3] = (bitmapV5Header->bV5AlphaMask) ? uint8_t(quad->rgbReserved) : 255;
}
}
}
else if (bitmapV5Header->bV5Compression == BI_RGB && bitcount == 24) {
int bytes_per_row = ((((width * bitcount) + 31) & ~31) >> 3);
int slack = bytes_per_row - (width * 3);
for (int h = 0; h < height; h++, source += slack) {
for (int w = 0; w < width; w++, target += 4, source += 3) {
RGBTRIPLE *triple = (RGBTRIPLE *)source;
target[0] = uint8_t(triple->rgbtRed);
target[1] = uint8_t(triple->rgbtGreen);
target[2] = uint8_t(triple->rgbtBlue);
target[3] = 255;
}
}
}
GlobalUnlock(hGlobal);
return rgba;
}
/* Works with any image format that ImBuf can load. */
static uint *getClipboardImageImBuf(int *r_width, int *r_height, UINT format)
{
HANDLE hGlobal = GetClipboardData(format);
if (hGlobal == nullptr) {
return nullptr;
}
LPVOID pMem = GlobalLock(hGlobal);
if (!pMem) {
return nullptr;
}
uint *rgba = nullptr;
ImBuf *ibuf = IMB_ibImageFromMemory(
(uchar *)pMem, GlobalSize(hGlobal), IB_rect, nullptr, "<clipboard>");
if (ibuf) {
*r_width = ibuf->x;
*r_height = ibuf->y;
rgba = (uint *)malloc(4 * ibuf->x * ibuf->y);
memcpy(rgba, ibuf->rect, 4 * ibuf->x * ibuf->y);
IMB_freeImBuf(ibuf);
}
GlobalUnlock(hGlobal);
return rgba;
}
uint *GHOST_SystemWin32::getClipboardImage(int *r_width, int *r_height) const
{
if (!OpenClipboard(nullptr)) {
return nullptr;
}
/* Synthesized formats are placed after posted formats. */
UINT cfPNG = RegisterClipboardFormat("PNG");
UINT format = 0;
for (int cf = EnumClipboardFormats(0); cf; cf = EnumClipboardFormats(cf)) {
if (ELEM(cf, CF_DIBV5, cfPNG)) {
format = cf;
}
if (cf == CF_DIBV5 || (cf == CF_BITMAP && format == cfPNG)) {
break; /* Favor CF_DIBV5, but not if synthesized. */
}
}
uint *rgba = nullptr;
if (format == CF_DIBV5) {
rgba = getClipboardImageDibV5(r_width, r_height);
}
else if (format == cfPNG) {
rgba = getClipboardImageImBuf(r_width, r_height, cfPNG);
}
else {
*r_width = 0;
*r_height = 0;
}
CloseClipboard();
return rgba;
}
static bool putClipboardImageDibV5(uint *rgba, int width, int height)
{
DWORD size_pixels = width * height * 4;
/* Pixel data is 12 bytes after the header. */
HGLOBAL hMem = GlobalAlloc(GHND, sizeof(BITMAPV5HEADER) + 12 + size_pixels);
if (!hMem) {
return false;
}
BITMAPV5HEADER *hdr = (BITMAPV5HEADER *)GlobalLock(hMem);
if (!hdr) {
GlobalFree(hMem);
return false;
}
hdr->bV5Size = sizeof(BITMAPV5HEADER);
hdr->bV5Width = width;
hdr->bV5Height = height;
hdr->bV5Planes = 1;
hdr->bV5BitCount = 32;
hdr->bV5SizeImage = size_pixels;
hdr->bV5Compression = BI_BITFIELDS;
hdr->bV5RedMask = 0x000000ff;
hdr->bV5GreenMask = 0x0000ff00;
hdr->bV5BlueMask = 0x00ff0000;
hdr->bV5AlphaMask = 0xff000000;
hdr->bV5CSType = LCS_sRGB;
hdr->bV5Intent = LCS_GM_IMAGES;
hdr->bV5ClrUsed = 0;
memcpy((char *)hdr + sizeof(BITMAPV5HEADER) + 12, rgba, size_pixels);
GlobalUnlock(hMem);
if (!SetClipboardData(CF_DIBV5, hMem)) {
GlobalFree(hMem);
return false;
}
return true;
}
static bool putClipboardImagePNG(uint *rgba, int width, int height)
{
UINT cf = RegisterClipboardFormat("PNG");
/* Load buffer into ImBuf, convert to PNG. */
ImBuf *ibuf = IMB_allocFromBuffer(rgba, nullptr, width, height, 32);
ibuf->ftype = IMB_FTYPE_PNG;
ibuf->foptions.quality = 15;
if (!IMB_saveiff(ibuf, "<memory>", IB_rect | IB_mem)) {
IMB_freeImBuf(ibuf);
return false;
}
HGLOBAL hMem = GlobalAlloc(GHND, ibuf->encodedbuffersize);
if (!hMem) {
IMB_freeImBuf(ibuf);
return false;
}
LPVOID pMem = GlobalLock(hMem);
if (!pMem) {
IMB_freeImBuf(ibuf);
GlobalFree(hMem);
return false;
}
memcpy(pMem, ibuf->encodedbuffer, ibuf->encodedbuffersize);
GlobalUnlock(hMem);
IMB_freeImBuf(ibuf);
if (!SetClipboardData(cf, hMem)) {
GlobalFree(hMem);
return false;
}
return true;
}
GHOST_TSuccess GHOST_SystemWin32::putClipboardImage(uint *rgba, int width, int height) const
{
if (!OpenClipboard(nullptr) || !EmptyClipboard()) {
return GHOST_kFailure;
}
bool ok = putClipboardImageDibV5(rgba, width, height) &&
putClipboardImagePNG(rgba, width, height);
CloseClipboard();
return (ok) ? GHOST_kSuccess : GHOST_kFailure;
}
/* -------------------------------------------------------------------- */
/** \name Message Box
* \{ */

View File

@ -215,6 +215,27 @@ class GHOST_SystemWin32 : public GHOST_System {
*/
void putClipboard(const char *buffer, bool selection) const;
/**
* Returns GHOST_kSuccess if the clipboard contains an image.
*/
GHOST_TSuccess hasClipboardImage(void) const;
/**
* Get image data from the Clipboard
* \param r_width: the returned image width in pixels.
* \param r_height: the returned image height in pixels.
* \return pointer uint array in RGBA byte order. Caller must free.
*/
uint *getClipboardImage(int *r_width, int *r_height) const;
/**
* Put image data to the Clipboard
* \param rgba: uint array in RGBA byte order.
* \param width: the image width in pixels.
* \param height: the image height in pixels.
*/
GHOST_TSuccess putClipboardImage(uint *rgba, int width, int height) const;
/**
* Show a system message box
* \param title: The title of the message box.

View File

@ -184,6 +184,8 @@ class IMAGE_MT_image(Menu):
bl_label = "Image"
def draw(self, context):
import sys
layout = self.layout
sima = context.space_data
@ -207,6 +209,11 @@ class IMAGE_MT_image(Menu):
layout.separator()
Harley marked this conversation as resolved Outdated

As this only works on windows, better only show these items on windows.

As this only works on windows, better only show these items on windows.
if sys.platform[:3] == "win":
layout.operator("image.clipboard_copy", text="Copy")
layout.operator("image.clipboard_paste", text="Paste")
layout.separator()
if ima:
layout.operator("image.save", text="Save", icon='FILE_TICK')
layout.operator("image.save_as", text="Save As...")

View File

@ -62,6 +62,8 @@ void IMAGE_OT_save_sequence(struct wmOperatorType *ot);
void IMAGE_OT_save_all_modified(struct wmOperatorType *ot);
void IMAGE_OT_pack(struct wmOperatorType *ot);
void IMAGE_OT_unpack(struct wmOperatorType *ot);
void IMAGE_OT_clipboard_copy(struct wmOperatorType *ot);
void IMAGE_OT_clipboard_paste(struct wmOperatorType *ot);
void IMAGE_OT_flip(struct wmOperatorType *ot);
void IMAGE_OT_invert(struct wmOperatorType *ot);

View File

@ -71,6 +71,7 @@
#include "ED_screen.h"
#include "ED_space_api.h"
#include "ED_util.h"
#include "ED_undo.h"
#include "ED_util_imbuf.h"
#include "ED_uvedit.h"
@ -2834,6 +2835,125 @@ void IMAGE_OT_flip(wmOperatorType *ot)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Clipboard Copy Operator
* \{ */
static int image_clipboard_copy_exec(bContext *C, wmOperator *op)
{
Image *ima = image_from_context(C);
if (ima == NULL) {
return false;
}
if (G.is_rendering && ima->source == IMA_SRC_VIEWER) {
BKE_report(op->reports, RPT_ERROR, "Images cannot be copied while rendering");
return false;
Harley marked this conversation as resolved Outdated

Image buffers can be float precision with rect_float, in which case rect will be null. Also, the byte buffer may have various colorspaces as defined by rect_colorspace, while this probably assumes sRGB.

Image buffers can be float precision with `rect_float`, in which case `rect` will be null. Also, the byte buffer may have various colorspaces as defined by `rect_colorspace`, while this probably assumes sRGB.
}
ImageUser *iuser = image_user_from_context(C);
WM_cursor_set(CTX_wm_window(C), WM_CURSOR_WAIT);
void *lock;
ImBuf *ibuf = BKE_image_acquire_ibuf(ima, iuser, &lock);
if (ibuf == NULL) {
BKE_image_release_ibuf(ima, ibuf, lock);
WM_cursor_set(CTX_wm_window(C), WM_CURSOR_DEFAULT);
return OPERATOR_CANCELLED;
}
WM_clipboard_image_set(ibuf);
BKE_image_release_ibuf(ima, ibuf, lock);
WM_cursor_set(CTX_wm_window(C), WM_CURSOR_DEFAULT);
return OPERATOR_FINISHED;
}
static bool image_clipboard_copy_poll(bContext *C)
{
if (!image_from_context_has_data_poll(C)) {
Harley marked this conversation as resolved Outdated

This check makes more sense to add into the exec function, reporting an error on failure. This way when pressing Ctrl-C shortcut while rendering - an error will show in the status bar. Also, if the user opens a menu just before render completes, the menu-item won't be outdated.

This check makes more sense to add into the exec function, reporting an error on failure. This way when pressing Ctrl-C shortcut while rendering - an error will show in the status bar. Also, if the user opens a menu just before render completes, the menu-item won't be outdated.

This check makes more sense to add into the exec function, reporting an error on failure. This way when pressing Ctrl-C shortcut while rendering - an error will show in the status bar. Also, if the user opens a menu just before render completes, the menu-item won't be outdated.

This check makes more sense to add into the exec function, reporting an error on failure. This way when pressing Ctrl-C shortcut while rendering - an error will show in the status bar. Also, if the user opens a menu just before render completes, the menu-item won't be outdated.
CTX_wm_operator_poll_msg_set(C, "No images available");
return false;
}
return true;
}
void IMAGE_OT_clipboard_copy(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Copy Image";
ot->idname = "IMAGE_OT_clipboard_copy";
ot->description = "Copy the image to the clipboard";
/* api callbacks */
ot->exec = image_clipboard_copy_exec;
ot->poll = image_clipboard_copy_poll;
/* flags */
ot->flag = OPTYPE_REGISTER;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Clipboard Paste Operator
* \{ */
static int image_clipboard_paste_exec(bContext *C, wmOperator *op)
{
WM_cursor_set(CTX_wm_window(C), WM_CURSOR_WAIT);
ImBuf *ibuf = WM_clipboard_image_get();
if (!ibuf) {
WM_cursor_set(CTX_wm_window(C), WM_CURSOR_DEFAULT);
return OPERATOR_CANCELLED;
}
ED_undo_push_op(C, op);
Main *bmain = CTX_data_main(C);
SpaceImage *sima = CTX_wm_space_image(C);
Image *ima = BKE_image_add_from_imbuf(bmain, ibuf, "Clipboard");
IMB_freeImBuf(ibuf);
ED_space_image_set(bmain, sima, ima, false);
BKE_image_signal(bmain, ima, (sima) ? &sima->iuser : NULL, IMA_SIGNAL_USER_NEW_IMAGE);
WM_event_add_notifier(C, NC_IMAGE | NA_ADDED, ima);
WM_cursor_set(CTX_wm_window(C), WM_CURSOR_DEFAULT);
return OPERATOR_FINISHED;
}
static bool image_clipboard_paste_poll(bContext *C)
{
if (!WM_clipboard_image_available()) {
CTX_wm_operator_poll_msg_set(C, "No compatible images are on the clipboard");
return false;
}
return true;
}
void IMAGE_OT_clipboard_paste(wmOperatorType *ot)
{
/* identifiers */
ot->name = "Paste Image";
ot->idname = "IMAGE_OT_clipboard_paste";
ot->description = "Paste new image from the clipboard";
/* api callbacks */
ot->exec = image_clipboard_paste_exec;
ot->poll = image_clipboard_paste_poll;
/* flags */
ot->flag = OPTYPE_REGISTER;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Invert Operators
* \{ */

View File

@ -216,6 +216,8 @@ static void image_operatortypes(void)
WM_operatortype_append(IMAGE_OT_save_all_modified);
WM_operatortype_append(IMAGE_OT_pack);
WM_operatortype_append(IMAGE_OT_unpack);
WM_operatortype_append(IMAGE_OT_clipboard_copy);
WM_operatortype_append(IMAGE_OT_clipboard_paste);
WM_operatortype_append(IMAGE_OT_flip);
WM_operatortype_append(IMAGE_OT_invert);

View File

@ -1572,6 +1572,27 @@ char *WM_clipboard_text_get(bool selection, int *r_len);
char *WM_clipboard_text_get_firstline(bool selection, int *r_len);
void WM_clipboard_text_set(const char *buf, bool selection);
/**
* Returns true if the clipboard contains an image.
*/
bool WM_clipboard_image_available(void);
/**
* Get image data from the Clipboard
* \param r_width: the returned image width in pixels.
* \param r_height: the returned image height in pixels.
* \return pointer uint array in RGBA byte order. Caller must free.
*/
struct ImBuf *WM_clipboard_image_get(void);
/**
* Put image data to the Clipboard
* \param rgba: uint array in RGBA byte order.
* \param width: the image width in pixels.
* \param height: the image height in pixels.
*/
bool WM_clipboard_image_set(struct ImBuf *ibuf);
/* progress */
void WM_progress_set(struct wmWindow *win, float progress);

View File

@ -60,6 +60,9 @@
#include "ED_scene.h"
#include "ED_screen.h"
#include "IMB_imbuf.h"
#include "IMB_imbuf_types.h"
#include "UI_interface.h"
#include "UI_interface_icons.h"
@ -2062,6 +2065,45 @@ void WM_clipboard_text_set(const char *buf, bool selection)
}
}
bool WM_clipboard_image_available(void)
{
return (bool)GHOST_hasClipboardImage();
}
ImBuf *WM_clipboard_image_get(void)
{
uint width, height;
uint *rgba = GHOST_getClipboardImage(&width, &height);
if (!rgba) {
return NULL;
}
ImBuf *ibuf = IMB_allocFromBuffer(rgba, NULL, width, height, 4);
free(rgba);
return ibuf;
}
bool WM_clipboard_image_set(ImBuf *ibuf)
{
bool free_byte_buffer = false;
Harley marked this conversation as resolved Outdated

Since this function doesn't know anything else about the ibuf, I think it would be best not to keep the byte-buffer (if it's created).

This can call imb_freerectImBuf(..); in the case there was previously no byte-buffer.

Since this function doesn't know anything else about the `ibuf`, I think it would be best not to keep the byte-buffer (if it's created). This can call `imb_freerectImBuf(..);` in the case there was previously no byte-buffer.
if (ibuf->rect == NULL) {
/* Add a byte buffer if it does not have one. */
IMB_rect_from_float(ibuf);
free_byte_buffer = true;
}
bool success = (bool)GHOST_putClipboardImage(ibuf->rect, ibuf->x, ibuf->y);
if (free_byte_buffer) {
/* Remove the byte buffer if we added it. */
imb_freerectImBuf(ibuf);
}
return success;
}
/** \} */
/* -------------------------------------------------------------------- */