I managed to resolve this issue without creating entirely new Bitmap object, by using LockBits()
with ImageLockMode.UserInputBuffer
.
Attached the code I came up with, feel free to use.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
public static class BitmapExtensions
{
public static ProperBitmapData LockBitsProper(this Bitmap bitmap, ImageLockMode flags)
{
Rectangle bitmapBounds = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
return bitmap.LockBitsProper(bitmapBounds, flags, bitmap.PixelFormat);
}
public static ProperBitmapData LockBitsProper(this Bitmap bitmap, Rectangle rect, ImageLockMode flags, PixelFormat format)
{
BitmapData bmpData = bitmap.LockBits(rect, flags, format);
int byteCount;
try
{
byteCount = Math.Abs(bmpData.Stride) * bmpData.Height;
if (bmpData.Stride > 0) return new ProperBitmapData(bitmap, bmpData, byteCount, IntPtr.Zero);
}
catch
{
bitmap.UnlockBits(bmpData);
throw;
}
// in case Stride is negative
bitmap.UnlockBits(bmpData);
// When Stride is negative, the LockBits locks the wrong memory area which results in AccessViolationException even when reading the right place
// starting with Scan0 + (Height - 1) * Stride (also not properly documented).
// This is a workaround to it using a user allocated area overload.
// For some reason, in Windows Vista (SP0) Stride is (almost?) always negative, while in >=Windows 7 it is positive more often.
// Some useful documentation: https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nf-gdiplusheaders-bitmap-lockbits
IntPtr userAllocatedArea = Marshal.AllocHGlobal(byteCount);
try
{
// Actually when Stride is negative, Scan0 have to point to where the last row will be written.
// This is not properly documented anywhere, and discovered just by trial and error.
bmpData.Scan0 = (IntPtr)((long)userAllocatedArea - (bmpData.Height - 1) * bmpData.Stride);
bmpData = bitmap.LockBits(rect, ImageLockMode.UserInputBuffer | flags, format, bmpData);
try
{
return new ProperBitmapData(bitmap, bmpData, byteCount, userAllocatedArea);
}
catch
{
bitmap.UnlockBits(bmpData);
throw;
}
}
catch
{
Marshal.FreeHGlobal(userAllocatedArea);
throw;
}
}
}
public class ProperBitmapData : IDisposable
{
private Bitmap _bitmap;
private BitmapData _bitmapData;
private int _byteCount;
private IntPtr _userAllocatedBuffer;
public int Width => _bitmapData.Width;
public int Height => _bitmapData.Height;
public int Stride => _bitmapData.Stride;
public PixelFormat PixelFormat => _bitmapData.PixelFormat;
public IntPtr Scan0 => (_userAllocatedBuffer != IntPtr.Zero) ? _userAllocatedBuffer : _bitmapData.Scan0;
public int Reserved => _bitmapData.Reserved;
public int ByteCount => _byteCount;
public ProperBitmapData(Bitmap bitmap, BitmapData bitmapData, int byteCount, IntPtr userAllocatedBuffer)
{
_bitmap = bitmap;
_bitmapData = bitmapData;
_byteCount = byteCount;
_userAllocatedBuffer = userAllocatedBuffer;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
_bitmap?.UnlockBits(_bitmapData);
_bitmap = null;
_bitmapData = null;
if (_userAllocatedBuffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(_userAllocatedBuffer);
_userAllocatedBuffer = IntPtr.Zero;
}
}
~ProperBitmapData()
{
Dispose(false);
}
}
Example of usage:
using (ProperBitmapData bmpData = bitmap.LockBitsProper(ImageLockMode.ReadOnly))
{
// Beware that bmpData.Scan0 here always points to the start of the allocated memory block.
this.data = new byte[bmpData.ByteCount];
Marshal.Copy(bmpData.Scan0, data, 0, bmpData.ByteCount);
}