Added pasting and sending features

This commit is contained in:
Ebbe Baß
2026-08-13 15:43:34 +02:00
parent 940e80b630
commit ceadaff995
22 changed files with 771 additions and 55 deletions
@@ -3,6 +3,7 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Media.Imaging;
namespace EbbesMemeClipboard.Services;
@@ -10,8 +11,6 @@ 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();
@@ -23,18 +22,36 @@ public sealed class ClipboardService : IClipboardService
// 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));
}
// "PNG": a registered clipboard format (not one of the classic CF_* constants) that
// Chromium-based apps specifically look for and prefer over CF_DIB/CF_BITMAP when
// reading a pasted image, because those older formats are lossy for transparency. This
// covers Teams, Slack, Discord's web layer, and browsers - without it, paste into any of
// them can silently produce nothing even though CF_DIB is present and perfectly valid.
using var pngStream = new MemoryStream();
var pngEncoder = new PngBitmapEncoder();
pngEncoder.Frames.Add(BitmapFrame.Create(firstFrame));
pngEncoder.Save(pngStream);
pngStream.Position = 0;
data.SetData("PNG", pngStream);
// CF_HTML: the other route those same web-based paste targets check, and the only one of
// these formats that preserves animation for them, for GIFs.
var bytes = await File.ReadAllBytesAsync(filePath);
var fragment = $"<img src=\"data:{GetMimeType(filePath)};base64,{Convert.ToBase64String(bytes)}\">";
data.SetData(DataFormats.Html, BuildCfHtml(fragment));
await SetClipboardWithRetryAsync(data);
}
private static string GetMimeType(string filePath) =>
Path.GetExtension(filePath).ToLowerInvariant() switch
{
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
_ => "application/octet-stream",
};
private static async Task SetClipboardWithRetryAsync(DataObject data)
{
const int maxAttempts = 5;