Created
March 5, 2026 15:21
-
-
Save DrkWzrd/32049daaad944cd5dbcc6b14a2b9eedd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using SixLabors.ImageSharp; | |
| using SixLabors.ImageSharp.Advanced; | |
| using SixLabors.ImageSharp.PixelFormats; | |
| using System.ComponentModel; | |
| using System.Globalization; | |
| using System.Runtime.CompilerServices; | |
| using System.Runtime.InteropServices; | |
| using System.Windows; | |
| using System.Windows.Media; | |
| using System.Windows.Media.Imaging; | |
| #pragma warning disable CS0067 | |
| public sealed class ImageSharpBitmapSource : BitmapSource | |
| { | |
| private static readonly Type ByteType = typeof(byte); | |
| [EditorBrowsable(EditorBrowsableState.Never)] | |
| internal sealed class ImageSharpBitmapSourceTypeConverter : TypeConverter | |
| { | |
| private static readonly ConditionalWeakTable<Image, ImageSource> Cache = []; | |
| private static readonly Type ImageType = typeof(Image); | |
| private static readonly Type ImageSourceType = typeof(ImageSource); | |
| [ModuleInitializer] | |
| public static void LoadTypeConverter() | |
| { | |
| _ = TypeDescriptor.AddAttributes(ImageType, new TypeConverterAttribute(typeof(ImageSharpBitmapSourceTypeConverter))); | |
| } | |
| public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) | |
| { | |
| return sourceType.IsAssignableTo(ImageType); | |
| } | |
| public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) | |
| { | |
| return destinationType?.IsAssignableTo(ImageSourceType) ?? false; | |
| } | |
| public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) | |
| { | |
| return value is Image image ? (object)Cache.GetValue(image, img => new ImageSharpBitmapSource(img)) : throw new InvalidCastException(); | |
| } | |
| public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) | |
| { | |
| return value is Image image && destinationType.IsAssignableTo(ImageSourceType) | |
| ? (object)Cache.GetValue(image, img => new ImageSharpBitmapSource(img)) | |
| : throw new InvalidCastException(); | |
| } | |
| } | |
| /// <summary> | |
| /// The number of pixels (Width * Height) above which CopyPixels will use | |
| /// a parallel loop. | |
| /// | |
| /// 512 * 512 = 262144. | |
| /// maxValue for disable. | |
| /// </summary> | |
| private const long PARALLEL_COPY_THRESHOLD = 262144; | |
| private readonly bool _isClone; | |
| public Image<Bgra32> Source { get; } | |
| public override bool IsDownloading => false; | |
| public override PixelFormat Format => PixelFormats.Bgra32; | |
| public override int PixelHeight => Source.Height; | |
| public override int PixelWidth => Source.Width; | |
| public override double DpiX => Source.Metadata.HorizontalDpi ?? 96; | |
| public override double DpiY => Source.Metadata.VerticalDpi ?? 96; | |
| public override BitmapPalette? Palette => null; | |
| public override event EventHandler<ExceptionEventArgs>? DecodeFailed; | |
| public override event EventHandler<DownloadProgressEventArgs>? DownloadProgress; | |
| public override event EventHandler<ExceptionEventArgs>? DownloadFailed; | |
| public override event EventHandler? DownloadCompleted; | |
| public ImageSharpBitmapSource(Image image) | |
| : this(image is Image<Bgra32> bgra ? bgra : image.CloneAs<Bgra32>(), image is not Image<Bgra32>) { } | |
| private ImageSharpBitmapSource(Image<Bgra32> image, bool disposeAfter) | |
| { | |
| Source = image; | |
| _isClone = disposeAfter; | |
| } | |
| //WPF doesn't dispose IDisposable BitmapSource, | |
| //and we can not do it ourselves because is generated behind, | |
| //we have to trust in the GC, if we cloned the image. | |
| ~ImageSharpBitmapSource() | |
| { | |
| if (_isClone) | |
| Source.Dispose(); | |
| } | |
| protected override Freezable CreateInstanceCore() | |
| { | |
| return new ImageSharpBitmapSource(Source.Clone(), true); | |
| } | |
| public override void CopyPixels(Int32Rect sourceRect, Array pixels, int stride, int offset) | |
| { | |
| if (Source is null) | |
| return; | |
| if (pixels.Length <= 0) | |
| throw new ArgumentException("The array must have at least one element", nameof(pixels)); | |
| var value = pixels.GetValue(0) ?? throw new InvalidOperationException("Array contains null values"); | |
| var valueType = value.GetType(); | |
| if (valueType != ByteType) | |
| throw new ArgumentException("The content of the array must be bytes", nameof(pixels)); | |
| var elementSize = Marshal.SizeOf(valueType); | |
| int bufferSize = Buffer.ByteLength(pixels); | |
| if (sourceRect.IsEmpty) | |
| sourceRect = new Int32Rect(0, 0, PixelWidth, PixelHeight); | |
| int offsetInBytes = checked(offset * elementSize); | |
| ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(offsetInBytes, bufferSize, nameof(stride)); | |
| int actualBufferSize = bufferSize - offsetInBytes; | |
| int bytesPerPixel = Format.GetBytesPerPixel(); | |
| long totalPixels = (long)sourceRect.Width * sourceRect.Height; | |
| if (totalPixels < PARALLEL_COPY_THRESHOLD) | |
| { | |
| Source.ProcessPixelRows(accessor => | |
| { | |
| Span<byte> rawSpan = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(pixels), bufferSize); | |
| Span<byte> actualBuffer = rawSpan.Slice(offsetInBytes, actualBufferSize); | |
| for (int y = 0; y < sourceRect.Height; y++) | |
| { | |
| Span<byte> bufferRowSection = actualBuffer.Slice((y * stride) + (sourceRect.X * bytesPerPixel), sourceRect.Width * bytesPerPixel); | |
| Span<Bgra32> sourceRowMemory = accessor.GetRowSpan(y + sourceRect.Y); | |
| Span<Bgra32> sourceRowSectionSpan = sourceRowMemory.Slice(sourceRect.X, sourceRect.Width); | |
| Span<byte> rawSourceRowSectionSpan = MemoryMarshal.AsBytes(sourceRowSectionSpan); | |
| rawSourceRowSectionSpan.CopyTo(bufferRowSection); | |
| } | |
| }); | |
| } | |
| else | |
| { | |
| _ = Parallel.For(0, sourceRect.Height, y => | |
| { | |
| Span<byte> rawSpan = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(pixels), bufferSize); | |
| Span<byte> actualBuffer = rawSpan.Slice(offsetInBytes, actualBufferSize); | |
| Span<byte> bufferRowSection = actualBuffer.Slice((y * stride) + (sourceRect.X * bytesPerPixel), sourceRect.Width * bytesPerPixel); | |
| Memory<Bgra32> sourceRowMemory = Source.DangerousGetPixelRowMemory(y + sourceRect.Y); | |
| Span<Bgra32> sourceRowSectionSpan = sourceRowMemory.Span.Slice(sourceRect.X, sourceRect.Width); | |
| Span<byte> rawSourceRowSectionSpan = MemoryMarshal.AsBytes(sourceRowSectionSpan); | |
| rawSourceRowSectionSpan.CopyTo(bufferRowSection); | |
| }); | |
| } | |
| } | |
| public override void CopyPixels(Int32Rect sourceRect, nint buffer, int bufferSize, int stride) | |
| { | |
| if (Source is null) | |
| return; | |
| if (sourceRect.IsEmpty) | |
| sourceRect = new Int32Rect(0, 0, PixelWidth, PixelHeight); | |
| ArgumentOutOfRangeException.ThrowIfNegative(sourceRect.X); | |
| ArgumentOutOfRangeException.ThrowIfNegative(sourceRect.Y); | |
| ArgumentOutOfRangeException.ThrowIfNegativeOrZero(sourceRect.Width); | |
| ArgumentOutOfRangeException.ThrowIfNegativeOrZero(sourceRect.Height); | |
| ArgumentOutOfRangeException.ThrowIfGreaterThan(sourceRect.X + sourceRect.Width, PixelWidth); | |
| ArgumentOutOfRangeException.ThrowIfGreaterThan(sourceRect.Y + sourceRect.Height, PixelHeight); | |
| ArgumentOutOfRangeException.ThrowIfNegativeOrZero(buffer); | |
| ArgumentOutOfRangeException.ThrowIfNegativeOrZero(stride); | |
| ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize); | |
| int bytesPerPixel = Format.GetBytesPerPixel(); | |
| uint minStride = (uint)(sourceRect.Width * bytesPerPixel); | |
| ArgumentOutOfRangeException.ThrowIfLessThan((uint)stride, minStride, nameof(stride)); | |
| uint requiredSize = (uint)((sourceRect.Height - 1) * stride) + minStride; | |
| ArgumentOutOfRangeException.ThrowIfLessThan((uint)bufferSize, requiredSize, nameof(bufferSize)); | |
| long totalPixels = (long)sourceRect.Width * sourceRect.Height; | |
| nint offset = buffer + (sourceRect.X * bytesPerPixel); | |
| int rowByteLength = sourceRect.Width * bytesPerPixel; | |
| if (totalPixels < PARALLEL_COPY_THRESHOLD) | |
| { | |
| Source.ProcessPixelRows(accessor => | |
| { | |
| for (int y = 0; y < sourceRect.Height; y++) | |
| { | |
| nint pDestRow = offset + (y * stride); | |
| unsafe | |
| { | |
| Span<byte> bufferRowSection = new(pDestRow.ToPointer(), rowByteLength); | |
| Span<Bgra32> sourceRowMemory = accessor.GetRowSpan(y + sourceRect.Y); | |
| Span<Bgra32> sourceRowSectionSpan = sourceRowMemory.Slice(sourceRect.X, sourceRect.Width); | |
| Span<byte> rawSourceRowSectionSpan = MemoryMarshal.AsBytes(sourceRowSectionSpan); | |
| rawSourceRowSectionSpan.CopyTo(bufferRowSection); | |
| } | |
| } | |
| }); | |
| } | |
| else | |
| { | |
| _ = Parallel.For(0, sourceRect.Height, y => | |
| { | |
| nint pDestRow = offset + (y * stride); | |
| unsafe | |
| { | |
| Span<byte> bufferRowSection = new(pDestRow.ToPointer(), rowByteLength); | |
| Memory<Bgra32> sourceRowMemory = Source.DangerousGetPixelRowMemory(y + sourceRect.Y); | |
| Span<Bgra32> sourceRowSectionSpan = sourceRowMemory.Span.Slice(sourceRect.X, sourceRect.Width); | |
| Span<byte> rawSourceRowSectionSpan = MemoryMarshal.AsBytes(sourceRowSectionSpan); | |
| rawSourceRowSectionSpan.CopyTo(bufferRowSection); | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| #pragma warning restore CS0067 | |
| public static class ImageSharpMetadataExtensions | |
| { | |
| extension(ImageMetadata metadata) | |
| { | |
| public double? HorizontalDpi => metadata.ResolutionUnits switch | |
| { | |
| PixelResolutionUnit.PixelsPerInch => metadata.HorizontalResolution, | |
| PixelResolutionUnit.PixelsPerCentimeter => metadata.HorizontalResolution * 2.54, | |
| PixelResolutionUnit.PixelsPerMeter => metadata.HorizontalResolution * 0.0254, | |
| _ => null | |
| }; | |
| public double? VerticalDpi => metadata.ResolutionUnits switch | |
| { | |
| PixelResolutionUnit.PixelsPerInch => metadata.VerticalResolution, | |
| PixelResolutionUnit.PixelsPerCentimeter => metadata.VerticalResolution * 2.54, | |
| PixelResolutionUnit.PixelsPerMeter => metadata.VerticalResolution * 0.0254, | |
| _ => null | |
| }; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment