src/RichImageData.cs
Rama main · réplica de lectura del repositorio original.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Windows.Forms;
namespace Clipman
{
internal sealed class RichImageInfo : IDisposable
{
public string FileName { get; set; }
public string MimeType { get; set; }
public byte[] Data { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public Image Image { get; set; }
public void Dispose()
{
if (Image != null) Image.Dispose();
Image = null;
}
}
internal sealed class RichImageCapture
{
public string Text { get; set; }
public RichTextPayload RichText { get; set; }
public int ImageBytes { get; set; }
}
internal static class RichImageData
{
public const int MaximumStoredImageBytes = 512 * 1024;
public const int MaximumInputBytes = 16 * 1024 * 1024;
public const int MaximumPixels = 16 * 1024 * 1024;
public const int MaximumDimension = 4096;
public const int OptimizedMaximumDimension = 2048;
public const long MaximumDatabaseImageBytes = 8L * 1024L * 1024L;
private const string Prefix = "<img data-clipman-image=\"1\" data-clipman-filename=\"";
private const string AltMarker = "\" alt=\"";
private const string SourceMarker = "\" src=\"data:";
private const string Base64Marker = ";base64,";
private const string Suffix = "\">";
public static bool ClipboardHasStandaloneImage()
{
try
{
if (Clipboard.ContainsImage()) return true;
var data = Clipboard.GetDataObject();
return data != null && new[] { "PNG", "image/png", "JFIF", "image/jpeg" }.Any(data.GetDataPresent);
}
catch { return false; }
}
public static RichImageCapture CaptureFromClipboard(string sourceApplication = "")
{
byte[] original;
string mime;
if (!TryReadRawClipboardImage(out original, out mime))
{
Image clipboardImage;
try { clipboardImage = Clipboard.GetImage(); }
catch { return null; }
if (clipboardImage == null) return null;
using (clipboardImage)
{
if (!DimensionsAllowed(clipboardImage.Width, clipboardImage.Height)) return null;
original = Optimize(clipboardImage, "image/png", null, out mime);
}
}
return CreateCapture(original, mime, SourceImageFileName(sourceApplication, mime));
}
internal static string SourceImageFileName(string sourceApplication, string mime)
{
var source = NormalizeVirtualFileStem(sourceApplication, 64);
return "Clipboard image" + (source.Length == 0 ? "" : " - " + source) +
(mime == "image/png" ? ".png" : ".jpg");
}
internal static string CreateViewerFile(ClipEntry entry, string root = null)
{
RichImageInfo image;
if (entry == null || !TryDecode(entry.RichText, out image)) return null;
using (image)
{
var name = BuildExplorerFileName(entry, image.MimeType);
return root == null
? RichImageFileDropData.Add(new DataObject(), image.Data, name, entry.CreatedUnixMs)
: RichImageFileDropData.Add(new DataObject(), image.Data, name, root, DateTime.UtcNow, entry.CreatedUnixMs);
}
}
public static bool TryGetSingleClipboardImageFile(out string path, out string failureMessage)
{
path = string.Empty;
failureMessage = string.Empty;
StringCollection files;
try
{
if (!Clipboard.ContainsFileDropList()) return false;
files = Clipboard.GetFileDropList();
}
catch
{
failureMessage = "Could not read copied files from the clipboard.";
return false;
}
if (files == null || files.Count != 1)
{
failureMessage = "Copy exactly one PNG or JPEG file before pasting into Rich Text history.";
return false;
}
var candidate = files[0];
if (string.IsNullOrWhiteSpace(candidate) || !File.Exists(candidate) || Directory.Exists(candidate))
{
failureMessage = "The copied image file is no longer available.";
return false;
}
if (MimeForImageFileName(candidate).Length == 0)
{
failureMessage = "Only a copied PNG or JPEG file can be pasted into Rich Text history.";
return false;
}
path = candidate;
return true;
}
public static RichImageCapture CaptureFromFile(string path)
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path) || Directory.Exists(path)) return null;
var expectedMime = MimeForImageFileName(path);
if (expectedMime.Length == 0) return null;
byte[] original;
try
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
{
if (stream.Length <= 0 || stream.Length > MaximumInputBytes) return null;
original = DataBytes(stream);
}
}
catch
{
return null;
}
if (original == null || !MatchesMime(original, expectedMime)) return null;
return CreateCapture(original, expectedMime, Path.GetFileName(path));
}
private static RichImageCapture CreateCapture(byte[] original, string mime, string requestedFileName)
{
if (original == null || original.Length == 0 || original.Length > MaximumInputBytes) return null;
if (!MatchesMime(original, mime)) return null;
if (mime == "image/png" && IsAnimatedPng(original)) return null;
byte[] stored = original;
int width;
int height;
if (!TryReadDimensions(original, mime, out width, out height) || !DimensionsAllowed(width, height)) return null;
using (var source = LoadImage(original, out width, out height))
{
if (source == null || !DimensionsAllowed(width, height)) return null;
if (stored.Length > MaximumStoredImageBytes || width > OptimizedMaximumDimension || height > OptimizedMaximumDimension)
{
stored = Optimize(source, mime, ReadMetadata(source), out mime);
}
}
if (stored == null || stored.Length == 0 || stored.Length > MaximumStoredImageBytes) return null;
var extension = mime == "image/png" ? ".png" : ".jpg";
var fileName = string.IsNullOrWhiteSpace(requestedFileName) ? "Clipboard image" + extension : NormalizeFileName(requestedFileName, mime);
var html = BuildHtml(stored, mime, fileName, "Image: " + fileName);
if (Encoding.UTF8.GetByteCount(html) > RichTextData.MaxHtmlBytes) return null;
return new RichImageCapture
{
Text = FallbackText(fileName, stored),
ImageBytes = stored.Length,
RichText = new RichTextPayload
{
Version = 1,
HtmlFragment = html,
RtfBase64 = string.Empty,
PreferredFormat = "Html"
}
};
}
private static string MimeForImageFileName(string path)
{
var extension = Path.GetExtension(path ?? string.Empty);
if (string.Equals(extension, ".png", StringComparison.OrdinalIgnoreCase)) return "image/png";
if (string.Equals(extension, ".jpg", StringComparison.OrdinalIgnoreCase) ||
string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase)) return "image/jpeg";
return string.Empty;
}
public static string BuildHtml(byte[] data, string mimeType, string fileName, string alternateText)
{
if (data == null) return string.Empty;
var mime = NormalizeMime(mimeType);
if (mime.Length == 0) return string.Empty;
var canonicalFileName = NormalizeFileName(fileName, mime);
var canonicalAlternateText = "Image: " + canonicalFileName;
return Prefix + Attribute(canonicalFileName, 120) + AltMarker + Attribute(canonicalAlternateText, 200) + SourceMarker + mime + Base64Marker + Convert.ToBase64String(data) + Suffix;
}
public static bool TryDecode(RichTextPayload payload, out RichImageInfo info)
{
return TryDecode(payload, true, out info);
}
public static bool TryDescribe(RichTextPayload payload, out RichImageInfo info)
{
return TryDecode(payload, false, out info);
}
private static bool TryDecode(RichTextPayload payload, bool loadBitmap, out RichImageInfo info)
{
info = null;
if (payload == null || string.IsNullOrEmpty(payload.HtmlFragment)) return false;
var html = payload.HtmlFragment;
if (!html.StartsWith(Prefix, StringComparison.Ordinal) || !html.EndsWith(Suffix, StringComparison.Ordinal)) return false;
var fileEnd = html.IndexOf(AltMarker, Prefix.Length, StringComparison.Ordinal);
if (fileEnd < 0) return false;
var altStart = fileEnd + AltMarker.Length;
var altEnd = html.IndexOf(SourceMarker, altStart, StringComparison.Ordinal);
if (altEnd < 0) return false;
var mimeStart = altEnd + SourceMarker.Length;
var base64Start = html.IndexOf(Base64Marker, mimeStart, StringComparison.Ordinal);
if (base64Start < 0) return false;
var mime = NormalizeMime(html.Substring(mimeStart, base64Start - mimeStart));
if (mime.Length == 0) return false;
var encodedStart = base64Start + Base64Marker.Length;
var encodedLength = html.Length - Suffix.Length - encodedStart;
if (encodedLength <= 0 || encodedLength > ((MaximumStoredImageBytes + 2) / 3) * 4 + 4) return false;
byte[] data;
try { data = Convert.FromBase64String(html.Substring(encodedStart, encodedLength)); }
catch { return false; }
if (data.Length == 0 || data.Length > MaximumStoredImageBytes) return false;
if (!MatchesMime(data, mime)) return false;
if (mime == "image/png" && IsAnimatedPng(data)) return false;
int width;
int height;
if (!TryReadDimensions(data, mime, out width, out height) || !StoredDimensionsAllowed(width, height)) return false;
var encodedFileName = html.Substring(Prefix.Length, fileEnd - Prefix.Length);
var fileName = HttpUtility.HtmlDecode(encodedFileName);
if (!IsCanonicalFileName(fileName, mime)) return false;
var encodedAlternateText = html.Substring(altStart, altEnd - altStart);
var alternateText = HttpUtility.HtmlDecode(encodedAlternateText);
if (!string.Equals(alternateText, "Image: " + fileName, StringComparison.Ordinal)) return false;
if (!string.Equals(Attribute(fileName, 120), encodedFileName, StringComparison.Ordinal) ||
!string.Equals(Attribute(alternateText, 200), encodedAlternateText, StringComparison.Ordinal) ||
!string.Equals(BuildHtml(data, mime, fileName, alternateText), html, StringComparison.Ordinal)) return false;
Image image = null;
if (loadBitmap) image = LoadImage(data, out width, out height);
if ((loadBitmap && image == null) || !StoredDimensionsAllowed(width, height))
{
if (image != null) image.Dispose();
return false;
}
info = new RichImageInfo
{
FileName = fileName,
MimeType = mime,
Data = data,
Width = width,
Height = height,
Image = image
};
return true;
}
public static int StoredByteCount(RichTextPayload payload)
{
RichImageInfo info;
if (!TryDescribe(payload, out info)) return 0;
using (info) return info.Data == null ? 0 : info.Data.Length;
}
public static string FallbackText(string fileName, byte[] data)
{
var cleanName = HttpUtility.HtmlDecode(Attribute(fileName, 120));
if (string.IsNullOrWhiteSpace(cleanName)) cleanName = "Clipboard image";
using (var sha256 = SHA256.Create())
{
var digest = sha256.ComputeHash(data ?? new byte[0]);
var fingerprint = string.Concat(digest.Take(6).Select(value => value.ToString("x2")).ToArray());
return "Image: " + cleanName + " (" + fingerprint + ")";
}
}
public static bool AddNativeClipboardFormats(DataObject target, RichTextPayload payload)
{
return AddNativeClipboardFormats(target, payload, null);
}
public static bool AddNativeClipboardFormats(DataObject target, RichTextPayload payload, ClipEntry entry)
{
return AddNativeClipboardFormats(target, payload, entry, string.Empty, DateTime.UtcNow);
}
internal static bool AddNativeClipboardFormats(
DataObject target,
RichTextPayload payload,
ClipEntry entry,
string fileDropRoot,
DateTime nowUtc)
{
if (target == null) return false;
RichImageInfo info;
if (!TryDecode(payload, out info)) return false;
using (info)
{
var bitmap = new Bitmap(info.Image);
target.SetImage(bitmap);
target.SetData(info.MimeType == "image/png" ? "PNG" : "JFIF", false, new MemoryStream(info.Data, false));
if (entry != null)
{
var fileName = BuildExplorerFileName(entry, info.MimeType);
if (string.IsNullOrWhiteSpace(fileDropRoot)) RichImageFileDropData.Add(target, info.Data, fileName, entry.CreatedUnixMs);
else RichImageFileDropData.Add(target, info.Data, fileName, fileDropRoot, nowUtc, entry.CreatedUnixMs);
}
}
return true;
}
internal static string BuildExplorerFileName(ClipEntry entry, string mimeType)
{
var created = EntryCreatedTime(entry);
var device = NormalizeVirtualFileStem(entry == null ? string.Empty : entry.SourceMachine, 80);
if (device.Length == 0) device = "Unknown device";
var extension = string.Equals(NormalizeMime(mimeType), "image/png", StringComparison.Ordinal) ? ".png" : ".jpg";
return "Clipman image " + created.ToString("yyyy-MM-dd HH-mm-ss", CultureInfo.InvariantCulture) + " - " + device + extension;
}
private static DateTime EntryCreatedTime(ClipEntry entry)
{
try
{
return entry != null && entry.CreatedUnixMs > 0 ? TimeUtil.FromUnixMs(entry.CreatedUnixMs) : DateTime.Now;
}
catch
{
return DateTime.Now;
}
}
private static string NormalizeVirtualFileStem(string value, int maximumScalars)
{
var raw = HasWellFormedUtf16(value) ? (value ?? string.Empty).Trim() : string.Empty;
var invalid = new HashSet<char>(Path.GetInvalidFileNameChars());
var clean = new StringBuilder();
var pendingSpace = false;
for (var index = 0; index < raw.Length; index++)
{
var scalarLength = char.IsHighSurrogate(raw[index]) && index + 1 < raw.Length && char.IsLowSurrogate(raw[index + 1]) ? 2 : 1;
var category = CharUnicodeInfo.GetUnicodeCategory(raw, index);
if (char.IsWhiteSpace(raw, index) || category == UnicodeCategory.LineSeparator || category == UnicodeCategory.ParagraphSeparator)
{
pendingSpace = clean.Length > 0;
index += scalarLength - 1;
continue;
}
if (category == UnicodeCategory.Control || category == UnicodeCategory.Format || category == UnicodeCategory.Surrogate ||
invalid.Contains(raw[index]))
{
index += scalarLength - 1;
continue;
}
if (pendingSpace)
{
clean.Append(' ');
pendingSpace = false;
}
clean.Append(raw, index, scalarLength);
index += scalarLength - 1;
}
return TruncateScalars(clean.ToString().Trim().TrimEnd('.'), maximumScalars).TrimEnd(' ', '.');
}
private static bool TryReadRawClipboardImage(out byte[] data, out string mime)
{
data = null;
mime = string.Empty;
IDataObject clipboard;
try { clipboard = Clipboard.GetDataObject(); }
catch { return false; }
if (clipboard == null) return false;
foreach (var candidate in new[]
{
new KeyValuePair<string, string>("PNG", "image/png"),
new KeyValuePair<string, string>("image/png", "image/png"),
new KeyValuePair<string, string>("JFIF", "image/jpeg"),
new KeyValuePair<string, string>("image/jpeg", "image/jpeg")
})
{
if (!clipboard.GetDataPresent(candidate.Key)) continue;
var bytes = DataBytes(clipboard.GetData(candidate.Key));
if (bytes == null || bytes.Length == 0 || bytes.Length > MaximumInputBytes) continue;
data = bytes;
mime = candidate.Value;
return true;
}
return false;
}
private static byte[] DataBytes(object value)
{
var bytes = value as byte[];
if (bytes != null) return bytes;
var stream = value as Stream;
if (stream == null) return null;
try
{
if (stream.CanSeek && stream.Length > MaximumInputBytes) return null;
using (var output = new MemoryStream())
{
var buffer = new byte[8192];
while (output.Length <= MaximumInputBytes)
{
var read = stream.Read(buffer, 0, Math.Min(buffer.Length, MaximumInputBytes + 1 - (int)output.Length));
if (read <= 0) break;
output.Write(buffer, 0, read);
}
return output.Length <= MaximumInputBytes ? output.ToArray() : null;
}
}
catch { return null; }
}
private static Image LoadImage(byte[] data, out int width, out int height)
{
width = 0;
height = 0;
try
{
string mime;
if (MatchesMime(data, "image/png")) mime = "image/png";
else if (MatchesMime(data, "image/jpeg")) mime = "image/jpeg";
else return null;
if (!TryReadDimensions(data, mime, out width, out height) || !DimensionsAllowed(width, height)) return null;
using (var stream = new MemoryStream(data, false))
using (var source = Image.FromStream(stream, true, true))
{
if (source.Width != width || source.Height != height || !DimensionsAllowed(source.Width, source.Height)) return null;
var copy = new Bitmap(source);
CopyBoundedMetadata(copy, ReadMetadata(source));
return copy;
}
}
catch { return null; }
}
private static bool TryReadDimensions(byte[] data, string mime, out int width, out int height)
{
width = 0;
height = 0;
if (!MatchesMime(data, mime)) return false;
if (mime == "image/png")
{
if (data.Length < 24) return false;
width = ReadBigEndianInt32(data, 16);
height = ReadBigEndianInt32(data, 20);
return width > 0 && height > 0;
}
var position = 2;
while (position + 3 < data.Length)
{
if (data[position] != 0xff) return false;
while (position < data.Length && data[position] == 0xff) position++;
if (position >= data.Length) return false;
var marker = data[position++];
if (marker == 0xd8 || marker == 0x01) continue;
if (marker == 0xd9 || marker == 0xda) return false;
if (position + 1 >= data.Length) return false;
var segmentLength = (data[position] << 8) | data[position + 1];
if (segmentLength < 2 || position + segmentLength > data.Length) return false;
if (IsStartOfFrame(marker))
{
if (segmentLength < 7) return false;
height = (data[position + 3] << 8) | data[position + 4];
width = (data[position + 5] << 8) | data[position + 6];
return width > 0 && height > 0;
}
position += segmentLength;
}
return false;
}
private static bool IsStartOfFrame(byte marker)
{
return marker >= 0xc0 && marker <= 0xcf && marker != 0xc4 && marker != 0xc8 && marker != 0xcc;
}
private static int ReadBigEndianInt32(byte[] data, int offset)
{
if (data == null || offset < 0 || offset + 4 > data.Length) return 0;
var value = ((long)data[offset] << 24) | ((long)data[offset + 1] << 16) | ((long)data[offset + 2] << 8) | data[offset + 3];
return value > int.MaxValue ? 0 : (int)value;
}
private static bool DimensionsAllowed(int width, int height)
{
return width > 0 && height > 0 && width <= MaximumDimension && height <= MaximumDimension && (long)width * height <= MaximumPixels;
}
private static bool StoredDimensionsAllowed(int width, int height)
{
return width > 0 && height > 0 && width <= OptimizedMaximumDimension && height <= OptimizedMaximumDimension;
}
private static bool IsAnimatedPng(byte[] data)
{
if (!MatchesMime(data, "image/png")) return false;
var offset = 8;
while (offset + 12 <= data.Length)
{
var length = ReadBigEndianInt32(data, offset);
if (length < 0 || (long)offset + 12 + length > data.Length) return true;
var typeOffset = offset + 4;
if (data[typeOffset] == (byte)'a' && data[typeOffset + 1] == (byte)'c' &&
data[typeOffset + 2] == (byte)'T' && data[typeOffset + 3] == (byte)'L') return true;
offset += 12 + length;
if (data[typeOffset] == (byte)'I' && data[typeOffset + 1] == (byte)'E' &&
data[typeOffset + 2] == (byte)'N' && data[typeOffset + 3] == (byte)'D') return false;
}
return true;
}
private static byte[] Optimize(Image source, string sourceMime, PropertyItem[] metadata, out string mime)
{
mime = sourceMime == "image/png" && HasAlpha(source) ? "image/png" : "image/jpeg";
var scale = Math.Min(1.0, Math.Min((double)OptimizedMaximumDimension / source.Width, (double)OptimizedMaximumDimension / source.Height));
var width = Math.Max(1, (int)Math.Round(source.Width * scale));
var height = Math.Max(1, (int)Math.Round(source.Height * scale));
using (var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb))
{
var horizontalResolution = SafeResolution(source.HorizontalResolution);
var verticalResolution = SafeResolution(source.VerticalResolution);
bitmap.SetResolution(horizontalResolution, verticalResolution);
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(mime == "image/png" ? Color.Transparent : Color.White);
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.DrawImage(source, 0, 0, width, height);
}
CopyBoundedMetadata(bitmap, metadata);
if (mime == "image/png")
{
var png = SavePng(bitmap);
if (png.Length <= MaximumStoredImageBytes) return png;
mime = "image/jpeg";
}
foreach (var quality in new long[] { 88, 80, 72, 64, 55, 45 })
{
var jpeg = SaveJpeg(bitmap, quality);
if (jpeg.Length <= MaximumStoredImageBytes) return jpeg;
}
}
return null;
}
private static float SafeResolution(float value)
{
return value >= 1 && value <= 2400 && !float.IsNaN(value) && !float.IsInfinity(value) ? value : 96;
}
private static void CopyBoundedMetadata(Image destination, IEnumerable<PropertyItem> items)
{
if (destination == null || items == null) return;
var total = 0;
foreach (var item in items)
{
if (item == null || item.Value == null || item.Value.Length > 16 * 1024 || total + item.Value.Length > 64 * 1024) continue;
try
{
destination.SetPropertyItem(item);
total += item.Value.Length;
}
catch { }
}
}
private static PropertyItem[] ReadMetadata(Image image)
{
try { return image == null ? new PropertyItem[0] : image.PropertyItems; }
catch { return new PropertyItem[0]; }
}
private static byte[] SavePng(Image image)
{
using (var stream = new MemoryStream())
{
image.Save(stream, ImageFormat.Png);
return stream.ToArray();
}
}
private static byte[] SaveJpeg(Image image, long quality)
{
using (var stream = new MemoryStream())
{
var codec = ImageCodecInfo.GetImageEncoders().First(value => value.FormatID == ImageFormat.Jpeg.Guid);
using (var parameters = new EncoderParameters(1))
{
parameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
image.Save(stream, codec, parameters);
}
return stream.ToArray();
}
}
private static bool HasAlpha(Image image)
{
return image != null && (Image.IsAlphaPixelFormat(image.PixelFormat) || Image.IsExtendedPixelFormat(image.PixelFormat));
}
private static string NormalizeMime(string value)
{
if (string.Equals(value, "image/png", StringComparison.OrdinalIgnoreCase)) return "image/png";
if (string.Equals(value, "image/jpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "image/jpg", StringComparison.OrdinalIgnoreCase)) return "image/jpeg";
return string.Empty;
}
private static bool MatchesMime(byte[] data, string mime)
{
if (data == null) return false;
if (mime == "image/png")
{
return data.Length >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4e && data[3] == 0x47 &&
data[4] == 0x0d && data[5] == 0x0a && data[6] == 0x1a && data[7] == 0x0a;
}
if (mime == "image/jpeg")
{
return data.Length >= 3 && data[0] == 0xff && data[1] == 0xd8 && data[2] == 0xff;
}
return false;
}
private static string Attribute(string value, int maximumLength)
{
var text = (value ?? string.Empty).Trim();
text = TruncateScalars(text, maximumLength);
return text.Replace("&", "&")
.Replace("\"", """)
.Replace("<", "<")
.Replace(">", ">");
}
private static bool IsCanonicalFileName(string value, string mime)
{
return !string.IsNullOrEmpty(value) &&
string.Equals(value, NormalizeFileName(value, mime), StringComparison.Ordinal);
}
private static string NormalizeFileName(string value, string mime)
{
var fallbackExtension = mime == "image/png" ? ".png" : ".jpg";
var raw = HasWellFormedUtf16(value) ? (value ?? string.Empty).Trim() : string.Empty;
var lower = raw.ToLowerInvariant();
if (lower.StartsWith("content:", StringComparison.Ordinal) ||
lower.StartsWith("file:", StringComparison.Ordinal) ||
lower.StartsWith("ph:", StringComparison.Ordinal) ||
lower.StartsWith("assets-library:", StringComparison.Ordinal) ||
lower.Contains("://"))
{
raw = string.Empty;
}
else
{
raw = raw.Replace('\\', '/');
var slash = raw.LastIndexOf('/');
if (slash >= 0) raw = raw.Substring(slash + 1);
var marker = raw.IndexOfAny(new[] { '?', '#' });
if (marker >= 0) raw = raw.Substring(0, marker);
}
var clean = new StringBuilder();
var pendingSpace = false;
for (var index = 0; index < raw.Length; index++)
{
var scalarLength = char.IsHighSurrogate(raw[index]) && index + 1 < raw.Length && char.IsLowSurrogate(raw[index + 1]) ? 2 : 1;
var codePoint = scalarLength == 2 ? char.ConvertToUtf32(raw, index) : raw[index];
var category = CharUnicodeInfo.GetUnicodeCategory(raw, index);
if (char.IsWhiteSpace(raw, index) || category == UnicodeCategory.LineSeparator || category == UnicodeCategory.ParagraphSeparator)
{
pendingSpace = clean.Length > 0;
index += scalarLength - 1;
continue;
}
if (category == UnicodeCategory.Control || category == UnicodeCategory.Format || category == UnicodeCategory.Surrogate ||
codePoint == 0xfffd || codePoint == '/' || codePoint == '\\')
{
index += scalarLength - 1;
continue;
}
if (pendingSpace)
{
clean.Append(' ');
pendingSpace = false;
}
if (codePoint != ':') clean.Append(char.ConvertFromUtf32(codePoint));
index += scalarLength - 1;
}
var normalized = clean.ToString().Trim();
var normalizedLower = normalized.ToLowerInvariant();
string extension;
string stem;
if (mime == "image/png")
{
extension = ".png";
stem = normalizedLower.EndsWith(".png", StringComparison.Ordinal) ? normalized.Substring(0, normalized.Length - 4) : RemoveImageExtension(normalized);
}
else if (normalizedLower.EndsWith(".jpeg", StringComparison.Ordinal))
{
extension = ".jpeg";
stem = normalized.Substring(0, normalized.Length - 5);
}
else if (normalizedLower.EndsWith(".jpg", StringComparison.Ordinal))
{
extension = ".jpg";
stem = normalized.Substring(0, normalized.Length - 4);
}
else
{
extension = fallbackExtension;
stem = RemoveImageExtension(normalized);
}
stem = stem.Trim();
if (stem.Length == 0 || stem == ".") stem = "Clipboard image";
stem = TruncateScalars(stem, 120 - ScalarCount(extension)).TrimEnd();
if (stem.Length == 0) stem = "Clipboard image";
return stem + extension;
}
private static string RemoveImageExtension(string value)
{
var lower = (value ?? string.Empty).ToLowerInvariant();
foreach (var extension in new[] { ".jpeg", ".jpg", ".png" })
{
if (lower.EndsWith(extension, StringComparison.Ordinal)) return value.Substring(0, value.Length - extension.Length);
}
return value ?? string.Empty;
}
private static int ScalarCount(string value)
{
var count = 0;
for (var index = 0; index < (value ?? string.Empty).Length; index++)
{
if (char.IsHighSurrogate(value[index]) && index + 1 < value.Length && char.IsLowSurrogate(value[index + 1])) index++;
count++;
}
return count;
}
private static bool HasWellFormedUtf16(string value)
{
for (var index = 0; index < (value ?? string.Empty).Length; index++)
{
if (char.IsHighSurrogate(value[index]))
{
if (index + 1 >= value.Length || !char.IsLowSurrogate(value[index + 1])) return false;
index++;
}
else if (char.IsLowSurrogate(value[index]))
{
return false;
}
}
return true;
}
private static string TruncateScalars(string value, int maximumScalars)
{
if (string.IsNullOrEmpty(value) || maximumScalars < 1) return string.Empty;
var scalars = 0;
var index = 0;
while (index < value.Length && scalars < maximumScalars)
{
if (char.IsHighSurrogate(value[index]))
{
if (index + 1 < value.Length && char.IsLowSurrogate(value[index + 1])) index += 2;
else
{
index++;
continue;
}
}
else if (char.IsLowSurrogate(value[index]))
{
index++;
continue;
}
else index++;
scalars++;
}
return index >= value.Length ? value : value.Substring(0, index);
}
}
}