Initial first version.
This commit is contained in:
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user