using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace Tango.RemoteDesktop.Utils { /// /// Encapsulates a Bitmap for fast bitmap pixel operations using 32bpp images /// public unsafe class FastBitmap : IDisposable { /// /// Specifies the number of bytes available per pixel of the bitmap object being manipulated /// public const int BytesPerPixel = 4; /// /// The Bitmap object encapsulated on this FastBitmap /// private readonly Bitmap _bitmap; /// /// The BitmapData resulted from the lock operation /// private BitmapData _bitmapData; /// /// The first pixel of the bitmap /// private int* _scan0; /// /// Gets the width of this FastBitmap object /// public int Width { get; } /// /// Gets the height of this FastBitmap object /// public int Height { get; } /// /// Gets the pointer to the first pixel of the bitmap /// public IntPtr Scan0 => _bitmapData.Scan0; /// /// Gets the stride width (in int32-sized values) of the bitmap /// public int Stride { get; private set; } /// /// Gets the stride width (in bytes) of the bitmap /// public int StrideInBytes { get; private set; } /// /// Gets a boolean value that states whether this FastBitmap is currently locked in memory /// public bool Locked { get; private set; } /// /// Gets an array of 32-bit color pixel values that represent this FastBitmap /// /// The locking operation required to extract the values off from the underlying bitmap failed /// The bitmap is already locked outside this fast bitmap public int[] DataArray { get { bool unlockAfter = false; if (!Locked) { Lock(); unlockAfter = true; } // Declare an array to hold the bytes of the bitmap int bytes = Math.Abs(_bitmapData.Stride) * _bitmap.Height; int[] argbValues = new int[bytes / BytesPerPixel]; // Copy the RGB values into the array Marshal.Copy(_bitmapData.Scan0, argbValues, 0, bytes / BytesPerPixel); if (unlockAfter) { Unlock(); } return argbValues; } } /// /// Creates a new instance of the FastBitmap class with a specified Bitmap. /// The bitmap provided must have a 32bpp depth /// /// The Bitmap object to encapsulate on this FastBitmap object /// The bitmap provided does not have a 32bpp pixel format public FastBitmap(Bitmap bitmap) { if (Image.GetPixelFormatSize(bitmap.PixelFormat) != 32) { throw new ArgumentException(@"The provided bitmap must have a 32bpp depth", nameof(bitmap)); } _bitmap = bitmap; Width = bitmap.Width; Height = bitmap.Height; } /// /// Disposes of this fast bitmap object and releases any pending resources. /// The underlying bitmap is not disposes, and is unlocked, if currently locked /// public void Dispose() { if (Locked) { Unlock(); } } /// /// Locks the bitmap to start the bitmap operations. If the bitmap is already locked, /// an exception is thrown /// /// A fast bitmap locked struct that will unlock the underlying bitmap after disposal /// The bitmap is already locked /// The locking operation in the underlying bitmap failed /// The bitmap is already locked outside this fast bitmap public FastBitmapLocker Lock() { return Lock((FastBitmapLockFormat)_bitmap.PixelFormat); } /// /// Locks the bitmap to start the bitmap operations. If the bitmap is already locked, /// an exception is thrown. /// /// The provided pixel format should be a 32bpp format. /// /// A pixel format to use when locking the underlying bitmap /// A fast bitmap locked struct that will unlock the underlying bitmap after disposal /// The bitmap is already locked /// The locking operation in the underlying bitmap failed /// The bitmap is already locked outside this fast bitmap public FastBitmapLocker Lock(FastBitmapLockFormat pixelFormat) { if (Locked) { throw new InvalidOperationException("Unlock must be called before a Lock operation"); } return Lock(ImageLockMode.ReadWrite, (PixelFormat)pixelFormat); } /// /// Locks the bitmap to start the bitmap operations /// /// The lock mode to use on the bitmap /// A pixel format to use when locking the underlying bitmap /// A fast bitmap locked struct that will unlock the underlying bitmap after disposal /// The locking operation in the underlying bitmap failed /// The bitmap is already locked outside this fast bitmap /// is not a 32bpp format private FastBitmapLocker Lock(ImageLockMode lockMode, PixelFormat pixelFormat) { var rect = new Rectangle(0, 0, _bitmap.Width, _bitmap.Height); return Lock(lockMode, rect, pixelFormat); } /// /// Locks the bitmap to start the bitmap operations /// /// The lock mode to use on the bitmap /// The rectangle to lock /// A pixel format to use when locking the underlying bitmap /// A fast bitmap locked struct that will unlock the underlying bitmap after disposal /// The provided region is invalid /// The locking operation in the underlying bitmap failed /// The bitmap region is already locked /// is not a 32bpp format private FastBitmapLocker Lock(ImageLockMode lockMode, Rectangle rect, PixelFormat pixelFormat) { // Lock the bitmap's bits _bitmapData = _bitmap.LockBits(rect, lockMode, pixelFormat); _scan0 = (int*)_bitmapData.Scan0; Stride = _bitmapData.Stride / BytesPerPixel; StrideInBytes = _bitmapData.Stride; Locked = true; return new FastBitmapLocker(this); } /// /// Unlocks the bitmap and applies the changes made to it. If the bitmap was not locked /// beforehand, an exception is thrown /// /// The bitmap is already unlocked /// The unlocking operation in the underlying bitmap failed public void Unlock() { if (!Locked) { throw new InvalidOperationException("Lock must be called before an Unlock operation"); } _bitmap.UnlockBits(_bitmapData); Locked = false; } /// /// Sets the pixel color at the given coordinates. If the bitmap was not locked beforehands, /// an exception is thrown /// /// The X coordinate of the pixel to set /// The Y coordinate of the pixel to set /// The new color of the pixel to set /// The fast bitmap is not locked /// The provided coordinates are out of bounds of the bitmap public void SetPixel(int x, int y, Color color) { SetPixel(x, y, color.ToArgb()); } /// /// Sets the pixel color at the given coordinates. If the bitmap was not locked beforehands, /// an exception is thrown /// /// The X coordinate of the pixel to set /// The Y coordinate of the pixel to set /// The new color of the pixel to set /// The fast bitmap is not locked /// The provided coordinates are out of bounds of the bitmap public void SetPixel(int x, int y, int color) { SetPixel(x, y, unchecked((uint)color)); } /// /// Sets the pixel color at the given coordinates. If the bitmap was not locked beforehands, /// an exception is thrown /// /// The X coordinate of the pixel to set /// The Y coordinate of the pixel to set /// The new color of the pixel to set /// The fast bitmap is not locked /// The provided coordinates are out of bounds of the bitmap public void SetPixel(int x, int y, uint color) { if (!Locked) { throw new InvalidOperationException("The FastBitmap must be locked before any pixel operations are made"); } if (x < 0 || x >= Width) { throw new ArgumentOutOfRangeException(nameof(x), @"The X component must be >= 0 and < width"); } if (y < 0 || y >= Height) { throw new ArgumentOutOfRangeException(nameof(y), @"The Y component must be >= 0 and < height"); } *(uint*)(_scan0 + x + y * Stride) = color; } /// /// Gets the pixel color at the given coordinates. If the bitmap was not locked beforehands, /// an exception is thrown /// /// The X coordinate of the pixel to get /// The Y coordinate of the pixel to get /// The fast bitmap is not locked /// The provided coordinates are out of bounds of the bitmap public Color GetPixel(int x, int y) { return Color.FromArgb(GetPixelInt(x, y)); } /// /// Gets the pixel color at the given coordinates as an integer value. If the bitmap /// was not locked beforehands, an exception is thrown /// /// The X coordinate of the pixel to get /// The Y coordinate of the pixel to get /// The fast bitmap is not locked /// The provided coordinates are out of bounds of the bitmap public int GetPixelInt(int x, int y) { if (!Locked) { throw new InvalidOperationException("The FastBitmap must be locked before any pixel operations are made"); } if (x < 0 || x >= Width) { throw new ArgumentOutOfRangeException(nameof(x), @"The X component must be >= 0 and < width"); } if (y < 0 || y >= Height) { throw new ArgumentOutOfRangeException(nameof(y), @"The Y component must be >= 0 and < height"); } return *(_scan0 + x + y * Stride); } /// /// Gets the pixel color at the given coordinates as an unsigned integer value. /// If the bitmap was not locked beforehands, an exception is thrown /// /// The X coordinate of the pixel to get /// The Y coordinate of the pixel to get /// The fast bitmap is not locked /// The provided coordinates are out of bounds of the bitmap public uint GetPixelUInt(int x, int y) { if (!Locked) { throw new InvalidOperationException("The FastBitmap must be locked before any pixel operations are made"); } if (x < 0 || x >= Width) { throw new ArgumentOutOfRangeException(nameof(x), @"The X component must be >= 0 and < width"); } if (y < 0 || y >= Height) { throw new ArgumentOutOfRangeException(nameof(y), @"The Y component must be >= 0 and < height"); } return *((uint*)_scan0 + x + y * Stride); } /// /// Copies the contents of the given array of colors into this FastBitmap. /// Throws an ArgumentException if the count of colors on the array mismatches the pixel count from this FastBitmap /// /// The array of colors to copy /// Whether to ignore zeroes when copying the data public void CopyFromArray(int[] colors, bool ignoreZeroes = false) { if (colors.Length != Width * Height) { throw new ArgumentException(@"The number of colors of the given array mismatch the pixel count of the bitmap", nameof(colors)); } // Simply copy the argb values array // ReSharper disable once InconsistentNaming int* s0t = _scan0; fixed (int* source = colors) { // ReSharper disable once InconsistentNaming int* s0s = source; int count = Width * Height; if (!ignoreZeroes) { // Unfold the loop const int sizeBlock = 8; int rem = count % sizeBlock; count /= sizeBlock; while (count-- > 0) { *(s0t++) = *(s0s++); *(s0t++) = *(s0s++); *(s0t++) = *(s0s++); *(s0t++) = *(s0s++); *(s0t++) = *(s0s++); *(s0t++) = *(s0s++); *(s0t++) = *(s0s++); *(s0t++) = *(s0s++); } while (rem-- > 0) { *(s0t++) = *(s0s++); } } else { while (count-- > 0) { if (*(s0s) == 0) { s0t++; s0s++; continue; } *(s0t++) = *(s0s++); } } } } /// /// Clears the bitmap with the given color /// /// The color to clear the bitmap with public void Clear(Color color) { Clear(color.ToArgb()); } /// /// Clears the bitmap with the given color /// /// The color to clear the bitmap with public void Clear(int color) { bool unlockAfter = false; if (!Locked) { Lock(); unlockAfter = true; } // Clear all the pixels int count = Width * Height; int* curScan = _scan0; // Uniform color pixel values can be mem-set straight away int component = (color & 0xFF); if (component == ((color >> 8) & 0xFF) && component == ((color >> 16) & 0xFF) && component == ((color >> 24) & 0xFF)) { memset(_scan0, component, (ulong)(Height * Stride * BytesPerPixel)); } else { // Defines the ammount of assignments that the main while() loop is performing per loop. // The value specified here must match the number of assignment statements inside that loop const int assignsPerLoop = 8; int rem = count % assignsPerLoop; count /= assignsPerLoop; while (count-- > 0) { *(curScan++) = color; *(curScan++) = color; *(curScan++) = color; *(curScan++) = color; *(curScan++) = color; *(curScan++) = color; *(curScan++) = color; *(curScan++) = color; } while (rem-- > 0) { *(curScan++) = color; } if (unlockAfter) { Unlock(); } } } /// /// Clears a square region of this image w/ a given color /// /// /// public void ClearRegion(Rectangle region, Color color) { ClearRegion(region, color.ToArgb()); } /// /// Clears a square region of this image w/ a given color /// /// /// public void ClearRegion(Rectangle region, int color) { var thisReg = new Rectangle(0, 0, Width, Height); if (!region.IntersectsWith(thisReg)) return; // If the region covers the entire image, use faster Clear(). if (region == thisReg) { Clear(color); return; } int minX = region.X; int maxX = region.X + region.Width; int minY = region.Y; int maxY = region.Y + region.Height; // Bail out of optimization if there's too few rows to make this worth it if (maxY - minY < 16) { for (int y = minY; y < maxY; y++) { for (int x = minX; x < maxX; x++) { *(_scan0 + x + y * Stride) = color; } } return; } ulong strideWidth = (ulong)region.Width * BytesPerPixel; // Uniform color pixel values can be mem-set straight away int component = (color & 0xFF); if (component == ((color >> 8) & 0xFF) && component == ((color >> 16) & 0xFF) && component == ((color >> 24) & 0xFF)) { for (int y = minY; y < maxY; y++) { memset(_scan0 + minX + y * Stride, component, strideWidth); } } else { // Prepare a horizontal slice of pixels that will be copied over each horizontal row down. var row = new int[region.Width]; fixed (int* pRow = row) { int count = region.Width; int rem = count % 8; count /= 8; int* pSrc = pRow; while (count-- > 0) { *pSrc++ = color; *pSrc++ = color; *pSrc++ = color; *pSrc++ = color; *pSrc++ = color; *pSrc++ = color; *pSrc++ = color; *pSrc++ = color; } while (rem-- > 0) { *pSrc++ = color; } var sx = _scan0 + minX; for (int y = minY; y < maxY; y++) { memcpy(sx + y * Stride, pRow, strideWidth); } } } } /// /// Copies a region of the source bitmap into this fast bitmap /// /// The source image to copy /// The region on the source bitmap that will be copied over /// The region on this fast bitmap that will be changed /// The provided source bitmap is the same bitmap locked in this FastBitmap public void CopyRegion(Bitmap source, Rectangle srcRect, Rectangle destRect) { // Throw exception when trying to copy same bitmap over if (source == _bitmap) { throw new ArgumentException(@"Copying regions across the same bitmap is not supported", nameof(source)); } var srcBitmapRect = new Rectangle(0, 0, source.Width, source.Height); var destBitmapRect = new Rectangle(0, 0, Width, Height); // Check if the rectangle configuration doesn't generate invalid states or does not affect the target image if (srcRect.Width <= 0 || srcRect.Height <= 0 || destRect.Width <= 0 || destRect.Height <= 0 || !srcBitmapRect.IntersectsWith(srcRect) || !destRect.IntersectsWith(destBitmapRect)) return; // Find the areas of the first and second bitmaps that are going to be affected srcBitmapRect = Rectangle.Intersect(srcRect, srcBitmapRect); // Clip the source rectangle on top of the destination rectangle in a way that clips out the regions of the original bitmap // that will not be drawn on the destination bitmap for being out of bounds srcBitmapRect = Rectangle.Intersect(srcBitmapRect, new Rectangle(srcRect.X, srcRect.Y, destRect.Width, destRect.Height)); destBitmapRect = Rectangle.Intersect(destRect, destBitmapRect); // Clip the source bitmap region yet again here srcBitmapRect = Rectangle.Intersect(srcBitmapRect, new Rectangle(-destRect.X + srcRect.X, -destRect.Y + srcRect.Y, Width, Height)); // Calculate the rectangle containing the maximum possible area that is supposed to be affected by the copy region operation int copyWidth = Math.Min(srcBitmapRect.Width, destBitmapRect.Width); int copyHeight = Math.Min(srcBitmapRect.Height, destBitmapRect.Height); if (copyWidth == 0 || copyHeight == 0) return; int srcStartX = srcBitmapRect.Left; int srcStartY = srcBitmapRect.Top; int destStartX = destBitmapRect.Left; int destStartY = destBitmapRect.Top; using (var fastSource = source.FastLock()) { ulong strideWidth = (ulong)copyWidth * BytesPerPixel; // Perform copies of whole pixel rows for (int y = 0; y < copyHeight; y++) { int destX = destStartX; int destY = destStartY + y; int srcX = srcStartX; int srcY = srcStartY + y; long offsetSrc = (srcX + srcY * fastSource.Stride); long offsetDest = (destX + destY * Stride); memcpy(_scan0 + offsetDest, fastSource._scan0 + offsetSrc, strideWidth); } } } /// /// Performs a copy operation of the pixels from the Source bitmap to the Target bitmap. /// If the dimensions or pixel depths of both images don't match, the copy is not performed /// /// The bitmap to copy the pixels from /// The bitmap to copy the pixels to /// Whether the copy proceedure was successful /// The provided source and target bitmaps are the same public static bool CopyPixels(Bitmap source, Bitmap target) { if (source == target) { throw new ArgumentException(@"Copying pixels across the same bitmap is not supported", nameof(source)); } if (source.Width != target.Width || source.Height != target.Height || source.PixelFormat != target.PixelFormat) return false; using (FastBitmap fastSource = source.FastLock(), fastTarget = target.FastLock()) { memcpy(fastTarget.Scan0, fastSource.Scan0, (ulong)(fastSource.Height * fastSource.Stride * BytesPerPixel)); } return true; } /// /// Clears the given bitmap with the given color /// /// The bitmap to clear /// The color to clear the bitmap with public static void ClearBitmap(Bitmap bitmap, Color color) { ClearBitmap(bitmap, color.ToArgb()); } /// /// Clears the given bitmap with the given color /// /// The bitmap to clear /// The color to clear the bitmap with public static void ClearBitmap(Bitmap bitmap, int color) { using (var fb = bitmap.FastLock()) { fb.Clear(color); } } /// /// Copies a region of the source bitmap to a target bitmap /// /// The source image to copy /// The target image to be altered /// The region on the source bitmap that will be copied over /// The region on the target bitmap that will be changed /// The provided source and target bitmaps are the same bitmap public static void CopyRegion(Bitmap source, Bitmap target, Rectangle srcRect, Rectangle destRect) { var srcBitmapRect = new Rectangle(0, 0, source.Width, source.Height); var destBitmapRect = new Rectangle(0, 0, target.Width, target.Height); // If the copy operation results in an entire copy, use CopyPixels instead if (srcBitmapRect == srcRect && destBitmapRect == destRect && srcBitmapRect == destBitmapRect) { CopyPixels(source, target); return; } using (var fastTarget = target.FastLock()) { fastTarget.CopyRegion(source, srcRect, destRect); } } /// /// Returns a bitmap that is a slice of the original provided 32bpp Bitmap. /// The region must have a width and a height > 0, and must lie inside the source bitmap's area /// /// The source bitmap to slice /// The region of the source bitmap to slice /// A Bitmap that represents the rectangle region slice of the source bitmap /// The provided bimap is not 32bpp /// The provided region is invalid public static Bitmap SliceBitmap(Bitmap source, Rectangle region) { if (region.Width <= 0 || region.Height <= 0) { throw new ArgumentException(@"The provided region must have a width and a height > 0", nameof(region)); } var sliceRectangle = Rectangle.Intersect(new Rectangle(Point.Empty, source.Size), region); if (sliceRectangle.IsEmpty) { throw new ArgumentException(@"The provided region must not lie outside of the bitmap's region completely", nameof(region)); } var slicedBitmap = new Bitmap(sliceRectangle.Width, sliceRectangle.Height); CopyRegion(source, slicedBitmap, sliceRectangle, new Rectangle(0, 0, sliceRectangle.Width, sliceRectangle.Height)); return slicedBitmap; } #if NETSTANDARD public static void memcpy(IntPtr dest, IntPtr src, ulong count) { Buffer.MemoryCopy(src.ToPointer(), dest.ToPointer(), count, count); } public static void memcpy(void* dest, void* src, ulong count) { Buffer.MemoryCopy(src, dest, count, count); } public static void memset(void* dest, int value, ulong count) { Unsafe.InitBlock(dest, (byte)value, (uint)count); } #else /// /// .NET wrapper to native call of 'memcpy'. Requires Microsoft Visual C++ Runtime installed /// [DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)] public static extern IntPtr memcpy(IntPtr dest, IntPtr src, ulong count); /// /// .NET wrapper to native call of 'memcpy'. Requires Microsoft Visual C++ Runtime installed /// [DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)] public static extern IntPtr memcpy(void* dest, void* src, ulong count); /// /// .NET wrapper to native call of 'memset'. Requires Microsoft Visual C++ Runtime installed /// [DllImport("msvcrt.dll", EntryPoint = "memset", CallingConvention = CallingConvention.Cdecl, SetLastError = false)] public static extern IntPtr memset(void* dest, int value, ulong count); #endif /// /// Represents a disposable structure that is returned during Lock() calls, and unlocks the bitmap on Dispose calls /// public struct FastBitmapLocker : IDisposable { /// /// Gets the fast bitmap instance attached to this locker /// public FastBitmap FastBitmap { get; } /// /// Initializes a new instance of the FastBitmapLocker struct with an initial fast bitmap object. /// The fast bitmap object passed will be unlocked after calling Dispose() on this struct /// /// A fast bitmap to attach to this locker which will be released after a call to Dispose public FastBitmapLocker(FastBitmap fastBitmap) { FastBitmap = fastBitmap; } /// /// Disposes of this FastBitmapLocker, essentially unlocking the underlying fast bitmap /// public void Dispose() { if (FastBitmap.Locked) FastBitmap.Unlock(); } } } /// /// Describes a pixel format to use when locking a bitmap using . /// public enum FastBitmapLockFormat { /// Specifies that the format is 32 bits per pixel; 8 bits each are used for the red, green, and blue components. The remaining 8 bits are not used. Format32bppRgb = 139273, /// Specifies that the format is 32 bits per pixel; 8 bits each are used for the alpha, red, green, and blue components. The red, green, and blue components are premultiplied, according to the alpha component. Format32bppPArgb = 925707, /// Specifies that the format is 32 bits per pixel; 8 bits each are used for the alpha, red, green, and blue components. Format32bppArgb = 2498570, } /// /// Static class that contains fast bitmap extension methdos for the Bitmap class /// public static class FastBitmapExtensions { /// /// Locks this bitmap into memory and returns a FastBitmap that can be used to manipulate its pixels /// /// The bitmap to lock /// A locked FastBitmap public static FastBitmap FastLock(this Bitmap bitmap) { var fast = new FastBitmap(bitmap); fast.Lock(); return fast; } /// /// Locks this bitmap into memory and returns a FastBitmap that can be used to manipulate its pixels /// /// The bitmap to lock /// The underlying pixel format to use when locking the bitmap /// A locked FastBitmap public static FastBitmap FastLock(this Bitmap bitmap, FastBitmapLockFormat lockFormat) { var fast = new FastBitmap(bitmap); fast.Lock(lockFormat); return fast; } /// /// Returns a deep clone of this Bitmap object, with all the data copied over. /// After a deep clone, the new bitmap is completely independent from the original /// /// The bitmap to clone /// A deep clone of this Bitmap object, with all the data copied over public static Bitmap DeepClone(this Bitmap bitmap) { return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat); } } }