Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added image support to clipboard API. #946

Merged
merged 4 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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