Added pasting new memes, fixed offsets and changed out tray icon.

This commit is contained in:
Ebbe Baß
2026-08-19 10:15:16 +02:00
parent 4c755919d4
commit b6644ebecf
11 changed files with 226 additions and 9 deletions
@@ -2,12 +2,14 @@ using System.Collections.Specialized;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Media.Imaging;
using EbbesMemeClipboard.Models;
namespace EbbesMemeClipboard.Services;
public sealed class ClipboardService : IClipboardService
public sealed partial class ClipboardService : IClipboardService
{
public async Task CopyLocalFileAsync(string filePath)
{
@@ -60,6 +62,86 @@ public sealed class ClipboardService : IClipboardService
_ => "application/octet-stream",
};
private static readonly string[] ImportableExtensions = { ".jpg", ".jpeg", ".png", ".gif" };
/// <summary>
/// Formats are checked in descending order of fidelity, which matters a lot here: a GIF is
/// only still animated in the first two. Falling straight to the bitmap (as a naive
/// implementation would) silently flattens every pasted GIF to one frame.
/// </summary>
public ClipboardImportPayload? TryReadImage()
{
// 1. Real files (copied in Explorer) - byte-for-byte exact, animation intact.
if (Clipboard.ContainsFileDropList())
{
var paths = Clipboard.GetFileDropList()
.Cast<string>()
.Where(p => !string.IsNullOrWhiteSpace(p) &&
ImportableExtensions.Contains(Path.GetExtension(p).ToLowerInvariant()))
.ToList();
if (paths.Count > 0)
{
return new ClipboardImportPayload { FilePaths = paths };
}
}
// 2. CF_HTML with an embedded data: URI - how a real (possibly animated) GIF arrives
// when copied out of a browser or out of this app itself.
if (Clipboard.ContainsText(TextDataFormat.Html))
{
var match = DataUriRegex().Match(Clipboard.GetText(TextDataFormat.Html));
if (match.Success)
{
try
{
var bytes = Convert.FromBase64String(match.Groups["data"].Value);
var ext = match.Groups["mime"].Value.ToLowerInvariant() switch
{
"gif" => ".gif",
"jpeg" or "jpg" => ".jpg",
_ => ".png",
};
return new ClipboardImportPayload { Data = bytes, Extension = ext };
}
catch (FormatException)
{
// Malformed base64 - fall through to the bitmap formats below.
}
}
}
// 3. The registered "PNG" format - lossless and keeps transparency, unlike CF_DIB.
if (Clipboard.ContainsData("PNG") && Clipboard.GetData("PNG") is { } pngData)
{
byte[]? bytes = pngData switch
{
MemoryStream ms => ms.ToArray(),
byte[] raw => raw,
_ => null,
};
if (bytes is { Length: > 0 })
{
return new ClipboardImportPayload { Data = bytes, Extension = ".png" };
}
}
// 4. Plain bitmap (Snipping Tool, Paint) - always static, so it's the last resort.
if (Clipboard.ContainsImage() && Clipboard.GetImage() is { } bitmap)
{
using var stream = new MemoryStream();
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
encoder.Save(stream);
return new ClipboardImportPayload { Data = stream.ToArray(), Extension = ".png" };
}
return null;
}
[GeneratedRegex(@"data:image/(?<mime>gif|png|jpe?g);base64,(?<data>[A-Za-z0-9+/=]+)", RegexOptions.IgnoreCase)]
private static partial Regex DataUriRegex();
private static async Task SetClipboardWithRetryAsync(DataObject data)
{
const int maxAttempts = 5;