WIP: Add OSX support for Image copy/paste in the Image Editor osx-clipboard #112189

Draft
Jim-Snavely wants to merge 15 commits from Jim-Snavely/blender:osx-clipboard-111404 into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
3 changed files with 237 additions and 141 deletions

View File

@ -210,6 +210,27 @@ class GHOST_SystemCocoa : 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;
/**
* Handles a window event. Called by GHOST_WindowCocoa window delegate
* \param eventType: The type of window event.
@ -255,15 +276,15 @@ class GHOST_SystemCocoa : public GHOST_System {
/**
* Handles a mouse event.
* \param eventPtr: An #NSEvent pointer (cast to `void *` to enable compilation in standard C++).
* \return Indication whether the event was handled.
* \param eventPtr: An #NSEvent pointer (cast to `void *` to enable compilation in standard
* C++). \return Indication whether the event was handled.
*/
GHOST_TSuccess handleMouseEvent(void *eventPtr);
/**
* Handles a key event.
* \param eventPtr: An #NSEvent pointer (cast to `void *` to enable compilation in standard C++).
* \return Indication whether the event was handled.
* \param eventPtr: An #NSEvent pointer (cast to `void *` to enable compilation in standard
* C++). \return Indication whether the event was handled.
*/
GHOST_TSuccess handleKeyEvent(void *eventPtr);

View File

@ -72,6 +72,141 @@ static GHOST_TButton convertButton(int button)
}
}
/**
* Given an NSImage, returns an ImBuf in RGBA order.
* Caller must free
*/
static ImBuf *getImageBuffer(NSImage *droppedImg) {
NSSize imgSize = [droppedImg size];
ImBuf *ibuf = nullptr;
uint8_t *rasterRGB = nullptr;
uint8_t *rasterRGBA = nullptr;
uint8_t *toIBuf = nullptr;
int x, y, to_i, from_i;
NSBitmapImageRep *blBitmapFormatImageRGB, *blBitmapFormatImageRGBA, *bitmapImage = nil;
NSEnumerator *enumerator;
NSImageRep *representation;
ibuf = IMB_allocImBuf(imgSize.width, imgSize.height, 32, IB_rect);
if (!ibuf) {
[droppedImg release];
return nullptr;
}
/* Get the bitmap of the image. */
enumerator = [[droppedImg representations] objectEnumerator];
while ((representation = [enumerator nextObject])) {
if ([representation isKindOfClass:[NSBitmapImageRep class]]) {
bitmapImage = (NSBitmapImageRep *)representation;
break;
}
}
if (bitmapImage == nullptr) {
[droppedImg release];
return nullptr;
}
if (([bitmapImage bitsPerPixel] == 32) && (([bitmapImage bitmapFormat] & 0x5) == 0) &&
![bitmapImage isPlanar])
{
/* Try a fast copy if the image is a meshed RGBA 32bit bitmap. */
toIBuf = ibuf->byte_buffer.data;
rasterRGB = (uint8_t *)[bitmapImage bitmapData];
for (y = 0; y < imgSize.height; y++) {
to_i = (imgSize.height - y - 1) * imgSize.width;
from_i = y * imgSize.width;
memcpy(toIBuf + 4 * to_i, rasterRGB + 4 * from_i, 4 * imgSize.width);
}
[bitmapImage release];
} else {
/* Tell cocoa image resolution is same as current system one */
[bitmapImage setSize:imgSize];
/* Convert the image in a RGBA 32bit format */
/* As Core Graphics does not support contexts with non premutliplied alpha,
* we need to get alpha key values in a separate batch */
/* First get RGB values w/o Alpha to avoid pre-multiplication,
* 32bit but last byte is unused */
blBitmapFormatImageRGB = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:nullptr
pixelsWide:imgSize.width
pixelsHigh:imgSize.height
bitsPerSample:8
samplesPerPixel:3
hasAlpha:NO
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bitmapFormat:(NSBitmapFormat)0
bytesPerRow:4 * imgSize.width
bitsPerPixel:32 /* RGB format padded to 32bits. */];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext
setCurrentContext:[NSGraphicsContext
graphicsContextWithBitmapImageRep:blBitmapFormatImageRGB]];
[bitmapImage draw];
[NSGraphicsContext restoreGraphicsState];
rasterRGB = (uint8_t *)[blBitmapFormatImageRGB bitmapData];
if (rasterRGB == nullptr) {
[bitmapImage release];
[blBitmapFormatImageRGB release];
[droppedImg release];
return nullptr;
}
/* Then get Alpha values by getting the RGBA image (that is pre-multiplied BTW) */
blBitmapFormatImageRGBA = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:nullptr
pixelsWide:imgSize.width
pixelsHigh:imgSize.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bitmapFormat:(NSBitmapFormat)0
bytesPerRow:4 * imgSize.width
bitsPerPixel:32 /* RGBA */];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext
setCurrentContext:[NSGraphicsContext
graphicsContextWithBitmapImageRep:blBitmapFormatImageRGBA]];
[bitmapImage draw];
[NSGraphicsContext restoreGraphicsState];
rasterRGBA = (uint8_t *)[blBitmapFormatImageRGBA bitmapData];
if (rasterRGBA == nullptr) {
[bitmapImage release];
[blBitmapFormatImageRGB release];
[blBitmapFormatImageRGBA release];
[droppedImg release];
return nullptr;
}
/* Copy the image to ibuf, flipping it vertically. */
toIBuf = ibuf->byte_buffer.data;
for (y = 0; y < imgSize.height; y++) {
for (x = 0; x < imgSize.width; x++) {
to_i = (imgSize.height - y - 1) * imgSize.width + x;
from_i = y * imgSize.width + x;
toIBuf[4 * to_i] = rasterRGB[4 * from_i]; /* R */
toIBuf[4 * to_i + 1] = rasterRGB[4 * from_i + 1]; /* G */
toIBuf[4 * to_i + 2] = rasterRGB[4 * from_i + 2]; /* B */
toIBuf[4 * to_i + 3] = rasterRGBA[4 * from_i + 3]; /* A */
}
}
[blBitmapFormatImageRGB release];
[blBitmapFormatImageRGBA release];
[droppedImg release];
}
return ibuf;
}
/**
* Converts Mac raw-key codes (same for Cocoa & Carbon)
* into GHOST key codes
@ -916,15 +1051,14 @@ GHOST_TSuccess GHOST_SystemCocoa::getButtons(GHOST_Buttons &buttons) const
GHOST_TCapabilityFlag GHOST_SystemCocoa::getCapabilities() const
{
return GHOST_TCapabilityFlag(
GHOST_CAPABILITY_FLAG_ALL &
~(
/* Cocoa has no support for a primary selection clipboard. */
GHOST_kCapabilityPrimaryClipboard |
/* Cocoa has no support for sampling colors from the desktop. */
GHOST_kCapabilityDesktopSample |
/* This Cocoa back-end has not yet implemented image copy/paste. */
GHOST_kCapabilityClipboardImages));
return GHOST_TCapabilityFlag(GHOST_CAPABILITY_FLAG_ALL &
~(
/* OSX has no support for a primary selection clipboard. */
GHOST_kCapabilityPrimaryClipboard |
/* OSX has no support for a color sampling from the desktop. */
GHOST_kCapabilityDesktopSample ));
}
#pragma mark Event handlers
@ -1248,134 +1382,12 @@ GHOST_TSuccess GHOST_SystemCocoa::handleDraggingEvent(GHOST_TEventType eventType
case GHOST_kDragnDropTypeBitmap: {
NSImage *droppedImg = (NSImage *)data;
NSSize imgSize = [droppedImg size];
ImBuf *ibuf = nullptr;
uint8_t *rasterRGB = nullptr;
uint8_t *rasterRGBA = nullptr;
uint8_t *toIBuf = nullptr;
int x, y, to_i, from_i;
NSBitmapImageRep *blBitmapFormatImageRGB, *blBitmapFormatImageRGBA, *bitmapImage = nil;
NSEnumerator *enumerator;
NSImageRep *representation;
ibuf = IMB_allocImBuf(imgSize.width, imgSize.height, 32, IB_rect);
if (!ibuf) {
[droppedImg release];
return GHOST_kFailure;
}
/* Get the bitmap of the image. */
enumerator = [[droppedImg representations] objectEnumerator];
while ((representation = [enumerator nextObject])) {
if ([representation isKindOfClass:[NSBitmapImageRep class]]) {
bitmapImage = (NSBitmapImageRep *)representation;
break;
ImBuf *ibuf = getImageBuffer(droppedImg);
if (ibuf == nullptr) {
return GHOST_kFailure;
} else {
eventData = (GHOST_TEventDataPtr)ibuf;
}
}
if (bitmapImage == nil)
return GHOST_kFailure;
if (([bitmapImage bitsPerPixel] == 32) && (([bitmapImage bitmapFormat] & 0x5) == 0) &&
![bitmapImage isPlanar])
{
/* Try a fast copy if the image is a meshed RGBA 32bit bitmap. */
toIBuf = ibuf->byte_buffer.data;
rasterRGB = (uint8_t *)[bitmapImage bitmapData];
for (y = 0; y < imgSize.height; y++) {
to_i = (imgSize.height - y - 1) * imgSize.width;
from_i = y * imgSize.width;
memcpy(toIBuf + 4 * to_i, rasterRGB + 4 * from_i, 4 * imgSize.width);
}
}
else {
/* Tell cocoa image resolution is same as current system one */
[bitmapImage setSize:imgSize];
/* Convert the image in a RGBA 32bit format */
/* As Core Graphics does not support contexts with non premutliplied alpha,
* we need to get alpha key values in a separate batch */
/* First get RGB values w/o Alpha to avoid pre-multiplication,
* 32bit but last byte is unused */
blBitmapFormatImageRGB = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:nullptr
pixelsWide:imgSize.width
pixelsHigh:imgSize.height
bitsPerSample:8
samplesPerPixel:3
hasAlpha:NO
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bitmapFormat:(NSBitmapFormat)0
bytesPerRow:4 * imgSize.width
bitsPerPixel:32 /* RGB format padded to 32bits. */];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext
setCurrentContext:[NSGraphicsContext
graphicsContextWithBitmapImageRep:blBitmapFormatImageRGB]];
[bitmapImage draw];
[NSGraphicsContext restoreGraphicsState];
rasterRGB = (uint8_t *)[blBitmapFormatImageRGB bitmapData];
if (rasterRGB == nullptr) {
[bitmapImage release];
[blBitmapFormatImageRGB release];
[droppedImg release];
return GHOST_kFailure;
}
/* Then get Alpha values by getting the RGBA image (that is pre-multiplied BTW) */
blBitmapFormatImageRGBA = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:nullptr
pixelsWide:imgSize.width
pixelsHigh:imgSize.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bitmapFormat:(NSBitmapFormat)0
bytesPerRow:4 * imgSize.width
bitsPerPixel:32 /* RGBA */];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext
setCurrentContext:[NSGraphicsContext
graphicsContextWithBitmapImageRep:blBitmapFormatImageRGBA]];
[bitmapImage draw];
[NSGraphicsContext restoreGraphicsState];
rasterRGBA = (uint8_t *)[blBitmapFormatImageRGBA bitmapData];
if (rasterRGBA == nullptr) {
[bitmapImage release];
[blBitmapFormatImageRGB release];
[blBitmapFormatImageRGBA release];
[droppedImg release];
return GHOST_kFailure;
}
/* Copy the image to ibuf, flipping it vertically. */
toIBuf = ibuf->byte_buffer.data;
for (y = 0; y < imgSize.height; y++) {
for (x = 0; x < imgSize.width; x++) {
to_i = (imgSize.height - y - 1) * imgSize.width + x;
from_i = y * imgSize.width + x;
toIBuf[4 * to_i] = rasterRGB[4 * from_i]; /* R */
toIBuf[4 * to_i + 1] = rasterRGB[4 * from_i + 1]; /* G */
toIBuf[4 * to_i + 2] = rasterRGB[4 * from_i + 2]; /* B */
toIBuf[4 * to_i + 3] = rasterRGBA[4 * from_i + 3]; /* A */
}
}
[blBitmapFormatImageRGB release];
[blBitmapFormatImageRGBA release];
[droppedImg release];
}
eventData = (GHOST_TEventDataPtr)ibuf;
break;
}
default:
@ -1979,7 +1991,7 @@ char *GHOST_SystemCocoa::getClipboard(bool /*selection*/) const
if (textPasted == nil) {
return nullptr;
}
}
pastedTextSize = [textPasted lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
@ -2016,6 +2028,63 @@ void GHOST_SystemCocoa::putClipboard(const char *buffer, bool selection) const
}
}
GHOST_TSuccess GHOST_SystemCocoa::hasClipboardImage() const
{
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
if ([[pasteboard types] containsObject:NSPasteboardTypeTIFF] || [[pasteboard types] containsObject:NSPasteboardTypePNG]) {
return GHOST_kSuccess;
} else {
return GHOST_kFailure;
}
}
uint *GHOST_SystemCocoa::getClipboardImage(int *r_width, int *r_height) const
{
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
if ([[pasteboard types] containsObject:NSPasteboardTypeTIFF] || [[pasteboard types] containsObject:NSPasteboardTypePNG]) {
// Get the image
NSImage* image = [[NSImage alloc] initWithPasteboard:pasteboard];
NSSize size = [image size];
*r_width = size.width;
*r_height = size.height;
ImBuf *imageBuffer = getImageBuffer(image);
int msize = size.width * size.height * 4 ;
uint *pixels = (uint *)malloc(msize);
memcpy(pixels, imageBuffer->byte_buffer.data, msize);
IMB_freeImBuf(imageBuffer);
return pixels;
} else {
return nullptr;
}
}
GHOST_TSuccess GHOST_SystemCocoa::putClipboardImage(uint *rgba, int width, int height) const
{
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(rgba, width, height, 8, width * 4, colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGImageRef image = CGBitmapContextCreateImage(context);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSImage *nsImage = [[NSImage alloc] initWithCGImage:image size:NSZeroSize];
NSArray *copiedObjects = [NSArray arrayWithObject:nsImage];
BOOL copied = [pasteboard writeObjects:copiedObjects];
[nsImage release];
CGImageRelease(image);
if (copied) {
return GHOST_kSuccess;
} else {
return GHOST_kFailure;
}
}
GHOST_TSuccess GHOST_SystemCocoa::showMessageBox(const char *title,
const char *message,
const char *help_label,

View File

@ -216,6 +216,12 @@ class IMAGE_MT_image(Menu):
layout.operator("image.clipboard_paste", text="Paste")
layout.separator()
if sys.platform == "darwin":
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...")