8

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??

Pedro77
  • 5,176
  • 7
  • 61
  • 91

6 Answers6

9

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.

Rick Brewster
  • 3,374
  • 1
  • 17
  • 21
  • 1
    Code based on Rick Brewster answer is available on [Gist] (https://gist.github.com/tkouba/0b7f8496f1aadcfee2db). Simply: RowSize = bitmapData.Stride < 0 ? -bitmapData.Stride : bitmapData.Stride; for (int y = 0; y < Height; y++) { Marshal.Copy(IntPtr.Add(Iptr, y * bitmapData.Stride), Pixels, y * RowSize, RowSize); } – Arci Feb 07 '16 at 21:39
9

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

Community
  • 1
  • 1
Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • ok ok, it answers this question. But I don't want to copy line by line... I want to avoid the negative stride to ever happen. This is why I've created the new question (http://stackoverflow.com/questions/17115555/converting-24bpp-to-8bpp-how-can-i-fix-this-method-so-it-returns-a-bitmap-with) – Pedro77 Jun 14 '13 at 19:44
  • (I'm working in C++, not C#, but I'm guessing this is still useful.) `LockBits` itself can copy the data for you if you include the `ImageLockModeUserInputBuf` bit as one of the flags. For this to work, the stride should be negative and `Scan0` should be `data - (Height - 1) * Stride`. (Note the "minus" actually increases the pointer since `Stride < 0`.) – Tyler May 28 '14 at 15:07
5

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);
        }
JamieSee
  • 12,696
  • 2
  • 31
  • 47
  • Yes.. It is a possible solution. – Pedro77 Apr 27 '12 at 12:56
  • 1
    From memory, I think this should help. You have to clone the source bitmap. I used to write the results out directly with the unsafe code from one bitmap to a new bitmap with the desired pixel format etc. – Zac May 03 '12 at 09:00
  • I'm back to this problem and, no, it is now a solution because it will result in a 32bpp image!! The point it to get from 24bpp to 8bpp! – Pedro77 Jun 14 '13 at 18:54
1

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

Patrick Hughes
  • 345
  • 2
  • 8
  • The stride that you record into this.stride is the absolute value of the bmpData.Stride property. Old Windows APIs were pretty bad about stuffing extra info into data like that. – Patrick Hughes Jul 26 '11 at 19:09
  • I found one aswer here: http://stackoverflow.com/questions/4816956/accessviolation-when-using-lockbits. But it loops thru all image height. Does anyone have a better solution? – Pedro77 Jul 26 '11 at 19:13
  • Why the solution "2" turn the stride negative?? – Pedro77 Jul 26 '11 at 19:16
  • Setting the high bit of the integer is just Microsoft's way of adding that "I'm upside down" flag to the bitmap structure without adding any more space. BMPs are very old and space used to be very important to save. Ever since then bitmap code has had to worry about it. – Patrick Hughes Jul 26 '11 at 19:23
  • Ok, and how can I copy the pixel data from this bitmap to a byte array? – Pedro77 Jul 26 '11 at 19:33
  • 1
    I dont know how vote this as an answer, but it does not answer my question. Only ctrl+c ctrl+v of the stride definition that I already know! – Pedro77 Jul 28 '11 at 13:29
1

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);
}
DxCK
  • 4,402
  • 7
  • 50
  • 89
-2

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...).

Ben F
  • 336
  • 1
  • 10
  • 2
    This isn't it. Have another look at his code. He's trying to apply the -length to change a negative into a positive, only if the resulting length from a negative stride is less than 0. – JamieSee May 03 '12 at 15:09