LockBits seems to result in wrong stride when copy

2019-09-21 05:04发布

问题:

I'm trying to copy a simple byte array to an 8bit indexed bitmap. Using the exact same code as shown in countless answered questions on many forums, I still get the wrong result. The data I'm trying to write to image files is 360 bytes, setup as an 18x20 byte linear array. That is, the first 18 bytes (0-17) belong on the top row of the image, the next 18 bytes (18-35) belong on the 2nd row, etc. I have confirmed that this data is correct, as I can manually parse it in Excel (and even visualize it by setting the background color of cells). However, when I try to extract this using code in c#, I get a wrongly formatted image. Here is the code...

public Bitmap CopyByteDataToBitmap(byte[] byteData) {
    Bitmap bmp = new Bitmap(18, 20, PixelFormat.Format8bppIndexed);
    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);
    Marshal.Copy(byteData, 0, bmpData.Scan0, byteData.Length);
    bmp.UnlockBits(bmpData);

    return bmp;
}

The result is as follows. The first row is written correctly. However, the starting with the second row, there is a 2 byte offset. That is, the first byte of the second row of the image ends up being byte #20 instead of byte #18 (starting from 0). Also, if I set a breakpoint immediately after the LockBits call, I can see that the bmpData has a "Stride" property equal to 20... even though the width is clearly set to 18. And, if I manually set the stride to 18, after the LockBits, it has no effect on the returned bitmap. Why is this happening? Please help, thanks.

回答1:

You have to copy it row by row, advancing your read position by the stride used in your image data, and your write position by the stride set in the BitmapData object.

In your case, the input data's stride is just the width, but the BitmapData's stride won't match that since, as TaW said, it's always rounded up to the next multiple of 4 bytes.

Also note, it being an 8-bit image, you need to add the palette, or it'll end up with a standard Windows palette that probably won't match your image's intended colours at all.

/// <summary>
/// Creates a bitmap based on data, width, height, stride and pixel format.
/// </summary>
/// <param name="sourceData">Byte array of raw source data.</param>
/// <param name="width">Width of the image.</param>
/// <param name="height">Height of the image.</param>
/// <param name="stride">Scanline length inside the data.</param>
/// <param name="pixelFormat">Pixel format.</param>
/// <param name="palette">Color palette.</param>
/// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
/// <returns>The new image.</returns>
public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
{
    Bitmap newImage = new Bitmap(width, height, pixelFormat);
    BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
    Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
    Int32 targetStride = targetData.Stride;
    Int64 scan0 = targetData.Scan0.ToInt64();
    for (Int32 y = 0; y < height; ++y)
        Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
    newImage.UnlockBits(targetData);
    // For indexed images, set the palette.
    if ((pixelFormat & PixelFormat.Indexed) != 0 && (palette != null || defaultColor.HasValue))
    {
        if (palette == null)
            palette = new Color[0];
        ColorPalette pal = newImage.Palette;
        Int32 palLen = pal.Entries.Length;
        Int32 paletteLength = palette.Length;
        for (Int32 i = 0; i < palLen; ++i)
        {
            if (i < paletteLength)
                pal.Entries[i] = palette[i];
            else if (defaultColor.HasValue)
                pal.Entries[i] = defaultColor.Value;
            else
                break;
        }
        // Palette property getter creates a copy, so the newly filled in palette is
        // not actually referenced in the image until you set it again explicitly.
        newImage.Palette = pal;
    }
    return newImage;
}