From 4be786a5f62b6831d3de709e40f8fda341805a6b Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sat, 15 Nov 2025 04:35:07 +0000 Subject: [PATCH] Support reading from and copying images to clipboard (#19930) * support reading images from clipboard(win32/android) * Add DataFormat.Image. Prioritize png format when retrieving image from clipboard * add browser support * addressed comments * win32 - add support for CF_DIB and CF_BITMAP formats * win32 - add support for copying bitmaps to clipboard as DIB * browser - add support for copying bitmap to clipboard * Implement bitmap clipboard for iOS * rename DataFormat.Image to DataFormat.Bitmap * Implement Bitmap clipboard on macOS * Use MemoryStream for bitmap clipboard/dnd on macOS backend * Add public.jpeg support on macOS backend (convert it to png while in objc) * Support TIFF format on iOS backend, by converting it to UIImage first * Add Bitmap DND sample * add clipboard bitmap support on linux * simply bitmap format search on win32 clipboard * fix linux clipboard image * Bump MACOSX_DEPLOYMENT_TARGET * Fix IDL incompatibility * address review * more reviews * Fix Android crash on copy when a bitmap is present in the clipboard * Handle any error that might occur pasting from an external content provider * Add data transfer extension methods for Bitmap --------- Co-authored-by: Max Katz Co-authored-by: Julien Lebosquain --- .../project.pbxproj | 2 + native/Avalonia.Native/src/OSX/clipboard.mm | 77 +++++- .../ControlCatalog/Pages/ClipboardPage.xaml | 6 + .../Pages/ClipboardPage.xaml.cs | 31 ++- .../ControlCatalog/Pages/DragAndDropPage.xaml | 6 +- .../Pages/DragAndDropPage.xaml.cs | 26 +- .../Platform/AndroidDataFormatHelper.cs | 7 + .../ClipDataItemToDataTransferItemWrapper.cs | 84 +++++-- .../Platform/ClipDataToDataTransferWrapper.cs | 14 +- .../Platform/Storage/AndroidStorageItem.cs | 6 +- .../Input/AsyncDataTransferExtensions.cs | 16 +- .../Input/AsyncDataTransferItemExtensions.cs | 14 +- src/Avalonia.Base/Input/DataFormat.cs | 7 + .../Input/DataTransferExtensions.cs | 16 +- src/Avalonia.Base/Input/DataTransferItem.cs | 11 + .../Input/DataTransferItemExtensions.cs | 16 +- .../Input/Platform/ClipboardExtensions.cs | 28 ++- .../ClipboardDataFormatHelper.cs | 5 + .../ClipboardDataTransferItem.cs | 11 + ...ansferItemToAvnClipboardDataItemWrapper.cs | 55 +++- src/Avalonia.Native/avn.idl | 2 +- .../Clipboard/ClipboardDataFormatHelper.cs | 33 ++- .../Clipboard/ClipboardDataReader.cs | 13 +- src/Avalonia.X11/Clipboard/X11Clipboard.cs | 35 ++- .../BrowserClipboardDataTransferItem.cs | 7 +- .../BrowserDataFormatHelper.cs | 4 + .../BrowserDataTransferHelper.cs | 26 +- src/Browser/Avalonia.Browser/ClipboardImpl.cs | 16 ++ .../Avalonia.Win32/ClipboardFormatRegistry.cs | 39 ++- .../Interop/UnmanagedMethods.cs | 26 +- .../Avalonia.Win32/OleDataObjectHelper.cs | 234 +++++++++++++++++- .../OleDataObjectToDataTransferWrapper.cs | 19 ++ .../Clipboard/ClipboardDataFormatHelper.cs | 9 + .../Avalonia.iOS/Clipboard/ClipboardImpl.cs | 13 + ...PasteboardItemToDataTransferItemWrapper.cs | 45 +++- 35 files changed, 880 insertions(+), 79 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 70bcfc500a..e82c150961 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -485,6 +485,7 @@ DYLIB_CURRENT_VERSION = 1; EXECUTABLE_PREFIX = lib; HEADER_SEARCH_PATHS = ../../inc; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -496,6 +497,7 @@ DYLIB_CURRENT_VERSION = 1; EXECUTABLE_PREFIX = lib; HEADER_SEARCH_PATHS = ../../inc; + MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 45770f8c1e..9786a64b27 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -27,12 +27,11 @@ public: if (changeCount != [_pasteboard changeCount]) return COR_E_OBJECTDISPOSED; - - auto types = [_pasteboard types]; - *ret = types == nil ? nullptr : CreateAvnStringArray(types); + + *ret = ConvertPasteboardTypes([_pasteboard types]); return S_OK; } - + virtual HRESULT GetItemCount(int64_t changeCount, int* ret) override { START_COM_ARP_CALL; @@ -57,13 +56,36 @@ public: if (changeCount != [_pasteboard changeCount]) return COR_E_OBJECTDISPOSED; - + auto item = [[_pasteboard pasteboardItems] objectAtIndex:index]; - auto types = [item types]; - *ret = types == nil ? nullptr : CreateAvnStringArray(types); + + *ret = ConvertPasteboardTypes([item types]); return S_OK; } - + + static IAvnStringArray* ConvertPasteboardTypes(NSArray *types) + { + if (types != nil) + { + NSMutableArray *mutableTypes = [types mutableCopy]; + + // Add png if format list doesn't have PNG, + // but has any other image type that can be converter into PNG + if (![mutableTypes containsObject:NSPasteboardTypePNG]) + { + if ([mutableTypes containsObject:NSPasteboardTypeTIFF] + || [mutableTypes containsObject:@"public.jpeg"]) + { + [mutableTypes addObject: NSPasteboardTypePNG]; + } + } + + return CreateAvnStringArray(mutableTypes); + } + + return nil; + } + virtual HRESULT GetItemValueAsString(int index, int64_t changeCount, const char* format, IAvnString** ret) override { START_COM_ARP_CALL; @@ -91,8 +113,45 @@ public: return COR_E_OBJECTDISPOSED; auto item = [[_pasteboard pasteboardItems] objectAtIndex:index]; - auto value = [item dataForType:[NSString stringWithUTF8String:format]]; + auto formatStr = [NSString stringWithUTF8String:format]; + auto value = [item dataForType: formatStr]; + + // If PNG wasn't found, try to convert TIFF or JPEG to PNG + if (value == nil && [formatStr isEqualToString: NSPasteboardTypePNG]) + { + NSData *imageData = nil; + + // Try TIFF first + imageData = [item dataForType:NSPasteboardTypeTIFF]; + + // If no TIFF, try JPEG + if (imageData == nil) { + imageData = [item dataForType:@"public.jpeg"]; + } + + if (imageData != nil) + { + auto image = [[NSImage alloc] initWithData:imageData]; + + NSBitmapImageRep *bitmapRep = nil; + for (NSImageRep *rep in image.representations) { + if ([rep isKindOfClass:[NSBitmapImageRep class]]) { + bitmapRep = (NSBitmapImageRep *)rep; + break; + } + } + + if (!bitmapRep) { + [image lockFocus]; + bitmapRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0, 0, image.size.width, image.size.height)]; + [image unlockFocus]; + } + + value = [bitmapRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; + } + } + *ret = value == nil || [value length] == 0 ? nullptr : CreateByteArray((void*)[value bytes], (int)[value length]); diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml b/samples/ControlCatalog/Pages/ClipboardPage.xaml index 2c22348f6f..80b3f4d1ed 100644 --- a/samples/ControlCatalog/Pages/ClipboardPage.xaml +++ b/samples/ControlCatalog/Pages/ClipboardPage.xaml @@ -6,6 +6,8 @@