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);
}
}
}