Reputation: 5294
I was looking for the fastest way to convert a Bitmap to 8bpp. I found 2 ways:
1.
public static System.Drawing.Image ConvertTo8bpp(Bitmap oldbmp)
{
using (var ms = new MemoryStream())
{
oldbmp.Save(ms, ImageFormat.Gif);
ms.Position = 0;
return System.Drawing.Image.FromStream(ms);
}
}
2. http://www.wischik.com/lu/programmer/1bpp.html
But: 1. Results in a very low quality result (bad pallet)
and 2 gives me a Bitmap with negative stride, when I try to lockbits and copy the data to a byte array I get an exception: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, bmp.PixelFormat);
this.stride = bmpData.Stride;
this.bytesPerPixel = GetBytesPerPixel(bmp.PixelFormat);
int length = bmpData.Stride * bmp.Height;
if (this.stride < 0)
this.data = new byte[-length];
else
this.data = new byte[length];
Marshal.Copy(bmpData.Scan0, data, 0, length);
//Unlock the bitmap
bmp.UnlockBits(bmpData);
How can I make 2 gives a positive stride? Or how can I copy data using lockbits of a negative stride??
Upvotes: 7
Views: 6400
Reputation: 4552
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);
}
Upvotes: 1
Reputation: 133995
The problem here is that Scan0
points to the beginning of the first scan line, not the beginning of the first byte of data. In a bottom-up bitmap, the first scan line is Stride
bytes from the end of the bitmap data.
When you call Marshal.Copy
to copy the data from Scan0
, it tries to copy (Height*Stride)
bytes, starting from position ((Height-1)*Stride)
. Clearly, that's going to run off into the weeds.
If you just want to copy the bitmap data, you have to calculate the starting address with Scan0 - (Height-1)*Stride
. That will start you at the beginning of the bitmap data. You can pass that computed address to Marshal.Copy
.
If you want to copy the scan lines in order (i.e. top, next, next, ... bottom), then you have to copy a line at a time: copy Stride
bytes from Scan0
, then add Stride
(which is negative), copy that line, etc. Rick Brewster had the right answer there: https://stackoverflow.com/a/10360753/56778
Upvotes: 8
Reputation: 336
I'm guessing the exception you're getting is due to
this.data = new byte[-length];
And then trying to copy data into a byte array of negative size (I don't see how that even compiles really...).
Upvotes: -2
Reputation: 13010
I don't know why there is something strange about the Bitmap created by the FromHbitmap
method, but I do know that you can fix it by using Bitmap bmpClone = (Bitmap)bmp.Clone();
and doing the LockBits on bmpClone.
Also, I found that if you use bmp.Clone()
, you cannot Dispose() of bmp until you have finished with the clone.
This also works and let's you dispose of the negative stride image sooner rather than later:
Bitmap bmp = null;
using (Bitmap bmpT = CopyToBpp(bmpO, 1))
{
bmp = new Bitmap(bmpT);
}
Upvotes: 5
Reputation: 3494
Copy 1 row at a time, calculating the starting pointer for a row as ((byte*)scan0 + (y * stride))
. The code will be identical for either positive or negative stride.
Upvotes: 9
Reputation: 357
From the C# documentation on BitmapData: The stride is the width of a single row of pixels (a scan line), rounded up to a four-byte boundary. If the stride is positive, the bitmap is top-down. If the stride is negative, the bitmap is bottom-up
Upvotes: 3