4

I have a 8bpp indexed Bitmap with a custom 256 color Palette where specific colors in the palette (Color.Pink and Color.Green) indicate transparency.
I could use the MakeTransparent(color) method (twice for each color) on a bitmap but it converts it to 32bpp. So instead I'm using:

using var imageAttr = new ImageAttributes();
imageAttr.SetColorKey(pink, pink, ColorAdjustType.Default);

and then

g.DrawImage(bitmap, destRect, X, Y, Width, Height, GraphicsUnit.Pixel, imageAttr);

which draws the bitmap as it should, but only makes changes Color.Pink to a transparent Color. How can I do this also for the second color (Color.Green)?

Jimi
  • 29,621
  • 8
  • 43
  • 61
Kazgaa
  • 39
  • 4

1 Answers1

6

Both ImageAttributes.SetColorKey() and Bitmap.MakeTransparent are not the first choices when you need to remap indexed Colors of a Bitmap Palette: the former can only set one Color at a time, the latter transforms the original Image in a 32bpp image.

You need to either change the Indexed Image ColorPalette or draw a new Bitmap using the ImageAttributes.SetRemapTable() method. This method accepts an array of ColorMap objects. A ColorMap is used to specify a new Color that replaces an old Color when drawing a Bitmap.


Let's make a sample 8bpp Image and apply a partial Color Palette, then use Image.LockBits to parse the BitmapData and apply these Colors to a set of 3x3 rectangles:

var image = new Bitmap(12, 12, PixelFormat.Format8bppIndexed);
var palette = image.Palette;  // Copy the Palette entries
palette.Entries[0] = Color.SteelBlue;
palette.Entries[1] = Color.Pink;
palette.Entries[2] = Color.Red;
palette.Entries[3] = Color.Orange;
palette.Entries[4] = Color.YellowGreen;
palette.Entries[5] = Color.Khaki;
palette.Entries[6] = Color.Green;
palette.Entries[7] = Color.LightCoral;
palette.Entries[8] = Color.Maroon;
image.Palette = palette;  // Sets back the modified palette

var data = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.WriteOnly, image.PixelFormat);

int rectsCount = 3;                   // Generate 3 Rectangles per line...
int step = data.Stride / rectsCount;  // ...of this size (single dimension, since w=h here)
int colorIdx = 0, col = 0;            // Color and Column positions

byte[] buffer = new byte[Math.Abs(data.Stride) * image.Height];

for (int i = 1; i <= rectsCount; i++) {
    for (int y = 0; y < data.Height; y++) {
        for (int x = col; x < (step * i); x++) {
            buffer[x + y * data.Stride] = (byte)colorIdx;
        }
        colorIdx += (y + 1) % step == 0 ? 1 : 0;
    }
    col += step;
}

Marshal.Copy(buffer, 0, data.Scan0, buffer.Length);
image.UnlockBits(data);

Which generates this interesting Image (zoomed x25):

ImageAttributes SetColorMap Original Colors

Now, you want to make transparent two of the Colors in the Palette: Color.Pink and Color.Green.
You can build an array of ColorMap objects that specify which new Colors replace existing Colors:

var mapPink = new ColorMap() { OldColor = Color.Pink, NewColor = Color.Transparent };
var mapGreen = new ColorMap() { OldColor = Color.Green, NewColor = Color.Transparent };
var colorMap = new ColorMap[] { mapPink, mapGreen };

Then:

  • Either replace each Color in the Image Palette with the new mapped Color:
    (Note that I'm not passing the [Image].Palette object directly, I'm using the copy of the Palette created before (var palette = image.Palette;): if you pass the Image Palette directly, the changes are not registered)
var palette = image.Palette;
image.Palette = RemapImagePalette(palette, colorMap);

// [...]

private ColorPalette RemapImagePalette(ColorPalette palette, ColorMap[] colorMaps)
{
    for (int i = 0; i < palette.Entries.Length; i++) {
        foreach (ColorMap map in colorMaps) {
            if (palette.Entries[i] == map.OldColor) {
                palette.Entries[i] = map.NewColor;
            }
        }
    }
    return palette;
}
  • Or generate a new Bitmap, using the ImageAttributes.SetRemapTable() method and draw the Indexed Image with the new Color Map using the Graphics.DrawImage() method that accepts an ImageAttributes argument:
// Draws the 12x12 indexed 8bpp Image to a new 300x300 32bpp Bitmap
Bitmap remappedImage = ImageRemapColors(image, new Size(300, 300), colorMap);

// [...]

private Bitmap ImageRemapColors(Image image, Size newSize, ColorMap[] map)
{
    var bitmap = new Bitmap(newSize.Width, newSize.Height);

    using (var g = Graphics.FromImage(bitmap))
    using (var attributes = new ImageAttributes()) {
        if (map != null) attributes.SetRemapTable(map);

        g.InterpolationMode = InterpolationMode.NearestNeighbor;
        g.PixelOffsetMode = PixelOffsetMode.Half;
        g.DrawImage(image, new Rectangle(Point.Empty, newSize),
                    0, 0, image.Width, image.Height, GraphicsUnit.Pixel, attributes);
    }
    return bitmap;
}

These methods generates the same output.

  • One can be used to change the Palette of an indexed Image format.
  • The other to present the Image with remapped Colors in a device context (assigning the Bitmap to the Image property of a Control, for example).
  • [Extra] Another option is to use a TextureBrush as shown here:
    How to draw a transparent shape over an Image

ImageAttributes SetColorMap Remapped Colors

Jimi
  • 29,621
  • 8
  • 43
  • 61
  • In much less words: take the palette from the image (an operation that automatically makes a copy of the palette), find the indices of `Color.Pink` and `Color.Green` on it and set them both to Color.Transparent, and then re-assign the modified palette to the image. – Nyerguds Jan 04 '21 at 08:01