From 8bc8933ba997e08b50e576834402a733742061fc Mon Sep 17 00:00:00 2001 From: Etienne Baudoux Date: Sat, 16 Sep 2023 09:50:59 -0700 Subject: [PATCH 1/3] Added image support to Clipboard on Windows --- src/Directory.Packages.props | 1 + src/app/dev/DevToys.Api/Core/IClipboard.cs | 12 +- src/app/dev/DevToys.Api/Core/PickedFile.cs | 2 +- src/app/dev/DevToys.Api/DevToys.Api.csproj | 3 +- .../Controls/MicaWindowWithOverlay.xaml.cs | 2 +- .../desktop/DevToys.Windows/Core/Clipboard.cs | 188 ++++++++++++++--- .../Core/Helpers/ImageHelper.cs | 195 ++++++++++++++++++ .../DevToys.Windows/DevToys.Windows.csproj | 1 + .../DevToys.Windows/MainWindow.xaml.cs | 2 +- 9 files changed, 368 insertions(+), 38 deletions(-) create mode 100644 src/app/dev/platforms/desktop/DevToys.Windows/Core/Helpers/ImageHelper.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 5d425ccec3..87e5edd8fa 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -27,6 +27,7 @@ + diff --git a/src/app/dev/DevToys.Api/Core/IClipboard.cs b/src/app/dev/DevToys.Api/Core/IClipboard.cs index ad2d804bd5..c82e9b6612 100644 --- a/src/app/dev/DevToys.Api/Core/IClipboard.cs +++ b/src/app/dev/DevToys.Api/Core/IClipboard.cs @@ -33,13 +33,13 @@ public interface IClipboard Task GetClipboardFilesAsync(); /// - /// Retrieves bitmap from the system clipboard. + /// Retrieves the image from the system clipboard. /// /// /// This method may try to access to the UI thread. /// - /// The bitmap currently stored in the system clipboard, or null if nothing is present. - Task GetClipboardBitmapAsync(); + /// The image currently stored in the system clipboard, or null if nothing is present. + Task?> GetClipboardImageAsync(); /// /// Sets text to the system clipboard. @@ -60,11 +60,11 @@ public interface IClipboard Task SetClipboardFilesAsync(FileInfo[]? filePaths); /// - /// Sets bitmap to the system clipboard. + /// Sets image to the system clipboard. /// /// /// This method may try to access to the UI thread. /// - /// The data to be stored in the system clipboard. - Task SetClipboardBitmapAsync(string? data); + /// The image to be stored in the system clipboard. + Task SetClipboardImageAsync(Image? image); } diff --git a/src/app/dev/DevToys.Api/Core/PickedFile.cs b/src/app/dev/DevToys.Api/Core/PickedFile.cs index 6b5225708f..e3dfa955e4 100644 --- a/src/app/dev/DevToys.Api/Core/PickedFile.cs +++ b/src/app/dev/DevToys.Api/Core/PickedFile.cs @@ -8,7 +8,7 @@ public record PickedFile : IDisposable /// public PickedFile(string fileName, Stream stream) { - FileName = fileName; + FileName = Path.GetFileName(fileName); Stream = stream; } diff --git a/src/app/dev/DevToys.Api/DevToys.Api.csproj b/src/app/dev/DevToys.Api/DevToys.Api.csproj index e892f153eb..0995b29e32 100644 --- a/src/app/dev/DevToys.Api/DevToys.Api.csproj +++ b/src/app/dev/DevToys.Api/DevToys.Api.csproj @@ -1,4 +1,4 @@ - + $(NetCore) @@ -19,6 +19,7 @@ + diff --git a/src/app/dev/platforms/desktop/DevToys.Windows/Controls/MicaWindowWithOverlay.xaml.cs b/src/app/dev/platforms/desktop/DevToys.Windows/Controls/MicaWindowWithOverlay.xaml.cs index b32cef2c09..98927d572f 100644 --- a/src/app/dev/platforms/desktop/DevToys.Windows/Controls/MicaWindowWithOverlay.xaml.cs +++ b/src/app/dev/platforms/desktop/DevToys.Windows/Controls/MicaWindowWithOverlay.xaml.cs @@ -342,7 +342,7 @@ private void ApplyResizeBorderThickness() MarginMaximized = WindowState == WindowState.Maximized ? new Thickness(6) : new Thickness(0); - if (WindowState == WindowState.Maximized || ResizeMode == ResizeMode.NoResize) + if (WindowState == WindowState.Maximized || ResizeMode == System.Windows.ResizeMode.NoResize) { WindowChrome.SetWindowChrome(this, new WindowChrome() { diff --git a/src/app/dev/platforms/desktop/DevToys.Windows/Core/Clipboard.cs b/src/app/dev/platforms/desktop/DevToys.Windows/Core/Clipboard.cs index 46116fe83b..caa2c493cf 100644 --- a/src/app/dev/platforms/desktop/DevToys.Windows/Core/Clipboard.cs +++ b/src/app/dev/platforms/desktop/DevToys.Windows/Core/Clipboard.cs @@ -1,9 +1,15 @@ using System.Collections.Specialized; using System.IO; +using System.Windows.Media.Imaging; using System.Windows.Threading; using DevToys.Api; +using DevToys.Windows.Core.Helpers; using DevToys.Windows.Helpers; using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp.Advanced; +using DataFormats = System.Windows.DataFormats; +using DataObject = System.Windows.DataObject; +using Image = System.Drawing.Image; namespace DevToys.Windows.Core; @@ -28,15 +34,7 @@ public Clipboard() { if (System.Windows.Clipboard.ContainsFileDropList()) { - var files = new List(); - foreach (string? filePath in System.Windows.Clipboard.GetFileDropList()) - { - if (!string.IsNullOrEmpty(filePath)) - { - files.Add(new FileInfo(filePath)); - } - } - return files.ToArray(); + return GetClipboardFilesInternal(); } else if (System.Windows.Clipboard.ContainsText()) { @@ -44,8 +42,7 @@ public Clipboard() } else if (System.Windows.Clipboard.ContainsImage()) { - // TODO: convert to a cross-platform compatible format? - return System.Windows.Clipboard.GetImage(); + return GetClipboardImageInternal(); } } catch (Exception ex) @@ -89,15 +86,7 @@ public Clipboard() { if (System.Windows.Clipboard.ContainsFileDropList()) { - var files = new List(); - foreach (string? filePath in System.Windows.Clipboard.GetFileDropList()) - { - if (!string.IsNullOrEmpty(filePath)) - { - files.Add(new FileInfo(filePath)); - } - } - return files.ToArray(); + return GetClipboardFilesInternal(); } } catch (Exception ex) @@ -109,16 +98,26 @@ public Clipboard() }); } - public Task GetClipboardBitmapAsync() + public Task?> GetClipboardImageAsync() { - // TODO - throw new NotImplementedException(); - } + return ThreadHelper.RunOnUIThreadAsync?>( + DispatcherPriority.Background, + () => + { + try + { + if (System.Windows.Clipboard.ContainsImage()) + { + return GetClipboardImageInternal(); + } + } + catch (Exception ex) + { + LogGetClipboardFailed(ex); + } - public Task SetClipboardBitmapAsync(string? data) - { - // TODO - throw new NotImplementedException(); + return null; + }); } public Task SetClipboardFilesAsync(FileInfo[]? data) @@ -163,9 +162,142 @@ public Task SetClipboardTextAsync(string? data) }); } + public async Task SetClipboardImageAsync(SixLabors.ImageSharp.Image? image) + { + try + { + if (image is not null) + { + using MemoryStream pngMemoryStream = ImageHelper.GetPngMemoryStreamFromImage(image); + using MemoryStream dibMemoryStream = ImageHelper.GetDeviceIndependentBitmapFromImage(pngMemoryStream, image.Width, image.Height); + using Image bmpImage = ImageHelper.GetBitmapFromImage(pngMemoryStream); + + await ThreadHelper.RunOnUIThreadAsync( + DispatcherPriority.Background, + () => + { + var data = new DataObject(); + + // As standard bitmap, without transparency support + data.SetData(DataFormats.Bitmap, bmpImage, autoConvert: true); + + // As PNG. + data.SetData("PNG", pngMemoryStream, autoConvert: false); + + // As DIB. This is (wrongly) accepted as ARGB by many applications. + data.SetData(DataFormats.Dib, dibMemoryStream, autoConvert: false); + + // The 'copy = true' argument means the MemoryStreams can be safely disposed after the operation. + System.Windows.Clipboard.SetDataObject(data, copy: true); + }); + } + } + catch (Exception ex) + { + LogSetClipboardTextFailed(ex); + } + } + [LoggerMessage(0, LogLevel.Warning, "Failed to retrieve the clipboard data.")] partial void LogGetClipboardFailed(Exception ex); [LoggerMessage(1, LogLevel.Error, "Failed to set the clipboard text.")] partial void LogSetClipboardTextFailed(Exception ex); + + private static FileInfo[] GetClipboardFilesInternal() + { + var files = new List(); + foreach (string? filePath in System.Windows.Clipboard.GetFileDropList()) + { + if (!string.IsNullOrEmpty(filePath)) + { + files.Add(new FileInfo(filePath)); + } + } + return files.ToArray(); + } + + private static Image? GetClipboardImageInternal() + { + System.Windows.IDataObject dataObject = System.Windows.Clipboard.GetDataObject(); + BitmapSource? bitmapSource = GetBitmapSourceFromDataObject(dataObject); + + if (bitmapSource is not null) + { + using var pngMemoryStream = new MemoryStream(); + + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(bitmapSource)); + encoder.Save(pngMemoryStream); + + pngMemoryStream.Seek(0, SeekOrigin.Begin); + + using var image = SixLabors.ImageSharp.Image.Load(pngMemoryStream); + return image.CloneAs(image.GetConfiguration()); + } + + return null; + } + + private static BitmapSource? GetBitmapSourceFromDataObject(System.Windows.IDataObject dataObject) + { + string[] formats = dataObject.GetFormats(true); + string firstFormat = formats[0]; + + // Guess at Chromium and Moz Web Browsers which can just use WPF's formatting + if (firstFormat == DataFormats.Bitmap || formats.Contains("text/_moz_htmlinfo")) + { + return System.Windows.Clipboard.GetImage(); + } + + // retrieve image first then convert to image source + // to avoid those image types that don't work with WPF GetImage() + using Bitmap? bitmap = GetBitmapFromDataObject(dataObject); + + // couldn't convert image + if (bitmap == null) + { + return null; + } + + return ImageHelper.BitmapToBitmapSource(bitmap); + } + + private static Bitmap? GetBitmapFromDataObject(System.Windows.IDataObject dataObject) + { + try + { + string[] formats = dataObject.GetFormats(true); + if (formats == null || formats.Length == 0) + { + return null; + } + + string firstFormat = formats[0]; + + if (formats.Contains("PNG")) + { + using var pngMemoryStream = (MemoryStream)dataObject.GetData("PNG"); + pngMemoryStream.Position = 0; + return new Bitmap(pngMemoryStream); + } + // Guess at Chromium and Moz Web Browsers which can just use WPF's formatting + else if (firstFormat == DataFormats.Bitmap || formats.Contains("text/_moz_htmlinfo")) + { + BitmapSource bitmapSource = System.Windows.Clipboard.GetImage(); + return ImageHelper.BitmapSourceToBitmap(bitmapSource); + } + else if (formats.Contains("System.Drawing.Bitmap")) // (first == DataFormats.Dib) + { + var bitmap = (Bitmap)dataObject.GetData("System.Drawing.Bitmap"); + return bitmap; + } + + return System.Windows.Forms.Clipboard.GetImage() as Bitmap; + } + catch + { + return null; + } + } } diff --git a/src/app/dev/platforms/desktop/DevToys.Windows/Core/Helpers/ImageHelper.cs b/src/app/dev/platforms/desktop/DevToys.Windows/Core/Helpers/ImageHelper.cs new file mode 100644 index 0000000000..d41e4fcede --- /dev/null +++ b/src/app/dev/platforms/desktop/DevToys.Windows/Core/Helpers/ImageHelper.cs @@ -0,0 +1,195 @@ +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media.Imaging; +using SixLabors.ImageSharp.Formats.Png; +using Rectangle = System.Drawing.Rectangle; + +namespace DevToys.Windows.Core.Helpers; + +/// +/// An helper class to help with images. +/// +internal static partial class ImageHelper +{ + internal static Bitmap? BitmapSourceToBitmap(BitmapSource source) + { + if (source == null) + { + return null; + } + + PixelFormat pixelFormat = PixelFormat.Format32bppArgb; //Bgr32 equiv default + if (source.Format == System.Windows.Media.PixelFormats.Bgr24) + { + pixelFormat = PixelFormat.Format24bppRgb; + } + else if (source.Format == System.Windows.Media.PixelFormats.Pbgra32) + { + pixelFormat = PixelFormat.Format32bppPArgb; + } + else if (source.Format == System.Windows.Media.PixelFormats.Prgba64) + { + pixelFormat = PixelFormat.Format64bppPArgb; + } + + var bmp + = new Bitmap( + source.PixelWidth, + source.PixelHeight, + pixelFormat); + + BitmapData data + = bmp.LockBits( + new Rectangle(System.Drawing.Point.Empty, bmp.Size), + ImageLockMode.WriteOnly, + pixelFormat); + + source.CopyPixels( + Int32Rect.Empty, + data.Scan0, + data.Height * data.Stride, + data.Stride); + + bmp.UnlockBits(data); + + return bmp; + } + + internal static BitmapSource BitmapToBitmapSource(Bitmap bmp) + { + nint hBitmap = bmp.GetHbitmap(); + BitmapSource imageSource + = Imaging.CreateBitmapSourceFromHBitmap( + hBitmap, + IntPtr.Zero, + Int32Rect.Empty, + BitmapSizeOptions.FromEmptyOptions()); + DeleteObject(hBitmap); + return imageSource; + } + + internal static MemoryStream GetPngMemoryStreamFromImage(SixLabors.ImageSharp.Image image) + { + Guard.IsNotNull(image); + + var encoder = new PngEncoder + { + ColorType = PngColorType.RgbWithAlpha, + TransparentColorMode = PngTransparentColorMode.Preserve, + BitDepth = PngBitDepth.Bit8, + CompressionLevel = PngCompressionLevel.BestSpeed + }; + + var pngMemoryStream = new MemoryStream(); + image.SaveAsPng(pngMemoryStream, encoder); + pngMemoryStream.Seek(0, SeekOrigin.Begin); + + return pngMemoryStream; + } + + internal static System.Drawing.Image GetBitmapFromImage(MemoryStream pngMemoryStream) + { + Guard.IsNotNull(pngMemoryStream); + return System.Drawing.Image.FromStream(pngMemoryStream); + } + + internal static MemoryStream GetDeviceIndependentBitmapFromImage(MemoryStream pngMemoryStream, int width, int height) + { + // Ensure image is 32bppARGB by painting it on a new 32bppARGB image. + byte[] bm32bData = GetArgb32Bitmap(pngMemoryStream, width, height); + + // BITMAPINFOHEADER struct for DIB. + int hdrSize = 0x28; + byte[] fullImage = new byte[hdrSize + 12 + bm32bData.Length]; + + //Int32 biSize; + WriteIntToByteArray(fullImage, 0x00, 4, true, (uint)hdrSize); + + //Int32 biWidth; + WriteIntToByteArray(fullImage, 0x04, 4, true, (uint)width); + + //Int32 biHeight; + WriteIntToByteArray(fullImage, 0x08, 4, true, (uint)height); + + //Int16 biPlanes; + WriteIntToByteArray(fullImage, 0x0C, 2, true, 1); + + //Int16 biBitCount; + WriteIntToByteArray(fullImage, 0x0E, 2, true, 32); + + //BITMAPCOMPRESSION biCompression = BITMAPCOMPRESSION.BITFIELDS; + WriteIntToByteArray(fullImage, 0x10, 4, true, 3); + + //Int32 biSizeImage; + WriteIntToByteArray(fullImage, 0x14, 4, true, (uint)bm32bData.Length); + + // The aforementioned "BITFIELDS": color masks applied to the Int32 pixel value to get the R, G and B values. + WriteIntToByteArray(fullImage, hdrSize + 0, 4, true, 0x00FF0000); + WriteIntToByteArray(fullImage, hdrSize + 4, 4, true, 0x0000FF00); + WriteIntToByteArray(fullImage, hdrSize + 8, 4, true, 0x000000FF); + Array.Copy(bm32bData, 0, fullImage, hdrSize + 12, bm32bData.Length); + + var dibMemoryStream = new MemoryStream(); + dibMemoryStream.Write(fullImage, 0, fullImage.Length); + return dibMemoryStream; + } + + private static byte[] GetArgb32Bitmap(MemoryStream pngMemoryStream, int width, int height) + { + Guard.IsNotNull(pngMemoryStream); + + using var argb32Bitmap = new Bitmap(width, height, PixelFormat.Format32bppPArgb); + + using (var graphic = Graphics.FromImage(argb32Bitmap)) + { + graphic.DrawImage( + System.Drawing.Image.FromStream(pngMemoryStream), + new Rectangle(0, 0, argb32Bitmap.Width, argb32Bitmap.Height)); + } + + // Bitmap format has its lines reversed. + argb32Bitmap.RotateFlip(RotateFlipType.Rotate180FlipX); + return GetBitmapData(argb32Bitmap, out int stride); + } + + private static byte[] GetBitmapData(Bitmap sourceImage, out int stride) + { + BitmapData sourceData + = sourceImage.LockBits( + new Rectangle( + 0, + 0, + sourceImage.Width, + sourceImage.Height), + ImageLockMode.ReadOnly, + sourceImage.PixelFormat); + + stride = sourceData.Stride; + byte[] data = new byte[stride * sourceImage.Height]; + Marshal.Copy(sourceData.Scan0, data, 0, data.Length); + sourceImage.UnlockBits(sourceData); + return data; + } + + private static void WriteIntToByteArray(byte[] data, int startIndex, int bytes, bool littleEndian, uint value) + { + int lastByte = bytes - 1; + if (data.Length < startIndex + bytes) + { + ThrowHelper.ThrowArgumentOutOfRangeException("startIndex", $"Data array is too small to write a {bytes}-byte value at offset {startIndex}."); + } + + for (int index = 0; index < bytes; index++) + { + int offs = startIndex + (littleEndian ? index : lastByte - index); + data[offs] = (byte)(value >> (8 * index) & 0xFF); + } + } + + [LibraryImport("gdi32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool DeleteObject(IntPtr hObject); +} diff --git a/src/app/dev/platforms/desktop/DevToys.Windows/DevToys.Windows.csproj b/src/app/dev/platforms/desktop/DevToys.Windows/DevToys.Windows.csproj index 914e94740a..29db9012ca 100644 --- a/src/app/dev/platforms/desktop/DevToys.Windows/DevToys.Windows.csproj +++ b/src/app/dev/platforms/desktop/DevToys.Windows/DevToys.Windows.csproj @@ -23,6 +23,7 @@ all + diff --git a/src/app/dev/platforms/desktop/DevToys.Windows/MainWindow.xaml.cs b/src/app/dev/platforms/desktop/DevToys.Windows/MainWindow.xaml.cs index 30f25af963..4af6a81706 100644 --- a/src/app/dev/platforms/desktop/DevToys.Windows/MainWindow.xaml.cs +++ b/src/app/dev/platforms/desktop/DevToys.Windows/MainWindow.xaml.cs @@ -110,7 +110,7 @@ private void MainWindow_Loaded(object sender, System.Windows.RoutedEventArgs e) private void BlazorWebView_BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e) { // Set the web view transparent. - blazorWebView.WebView.DefaultBackgroundColor = Color.Transparent; + blazorWebView.WebView.DefaultBackgroundColor = System.Drawing.Color.Transparent; } private void BlazorWebView_BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e) From f4bf747d7e8b614b65ef2afbd8e901fc35d94cb6 Mon Sep 17 00:00:00 2001 From: Etienne Baudoux Date: Sat, 16 Sep 2023 17:05:11 -0700 Subject: [PATCH 2/3] Added MacOS support --- .../desktop/DevToys.MacOS/Core/Clipboard.cs | 123 +++++++++++++----- .../DevToys.Windows/DevToys.Windows.csproj | 1 - 2 files changed, 90 insertions(+), 34 deletions(-) diff --git a/src/app/dev/platforms/desktop/DevToys.MacOS/Core/Clipboard.cs b/src/app/dev/platforms/desktop/DevToys.MacOS/Core/Clipboard.cs index c23128403b..72d54f9237 100644 --- a/src/app/dev/platforms/desktop/DevToys.MacOS/Core/Clipboard.cs +++ b/src/app/dev/platforms/desktop/DevToys.MacOS/Core/Clipboard.cs @@ -3,6 +3,11 @@ using Foundation; using Microsoft.Extensions.Logging; using UIKit; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Advanced; +using System.IO; +using SixLabors.ImageSharp.Formats.Png; namespace DevToys.MacOS.Core; @@ -23,26 +28,15 @@ public Clipboard() { if (UIPasteboard.General.HasUrls && UIPasteboard.General.Urls is not null) { - var files = new List(); - foreach (NSUrl filePath in UIPasteboard.General.Urls) - { - if (filePath.AbsoluteString is not null && filePath.Path is not null) - { - if (filePath.AbsoluteString.StartsWith("file:///")) - { - files.Add(new FileInfo(filePath.Path)); - } - } - } - return files.ToArray(); + return GetClipboardFilesInternal(); } - if (Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.Default.HasText) + else if (Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.Default.HasText) { return await Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.Default.GetTextAsync(); } - else + else if (UIPasteboard.General.HasImages) { - // TODO: On Mac, use AppKit API to get file and bitmap from clipboard. + return await GetImageFromClipboardInternalAsync(); } } catch (Exception ex) @@ -74,18 +68,7 @@ public Clipboard() { if (UIPasteboard.General.HasUrls && UIPasteboard.General.Urls is not null) { - var files = new List(); - foreach (NSUrl filePath in UIPasteboard.General.Urls) - { - if (filePath.AbsoluteString is not null && filePath.Path is not null) - { - if (filePath.AbsoluteString.StartsWith("file:///")) - { - files.Add(new FileInfo(filePath.Path)); - } - } - } - return Task.FromResult(files.ToArray()); + return Task.FromResult(GetClipboardFilesInternal()); } } catch (Exception ex) @@ -95,16 +78,49 @@ public Clipboard() return Task.FromResult(null); } - public Task GetClipboardBitmapAsync() + public async Task?> GetClipboardImageAsync() { - // TODO - throw new NotImplementedException(); + try + { + if (UIPasteboard.General.HasImages) + { + return await GetImageFromClipboardInternalAsync(); + } + } + catch (Exception ex) + { + LogGetClipboardFailed(ex); + } + + return null; } - public Task SetClipboardBitmapAsync(string? data) + public async Task SetClipboardImageAsync(SixLabors.ImageSharp.Image? image) { - // TODO - throw new NotImplementedException(); + if (image is not null) + { + var encoder = new PngEncoder + { + ColorType = PngColorType.RgbWithAlpha, + TransparentColorMode = PngTransparentColorMode.Preserve, + BitDepth = PngBitDepth.Bit8, + CompressionLevel = PngCompressionLevel.BestSpeed + }; + + var pngMemoryStream = new MemoryStream(); + await image.SaveAsPngAsync(pngMemoryStream, encoder); + pngMemoryStream.Seek(0, SeekOrigin.Begin); + + var data = NSData.FromStream(pngMemoryStream); + if (data is not null) + { + var macImage = UIImage.LoadFromData(data); + if (macImage is not null) + { + UIPasteboard.General.SetData(macImage.AsPNG(), "public.png"); + } + } + } } public Task SetClipboardFilesAsync(FileInfo[]? data) @@ -151,4 +167,45 @@ public async Task SetClipboardTextAsync(string? data) [LoggerMessage(1, LogLevel.Error, "Failed to set the clipboard text.")] partial void LogSetClipboardTextFailed(Exception ex); + + private static FileInfo[]? GetClipboardFilesInternal() + { + if (UIPasteboard.General.HasUrls && UIPasteboard.General.Urls is not null) + { + var files = new List(); + foreach (NSUrl filePath in UIPasteboard.General.Urls) + { + if (filePath.AbsoluteString is not null && filePath.Path is not null) + { + if (filePath.AbsoluteString.StartsWith("file:///")) + { + files.Add(new FileInfo(filePath.Path)); + } + } + } + return files.ToArray(); + } + + return null; + } + + private static async Task?> GetImageFromClipboardInternalAsync() + { + UIImage? imageFromPasteboard = UIPasteboard.General.Image; + + if (imageFromPasteboard is not null) + { + using Stream imageFromPasteboardStream = imageFromPasteboard.AsPNG().AsStream(); + using var pngMemoryStream = new MemoryStream(); + + await imageFromPasteboardStream.CopyToAsync(pngMemoryStream); + pngMemoryStream.Seek(0, SeekOrigin.Begin); + + using var image = SixLabors.ImageSharp.Image.Load(pngMemoryStream); + imageFromPasteboard.Dispose(); + return image.CloneAs(image.GetConfiguration()); + } + + return null; + } } diff --git a/src/app/dev/platforms/desktop/DevToys.Windows/DevToys.Windows.csproj b/src/app/dev/platforms/desktop/DevToys.Windows/DevToys.Windows.csproj index 29db9012ca..914e94740a 100644 --- a/src/app/dev/platforms/desktop/DevToys.Windows/DevToys.Windows.csproj +++ b/src/app/dev/platforms/desktop/DevToys.Windows/DevToys.Windows.csproj @@ -23,7 +23,6 @@ all - From d49dff89c7a501478a82ae460fefb1a96e96ff20 Mon Sep 17 00:00:00 2001 From: Etienne Baudoux Date: Sun, 24 Sep 2023 18:03:28 -0700 Subject: [PATCH 3/3] Added Linux support --- .../desktop/DevToys.Linux/Core/Clipboard.cs | 98 ++++++++++++++++--- 1 file changed, 87 insertions(+), 11 deletions(-) diff --git a/src/app/dev/platforms/desktop/DevToys.Linux/Core/Clipboard.cs b/src/app/dev/platforms/desktop/DevToys.Linux/Core/Clipboard.cs index d2a7170c4f..b50098a520 100644 --- a/src/app/dev/platforms/desktop/DevToys.Linux/Core/Clipboard.cs +++ b/src/app/dev/platforms/desktop/DevToys.Linux/Core/Clipboard.cs @@ -2,6 +2,10 @@ using DevToys.Api; using Gdk; using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Advanced; namespace DevToys.Linux.Core; @@ -9,17 +13,13 @@ namespace DevToys.Linux.Core; internal sealed partial class Clipboard : IClipboard { private readonly ILogger _logger; + private readonly IFileStorage _fileStorage; [ImportingConstructor] - public Clipboard() + public Clipboard(IFileStorage fileStorage) { _logger = this.Log(); - } - - public Task GetClipboardBitmapAsync() - { - // TODO - throw new NotImplementedException(); + _fileStorage = fileStorage; } public async Task GetClipboardDataAsync() @@ -38,7 +38,11 @@ public Clipboard() return text; } - // TODO: Get bitmap from clipboard. + Image? image = await GetClipboardImageAsync(); + if (image is not null) + { + return image; + } } catch (Exception ex) { @@ -114,10 +118,82 @@ public Clipboard() return null; } - public Task SetClipboardBitmapAsync(string? data) + public async Task?> GetClipboardImageAsync() { - // TODO - throw new NotImplementedException(); + try + { + Gdk.Clipboard clipboard = Gdk.Display.GetDefault()!.GetClipboard(); + var tcs = new TaskCompletionSource?>(); + + Gdk.Internal.Clipboard.ReadTextureAsync( + clipboard.Handle, + IntPtr.Zero, + new Gio.Internal.AsyncReadyCallbackAsyncHandler( + async (_, args, _) => + { + nint textureHandle = Gdk.Internal.Clipboard.ReadTextureFinish( + clipboard.Handle, + args.Handle, + out GLib.Internal.ErrorOwnedHandle error); + + Texture? texture + = GObject.Internal.ObjectWrapper.WrapNullableHandle( + textureHandle, + ownedRef: true); + + if (texture is not null) + { + string tempFile = Path.GetTempFileName(); + texture.SaveToPng(tempFile); + + using Image image = await SixLabors.ImageSharp.Image.LoadAsync(tempFile); + tcs.SetResult(image.CloneAs(image.GetConfiguration())); + + File.Delete(tempFile); + texture.Dispose(); + } + else + { + tcs.SetResult(null); + } + }).NativeCallback, IntPtr.Zero); + + return await tcs.Task; + } + catch (Exception ex) + { + LogGetClipboardFailed(ex); + } + + return null; + } + + public async Task SetClipboardImageAsync(SixLabors.ImageSharp.Image? image) + { + Gdk.Clipboard clipboard = Gdk.Display.GetDefault()!.GetClipboard(); + + if (image is not null) + { + var encoder = new PngEncoder + { + ColorType = PngColorType.RgbWithAlpha, + TransparentColorMode = PngTransparentColorMode.Preserve, + BitDepth = PngBitDepth.Bit8, + CompressionLevel = PngCompressionLevel.BestSpeed + }; + + var pngMemoryStream = new MemoryStream(); + await image.SaveAsPngAsync(pngMemoryStream, encoder); + pngMemoryStream.Seek(0, SeekOrigin.Begin); + + using var pngBytes = GLib.Bytes.New(pngMemoryStream.ToArray()); + using var texture = Texture.NewFromBytes(pngBytes); + clipboard.SetTexture(texture); + } + else + { + clipboard.SetText(string.Empty); + } } public Task SetClipboardFilesAsync(FileInfo[]? filePaths)