Files
Olive/Olive/Services/ClipboardService.cs
T
2026-07-30 20:19:01 +02:00

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;
}
}