2

I have bitmap extracted from BitmapSource (RenderTargetBitmap) with blue circle in it. RenderTargetBitmap is created with PixelFormats.Pbgra32.

PixelFormats Pbgra32 pre-multiplies each color channel with alpha value. So, when I try to convert bitmap to cursor I was getting less opaque image than is should have.

I found solution to the problem here which clone the bitmap to Format24bppRgb and manually set R,B,G and alpha values. However, solutions works perfectly fine but for cloned bitmap I see black border around visual.

Can I get rid of that black border in cloned bitmap? (I suspect it's something inside SafeCopy method)

Methods used from the link are:

private static void SafeCopy(BitmapData srcData, BitmapData dstData, byte alphaLevel)
{
  for (int y = 0; y < srcData.Height; y++)
    for (int x = 0; x < srcData.Width; x++)
    {
       byte b = Marshal.ReadByte(srcData.Scan0, y * srcData.Stride + x * 3);
       byte g = Marshal.ReadByte(srcData.Scan0, y * srcData.Stride + x * 3 + 1);
       byte r = Marshal.ReadByte(srcData.Scan0, y * srcData.Stride + x * 3 + 2);

       Marshal.WriteByte(dstData.Scan0, y * dstData.Stride + x * 4, b);
       Marshal.WriteByte(dstData.Scan0, y * dstData.Stride + x * 4 + 1, g);
       Marshal.WriteByte(dstData.Scan0, y * dstData.Stride + x * 4 + 2, r);
       Marshal.WriteByte(dstData.Scan0, y * dstData.Stride + x * 4 + 3, alphaLevel);
    }
}

private static Cursor CreateCustomCursorInternal(Bitmap bitmap, double opacity)
{
    Bitmap cursorBitmap = null;
    IconInfo iconInfo = new IconInfo();
    Rectangle rectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height);

    try
    {
        byte alphaLevel = System.Convert.ToByte(byte.MaxValue * opacity);

        // Here, the pre-multiplied alpha channel is specified
        cursorBitmap = new Bitmap(bitmap.Width, bitmap.Height, 
                                   PixelFormat.Format32bppPArgb);

        // Assuming the source bitmap can be locked in a 24 bits per pixel format
        BitmapData bitmapData = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, 
                                                PixelFormat.Format24bppRgb);
        BitmapData cursorBitmapData = cursorBitmap.LockBits(rectangle, 
                               ImageLockMode.WriteOnly, cursorBitmap.PixelFormat);

        // Use SafeCopy() to set the bitmap contents
        SafeCopy(bitmapData, cursorBitmapData, alphaLevel);

        cursorBitmap.UnlockBits(cursorBitmapData);
        bitmap.UnlockBits(bitmapData);

        .......
}

Original bitmap:

enter image description here

Cloned bitmap:

enter image description here

Community
  • 1
  • 1
Rohit Vats
  • 79,502
  • 12
  • 161
  • 185
  • 1
    By converting to 24bpp RGB you've removed the alpha channel, i.e. you removed the transparent background. So what do you expect the background color to be instead? – Clemens Aug 15 '14 at 17:07
  • No `cursorBitmap` (cloned bitmap) is still `Format32bppPArgb`. It's just I locked the source bitmap on 24bpp to avoid pre-multiplication bits and then manually set R,G,B and alpha value on cursorBitmap in SafeCopy method from source bitmap. So, the cloned bitmap still have alpha value. – Rohit Vats Aug 15 '14 at 17:11
  • Sure, but you are (wrongly) applying the same alpha value to all pixels. But it has to be `0` for the fully transparent background pixels. Instead of converting to RGB you could simply revert the pre-multiplication by multiplying each pixel R, G, and B value by `255d/(double)A`. – Clemens Aug 15 '14 at 17:14
  • Because I want to apply opacity to each pixel so that transparency is consistent for all pixels. Isn't it? `Instead of converting to RGB you could simply revert the pre-multiplication by multiplying each pixel R, G, and B value by 255d/(double)A.`- I didn't get this. You mean I should remove the pre-multiplication and then set alpha value to all pixels? By locking the bit on format Format24bppRgb, will remove the pre-multiplication. I guess so but might be wrong. – Rohit Vats Aug 15 '14 at 17:19
  • 1
    You want to apply opacity to all pixels, sure. But the pixels of the transparent background (outside the circle) have an alpha value (i.e. an opacity) of `0` in the original image. You won't want to change that. They should keep that value. But by converting to 24bpp RGB you effectively remove all alpha values, which leaves you with black background pixels, or whatever (invisible) color the backgound pixels had. Again, converting to 24bpp is wrong. It effectively removes the transparency of the background, which is not what you want. – Clemens Aug 15 '14 at 17:27
  • Instead of doing that, keep a 32bpp ARGB format, multiply each pixel's R, G, and B values by 255 / A (except when A is zero), which reverts the pre-multiplication, and keep the alpha value as it is. – Clemens Aug 15 '14 at 17:30
  • 1
    Or even simpler, convert to `Format32bppArgb` instead of `Format24bppRgb` and then apply your global alpha value. That might already fix everything. – Clemens Aug 15 '14 at 17:34
  • @Clemens - You are right if I skip setting alpha value for pixels which already have alpha channel set to 0. I was able to get rid of it. I put the check before setting alpha channel that if alpha value is 0 do not set it. – Rohit Vats Aug 15 '14 at 17:58
  • But for your last comment of converting it to `Format32bppArgb`. Even after that I have to copy pixels from source to destination becuase I don't see any method in bitMap to change its format in place :( Do you know any straight forward way? Also can you post the skipping setting alpha channel for 0 value as a answer so that I can accept it. – Rohit Vats Aug 15 '14 at 17:59

1 Answers1

2

The simplest way to convert a WPF 32bit PBGRA bitmap to a WinForms PARGB bitmap and at the same time apply a global opacity seems to be just multiplying all A, R, G and B values with the opacity factor (a float value between 0 and 1) like in the method shown below. However, I would have expected that it would also be necessary to swap the bytes, but apparently it isn't.

private static void CopyBufferWithOpacity(byte[] sourceBuffer,
    System.Drawing.Imaging.BitmapData targetBuffer, double opacity)
{
    for (int i = 0; i < sourceBuffer.Length; i++)
    {
        sourceBuffer[i] = (byte)Math.Round(opacity * sourceBuffer[i]);
    }

    Marshal.Copy(sourceBuffer, 0, targetBuffer.Scan0, sourceBuffer.Length);
}

Given a 32bit PBGRA bitmap pbgraBitmap (e.g. a RenderTargetBitmap), you would use the method like this:

var width = pbgraBitmap.PixelWidth;
var height = pbgraBitmap.PixelHeight;
var stride = width * 4;
var buffer = new byte[stride * height];
pbgraBitmap.CopyPixels(buffer, stride, 0);

var targetFormat = System.Drawing.Imaging.PixelFormat.Format32bppPArgb;
var bitmap = new System.Drawing.Bitmap(width, height, targetFormat);
var bitmapData = bitmap.LockBits(
    new System.Drawing.Rectangle(0, 0, width, height),
    System.Drawing.Imaging.ImageLockMode.WriteOnly,
    targetFormat);

CopyBufferWithOpacity(buffer, bitmapData, 0.6);

bitmap.UnlockBits(bitmapData);
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • +1. Thanks Clemens. One quick question - `bgraBitmap` I get is BitmapSource. For cursor I need Bitmap, so do I need to create Bitmap now at `Format24bppRgb` or at `Format32bppPArgb`? – Rohit Vats Aug 16 '14 at 17:22
  • From your comments I fixed my problem by locking source bitmap in format `Format32bppArgb` and then copy individual RGB values + my alpha value to destination bitmap except for pixel with alpha set to 0. So, was just curious for this approach you mentioned in the answer above. – Rohit Vats Aug 16 '14 at 17:27
  • You should create the cursor bitmap with the same format as `bgraBitmap`, which would be `Format32bppRgba`. Unfortunately I just realized that there is no such format. So you might also need to swap bytes in the pixel buffer and then use `Format32bppArgb`. – Clemens Aug 16 '14 at 17:50
  • Ideally shouldn't it be `Format32bppBgra` because we converted to Bgra format? – Rohit Vats Aug 16 '14 at 18:01
  • Ok. Also I was thinking that is there any difference in these two approaches - 1) Removing pre-multiplication bit using your formula and generating bitmapSource again using format Bgra32. 2) Create FormatConvertedBitmap with source set to PBgra32 and set it's pixel format to Bgra32. – Rohit Vats Aug 16 '14 at 18:23
  • @Rohit Please see my edited answer. The method now copies directly from WPF PBGRA to WinForms PARGB. In my tests it does not reproduce the color/opacity exactly, but it seems the best I can get. I also tried to create a WinForms ARGB bitmap directly, but that didn't work at all. Transparency got lost entirely. – Clemens Aug 16 '14 at 19:11
  • Your updated method works perfectly to convert WPF bitmap to WinForms bitmap. But opacity issue still remains because what I feel is we convert from PBgra to PArgb so with this pre-multiplication bits still remains in bitmap which results in more transparency compared to actual visual drawn on canvas. Anyhow you have already guided to me the correct solution. Thanks a lot for your time and effort. Really appreciate it. :) Learned a lot about bitmaps and felt WPF should have provided a method to convert to traditional GDI's bitmaps. – Rohit Vats Aug 16 '14 at 19:35
  • I just tried with another color than pure blue and realized that it isn't even necessary to swap the bytes. I have to admit that I find this pretty confusing, because the color value order should simply be different between the two pixel formats. I will continue my investigations. – Clemens Aug 16 '14 at 20:25
  • Please see my edited answer again. I have still no idea why the resulting opacity differs from what we expect. When I set a value of e.g. `0.6` that resembles more or less the original shape with an opacity of `0.5`. Maybe that's simply because the underlying graphics system is something completely different (DirectX for WPF, GDI+ for WinForms). – Clemens Aug 16 '14 at 20:43
  • Yes issue is not in bitmap but when we generate cursor from bitmap using interop methods like described [here](http://tech.pro/tutorial/751/wpf-tutorial-how-to-use-custom-cursors). And in another post [here](http://stackoverflow.com/a/1247951/632337) it's mentioned that for pre-multiplied bitmaps, issue is somewhere in GDI or in cursor. So, I was able to achieve it by using your logic for generating correct bitmap and using the code from there to generate cursor with correct opacity. I also found it amusing bytes order need not to be in same order. – Rohit Vats Aug 17 '14 at 06:27