104 lines
3.6 KiB
C#
104 lines
3.6 KiB
C#
using System.Globalization;
|
|
using System.Drawing;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Windows.Forms;
|
|
using DataObject = System.Windows.Forms.DataObject;
|
|
using DataFormats = System.Windows.Forms.DataFormats;
|
|
using FormsClipboard = System.Windows.Forms.Clipboard;
|
|
|
|
namespace Olive.Services;
|
|
|
|
internal sealed class ClipboardService
|
|
{
|
|
public static async Task CopyGifBytesAsync(byte[] gifBytes, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(gifBytes);
|
|
if (gifBytes.Length == 0) throw new InvalidOperationException("GIF data is empty.");
|
|
|
|
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var thread = new Thread(() => CopyOnStaThread(gifBytes, completion))
|
|
{
|
|
Name = "Olive clipboard STA"
|
|
};
|
|
|
|
thread.SetApartmentState(ApartmentState.STA);
|
|
thread.Start();
|
|
|
|
cancellationToken.Register(() => completion.TrySetCanceled(cancellationToken));
|
|
await completion.Task.ConfigureAwait(false);
|
|
}
|
|
|
|
private static void CopyOnStaThread(byte[] gifBytes, TaskCompletionSource completion)
|
|
{
|
|
try
|
|
{
|
|
var clipboardHtml = BuildClipboardHtml(gifBytes);
|
|
|
|
const int retryCount = 5;
|
|
for (var attempt = 1; attempt <= retryCount; attempt++)
|
|
try
|
|
{
|
|
var data = new DataObject();
|
|
data.SetData("image/gif", false, GifStream(gifBytes));
|
|
data.SetData("GIF", false, GifStream(gifBytes));
|
|
data.SetData(DataFormats.Html, false, clipboardHtml);
|
|
data.SetData("text/html", false, clipboardHtml);
|
|
TryAddBitmapPreview(data, gifBytes);
|
|
FormsClipboard.SetDataObject(data, true, 10, 100);
|
|
completion.TrySetResult();
|
|
return;
|
|
}
|
|
catch (ExternalException) when (attempt < retryCount)
|
|
{
|
|
Thread.Sleep(120);
|
|
}
|
|
|
|
completion.TrySetException(new InvalidOperationException("The clipboard is temporarily unavailable."));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
completion.TrySetException(ex);
|
|
}
|
|
}
|
|
|
|
private static MemoryStream GifStream(byte[] gifBytes)
|
|
{
|
|
return new MemoryStream(gifBytes, false);
|
|
}
|
|
|
|
private static void TryAddBitmapPreview(DataObject data, byte[] gifBytes)
|
|
{
|
|
try
|
|
{
|
|
using var stream = new MemoryStream(gifBytes, false);
|
|
using var image = Image.FromStream(stream);
|
|
data.SetData(DataFormats.Bitmap, true, new Bitmap(image));
|
|
}
|
|
catch (Exception ex) when (ex is ArgumentException or ExternalException or OutOfMemoryException)
|
|
{
|
|
}
|
|
}
|
|
|
|
private static string BuildClipboardHtml(byte[] gifBytes)
|
|
{
|
|
var dataUri = "data:image/gif;base64," + Convert.ToBase64String(gifBytes);
|
|
var fragment = $"<img src=\"{dataUri}\" alt=\"GIF\">";
|
|
const string markerPrefix =
|
|
"Version:1.0\r\nStartHTML:{0:0000000000}\r\nEndHTML:{1:0000000000}\r\nStartFragment:{2:0000000000}\r\nEndFragment:{3:0000000000}\r\n";
|
|
var prefix = string.Format(CultureInfo.InvariantCulture, markerPrefix, 0, 0, 0, 0);
|
|
const string beforeFragment = "<!DOCTYPE html><html><body><!--StartFragment-->";
|
|
const string afterFragment = "<!--EndFragment--></body></html>";
|
|
var html = beforeFragment + fragment + afterFragment;
|
|
|
|
var startHtml = Encoding.UTF8.GetByteCount(prefix);
|
|
var startFragment = startHtml + Encoding.UTF8.GetByteCount(beforeFragment);
|
|
var endFragment = startFragment + Encoding.UTF8.GetByteCount(fragment);
|
|
var endHtml = startHtml + Encoding.UTF8.GetByteCount(html);
|
|
|
|
var header = string.Format(CultureInfo.InvariantCulture, markerPrefix, startHtml, endHtml, startFragment,
|
|
endFragment);
|
|
return header + html;
|
|
}
|
|
}
|