Skip to content

Commit

Permalink
Added image support to clipboard API. (#946)
Browse files Browse the repository at this point in the history
* Added image support to Clipboard on Windows

* Added MacOS support

* Added Linux support
  • Loading branch information
veler authored Sep 25, 2023
1 parent 1378eb2 commit b4a912a
Show file tree
Hide file tree
Showing 10 changed files with 544 additions and 82 deletions.
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NuGet.Packaging" Version="6.6.1" />
<PackageVersion Include="Nuke.Common" Version="7.0.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.0.2" />
<PackageVersion Include="System.Text.Json" Version="$(DotNetVersion)" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.ComponentModel.Composition" Version="$(DotNetVersion)" />
Expand Down
12 changes: 6 additions & 6 deletions src/app/dev/DevToys.Api/Core/IClipboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ public interface IClipboard
Task<FileInfo[]?> GetClipboardFilesAsync();

/// <summary>
/// Retrieves bitmap from the system clipboard.
/// Retrieves the image from the system clipboard.
/// </summary>
/// <remarks>
/// This method may try to access to the UI thread.
/// </remarks>
/// <returns>The bitmap currently stored in the system clipboard, or null if nothing is present.</returns>
Task<string?> GetClipboardBitmapAsync();
/// <returns>The image currently stored in the system clipboard, or null if nothing is present.</returns>
Task<Image<Rgba32>?> GetClipboardImageAsync();

/// <summary>
/// Sets text to the system clipboard.
Expand All @@ -60,11 +60,11 @@ public interface IClipboard
Task SetClipboardFilesAsync(FileInfo[]? filePaths);

/// <summary>
/// Sets bitmap to the system clipboard.
/// Sets image to the system clipboard.
/// </summary>
/// <remarks>
/// This method may try to access to the UI thread.
/// </remarks>
/// <param name="data">The data to be stored in the system clipboard.</param>
Task SetClipboardBitmapAsync(string? data);
/// <param name="image">The image to be stored in the system clipboard.</param>
Task SetClipboardImageAsync(Image? image);
}
2 changes: 1 addition & 1 deletion src/app/dev/DevToys.Api/Core/PickedFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public record PickedFile : IDisposable
/// </summary>
public PickedFile(string fileName, Stream stream)
{
FileName = fileName;
FileName = Path.GetFileName(fileName);
Stream = stream;
}

Expand Down
3 changes: 2 additions & 1 deletion src/app/dev/DevToys.Api/DevToys.Api.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetCore)</TargetFrameworks>

Expand All @@ -19,6 +19,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="System.ComponentModel.Composition" />
<PackageReference Include="System.Threading.Tasks.Extensions" />
<PackageReference Include="SixLabors.ImageSharp"/>
</ItemGroup>

<ItemGroup>
Expand Down
98 changes: 87 additions & 11 deletions src/app/dev/platforms/desktop/DevToys.Linux/Core/Clipboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@
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;

[Export(typeof(IClipboard))]
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<string?> GetClipboardBitmapAsync()
{
// TODO
throw new NotImplementedException();
_fileStorage = fileStorage;
}

public async Task<object?> GetClipboardDataAsync()
Expand All @@ -38,7 +38,11 @@ public Clipboard()
return text;
}

// TODO: Get bitmap from clipboard.
Image<Rgba32>? image = await GetClipboardImageAsync();
if (image is not null)
{
return image;
}
}
catch (Exception ex)
{
Expand Down Expand Up @@ -114,10 +118,82 @@ public Clipboard()
return null;
}

public Task SetClipboardBitmapAsync(string? data)
public async Task<Image<Rgba32>?> GetClipboardImageAsync()
{
// TODO
throw new NotImplementedException();
try
{
Gdk.Clipboard clipboard = Gdk.Display.GetDefault()!.GetClipboard();
var tcs = new TaskCompletionSource<Image<Rgba32>?>();

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<Texture>(
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<Rgba32>(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)
Expand Down
123 changes: 90 additions & 33 deletions src/app/dev/platforms/desktop/DevToys.MacOS/Core/Clipboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -23,26 +28,15 @@ public Clipboard()
{
if (UIPasteboard.General.HasUrls && UIPasteboard.General.Urls is not null)
{
var files = new List<FileInfo>();
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)
Expand Down Expand Up @@ -74,18 +68,7 @@ public Clipboard()
{
if (UIPasteboard.General.HasUrls && UIPasteboard.General.Urls is not null)
{
var files = new List<FileInfo>();
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<FileInfo[]?>(files.ToArray());
return Task.FromResult<FileInfo[]?>(GetClipboardFilesInternal());
}
}
catch (Exception ex)
Expand All @@ -95,16 +78,49 @@ public Clipboard()
return Task.FromResult<FileInfo[]?>(null);
}

public Task<string?> GetClipboardBitmapAsync()
public async Task<Image<Rgba32>?> 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)
Expand Down Expand Up @@ -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<FileInfo>();
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<Image<Rgba32>?> 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<Rgba32>(image.GetConfiguration());
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading

0 comments on commit b4a912a

Please sign in to comment.