Initial first version.

This commit is contained in:
Ebbe Baß
2026-08-13 10:04:59 +02:00
parent df725b43c4
commit 940e80b630
22 changed files with 890 additions and 0 deletions
@@ -0,0 +1,82 @@
using System.Collections.Specialized;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
namespace EbbesMemeClipboard.Services;
public sealed class ClipboardService : IClipboardService
{
public async Task CopyLocalFileAsync(string filePath)
{
bool isGif = ImageDecoding.IsGif(filePath);
var firstFrame = await Task.Run(() => ImageDecoding.DecodeFirstFrame(filePath));
var data = new DataObject();
// CF_HDROP: lets Discord/Slack/Teams/Explorer paste the real file (animated, for GIFs).
data.SetFileDropList(new StringCollection { filePath });
// CF_DIB fallback: apps that only accept bitmap paste (e.g. Paint) get a static image.
// WPF's DataObject.SetImage auto-converts to CF_DIB - no manual header work needed here.
data.SetImage(firstFrame);
if (isGif)
{
// CF_HTML: the format most browser/rich-text paste targets use, and the only one of
// the three that preserves animation for those targets (CF_DIB is a single frame).
var bytes = await File.ReadAllBytesAsync(filePath);
var fragment = $"<img src=\"data:image/gif;base64,{Convert.ToBase64String(bytes)}\">";
data.SetData(DataFormats.Html, BuildCfHtml(fragment));
}
await SetClipboardWithRetryAsync(data);
}
private static async Task SetClipboardWithRetryAsync(DataObject data)
{
const int maxAttempts = 5;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
Clipboard.SetDataObject(data, copy: true);
return;
}
catch (COMException) when (attempt < maxAttempts)
{
// Another process is transiently holding the clipboard - back off and retry.
await Task.Delay(75);
}
}
}
/// <summary>
/// Builds a CF_HTML clipboard payload. WPF's DataObject.SetData(DataFormats.Html, ...) does
/// NOT wrap plain HTML into CF_HTML automatically - the Version/StartHTML/EndHTML/
/// StartFragment/EndFragment header with exact byte offsets has to be built by hand, or
/// paste targets will reject or mis-render the fragment.
/// </summary>
private static string BuildCfHtml(string htmlFragment)
{
const string prefix = "<html><body><!--StartFragment-->";
const string suffix = "<!--EndFragment--></body></html>";
int headerLength = Encoding.UTF8.GetByteCount(FormatHeader(0, 0, 0, 0));
int startHtml = headerLength;
int startFragment = startHtml + Encoding.UTF8.GetByteCount(prefix);
int endFragment = startFragment + Encoding.UTF8.GetByteCount(htmlFragment);
int endHtml = endFragment + Encoding.UTF8.GetByteCount(suffix);
return FormatHeader(startHtml, endHtml, startFragment, endFragment) + prefix + htmlFragment + suffix;
}
private static string FormatHeader(int startHtml, int endHtml, int startFragment, int endFragment) =>
"Version:0.9\r\n" +
$"StartHTML:{startHtml:D10}\r\n" +
$"EndHTML:{endHtml:D10}\r\n" +
$"StartFragment:{startFragment:D10}\r\n" +
$"EndFragment:{endFragment:D10}\r\n";
}