3
votes

I'm trying to Resizing a Transparent png and saving it as a single frame gif image. Let skip resizing part, while you try to save a Transparent png as gif, you will see a black background in output gif:

Bitmap n = new Bitmap(targetPngPath);
n.Save(@"C:\1.gif", ImageFormat.Gif);

yes, I can make that black background into white, but it is not what I'm looking for. Even I can remove the black color using MakeTransparent Method, But it will remove about every black color in image and we will not have a standard transparent image.

we also can do a TRICK saving gif image, we keep extension in filename but we will save it as PNG Format like this:

n.Save(@"C:\1.gif", ImageFormat.Png);

But it is not also standard. So is there any way to safely save a transparent png as a gif image with transparency?

PNG = enter image description here GIF = enter image description here

GIF Saved With Photoshop = enter image description here

2
black background in output gif - does it comes from loading, resizing or saving?Sinatr
black background will come after savingInside Man
Technically GIF transparency means that one single color (from maximum of 256) is considered transparent. PNG transparency is different - it contains alpha (transparency) layer. If you could find color, not used in GIF, then replace PNG transparent parts with this color, make it transparent and save, then you should succeed.Arvo
I think you are wrong, it become black when you convert png into bitmap (which is loading). A dumb solution could be to use as background some color, which is not present in image, and then MakeTransparent that color before saving.Sinatr
The "trick" there isn't really a trick, because it's still a PNG in every way.Jon Hanna

2 Answers

9
votes

This is because the built-in GIF encoder cannot handle the source well, unless it is already a 8 bpp image. You must convert your PNG image to a 256 color image first, then you can save it correctly with the GIF encoder.

public static void SaveGif(string fileName, Image image)
{
    int bpp = Image.GetPixelFormatSize(image.PixelFormat);
    if (bpp == 8)
    {
        image.Save(fileName, ImageFormat.Gif);
        return;
    }

    // 1 and 4 bpp images are need to be converted, too; otherwise, gif encoder encodes the image from 32 bpp image resulting 256 color, no transparency
    if (bpp < 8)
    {
        using (Image image8Bpp = ConvertPixelFormat(image, PixelFormat.Format8bppIndexed, null))
        {
            image8Bpp.Save(fileName, ImageFormat.Gif);
            return;
        }
    }

    // high/true color bitmap: obtaining the colors
    // Converting always to 8 bpp pixel format; otherwise, gif encoder  would convert it to 32 bpp first.
    // With 8 bpp, gif encoder will preserve transparency and will save compact palette
    // Note: This works well for 256 color images in a 32bpp bitmap. Otherwise, you might try to pass null as palette so a default palette will be used.
    Color[] palette = GetColors((Bitmap)image, 256);
    using (Image imageIndexed = ConvertPixelFormat(image, PixelFormat.Format8bppIndexed, palette))
    {
        imageIndexed.Save(fileName, ImageFormat.Gif);
    }
}

// TODO: Use some quantizer
private static Color[] GetColors(Bitmap bitmap, int maxColors)
{
    if (bitmap == null)
        throw new ArgumentNullException("bitmap");
    if (maxColors < 0)
        throw new ArgumentOutOfRangeException("maxColors");

    HashSet<int> colors = new HashSet<int>();
    PixelFormat pixelFormat = bitmap.PixelFormat;
    if (Image.GetPixelFormatSize(pixelFormat) <= 8)
        return bitmap.Palette.Entries;

    // 32 bpp source: the performant variant
    if (pixelFormat == PixelFormat.Format32bppRgb ||
        pixelFormat == PixelFormat.Format32bppArgb ||
        pixelFormat == PixelFormat.Format32bppPArgb)
    {
        BitmapData data = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), ImageLockMode.ReadOnly, pixelFormat);
        try
        {
            unsafe
            {
                byte* line = (byte*)data.Scan0;
                for (int y = 0; y < data.Height; y++)
                {
                    for (int x = 0; x < data.Width; x++)
                    {
                        int c = ((int*)line)[x];
                        // if alpha is 0, adding the transparent color
                        if ((c >> 24) == 0)
                            c = 0xFFFFFF;
                        if (colors.Contains(c))
                            continue;

                        colors.Add(c);
                        if (colors.Count == maxColors)
                            return colors.Select(Color.FromArgb).ToArray();
                    }

                    line += data.Stride;
                }
            }
        }
        finally
        {
            bitmap.UnlockBits(data);
        }
    }
    else
    {
        // fallback: getpixel
        for (int y = 0; y < bitmap.Height; y++)
        {
            for (int x = 0; x < bitmap.Width; x++)
            {
                int c = bitmap.GetPixel(x, y).ToArgb();
                if (colors.Contains(c))
                    continue;

                colors.Add(c);
                if (colors.Count == maxColors)
                    return colors.Select(Color.FromArgb).ToArray();
            }
        }
    }

    return colors.Select(Color.FromArgb).ToArray();
}

private static Image ConvertPixelFormat(Image image, PixelFormat newPixelFormat, Color[] palette)
{
    if (image == null)
        throw new ArgumentNullException("image");

    PixelFormat sourcePixelFormat = image.PixelFormat;

    int bpp = Image.GetPixelFormatSize(newPixelFormat);
    if (newPixelFormat == PixelFormat.Format16bppArgb1555 || newPixelFormat == PixelFormat.Format16bppGrayScale)
        throw new NotSupportedException("This pixel format is not supported by GDI+");

    Bitmap result;

    // non-indexed target image (transparency preserved automatically)
    if (bpp > 8)
    {
        result = new Bitmap(image.Width, image.Height, newPixelFormat);
        using (Graphics g = Graphics.FromImage(result))
        {
            g.DrawImage(image, 0, 0, image.Width, image.Height);
        }

        return result;
    }

    int transparentIndex;
    Bitmap bmp;

    // indexed colors: using GDI+ natively
    RGBQUAD[] targetPalette = new RGBQUAD[256];
    int colorCount = InitPalette(targetPalette, bpp, (image is Bitmap) ? image.Palette : null, palette, out transparentIndex);
    BITMAPINFO bmi = new BITMAPINFO();
    bmi.icHeader.biSize = (uint)Marshal.SizeOf(typeof(BITMAPINFOHEADER));
    bmi.icHeader.biWidth = image.Width;
    bmi.icHeader.biHeight = image.Height;
    bmi.icHeader.biPlanes = 1;
    bmi.icHeader.biBitCount = (ushort)bpp;
    bmi.icHeader.biCompression = BI_RGB;
    bmi.icHeader.biSizeImage = (uint)(((image.Width + 7) & 0xFFFFFFF8) * image.Height / (8 / bpp));
    bmi.icHeader.biXPelsPerMeter = 0;
    bmi.icHeader.biYPelsPerMeter = 0;
    bmi.icHeader.biClrUsed = (uint)colorCount;
    bmi.icHeader.biClrImportant = (uint)colorCount;
    bmi.icColors = targetPalette;

    bmp = (image as Bitmap) ?? new Bitmap(image);

    // Creating the indexed bitmap
    IntPtr bits;
    IntPtr hbmResult = CreateDIBSection(IntPtr.Zero, ref bmi, DIB_RGB_COLORS, out bits, IntPtr.Zero, 0);

    // Obtaining screen DC
    IntPtr dcScreen = GetDC(IntPtr.Zero);

    // DC for the original hbitmap
    IntPtr hbmSource = bmp.GetHbitmap();
    IntPtr dcSource = CreateCompatibleDC(dcScreen);
    SelectObject(dcSource, hbmSource);

    // DC for the indexed hbitmap
    IntPtr dcTarget = CreateCompatibleDC(dcScreen);
    SelectObject(dcTarget, hbmResult);

    // Copy content
    BitBlt(dcTarget, 0, 0, image.Width, image.Height, dcSource, 0, 0, 0x00CC0020 /*TernaryRasterOperations.SRCCOPY*/);

    // obtaining result
    result = Image.FromHbitmap(hbmResult);
    result.SetResolution(image.HorizontalResolution, image.VerticalResolution);

    // cleanup
    DeleteDC(dcSource);
    DeleteDC(dcTarget);
    ReleaseDC(IntPtr.Zero, dcScreen);
    DeleteObject(hbmSource);
    DeleteObject(hbmResult);

    ColorPalette resultPalette = result.Palette;
    bool resetPalette = false;

    // restoring transparency
    if (transparentIndex >= 0)
    {
        // updating palette if transparent color is not actually transparent
        if (resultPalette.Entries[transparentIndex].A != 0)
        {
            resultPalette.Entries[transparentIndex] = Color.Transparent;
            resetPalette = true;
        }

        ToIndexedTransparentByArgb(result, bmp, transparentIndex);
    }

    if (resetPalette)
        result.Palette = resultPalette;

    if (!ReferenceEquals(bmp, image))
        bmp.Dispose();
    return result;
}

private static int InitPalette(RGBQUAD[] targetPalette, int bpp, ColorPalette originalPalette, Color[] desiredPalette, out int transparentIndex)
{
    int maxColors = 1 << bpp;

    // using desired palette
    Color[] sourcePalette = desiredPalette;

    // or, using original palette if it has fewer or the same amount of colors as requested
    if (sourcePalette == null && originalPalette != null && originalPalette.Entries.Length > 0 && originalPalette.Entries.Length <= maxColors)
        sourcePalette = originalPalette.Entries;

    // or, using default system palette
    if (sourcePalette == null)
    {
        using (Bitmap bmpReference = new Bitmap(1, 1, GetPixelFormat(bpp)))
        {
            sourcePalette = bmpReference.Palette.Entries;
        }
    }

    // it is ignored if source has too few colors (rest of the entries will be black)
    transparentIndex = -1;
    bool hasBlack = false;
    int colorCount = Math.Min(maxColors, sourcePalette.Length);
    for (int i = 0; i < colorCount; i++)
    {
        targetPalette[i] = new RGBQUAD(sourcePalette[i]);
        if (transparentIndex == -1 && sourcePalette[i].A == 0)
            transparentIndex = i;
        if (!hasBlack && (sourcePalette[i].ToArgb() & 0xFFFFFF) == 0)
            hasBlack = true;
    }

    // if transparent index is 0, relocating it and setting transparent index to 1
    if (transparentIndex == 0)
    {
        targetPalette[0] = targetPalette[1];
        transparentIndex = 1;
    }
    // otherwise, setting the color of transparent index the same as the previous color, so it will not be used during the conversion
    else if (transparentIndex != -1)
    {
        targetPalette[transparentIndex] = targetPalette[transparentIndex - 1];
    }

    // if black color is not found in palette, counting 1 extra colors because it can be used in conversion
    if (colorCount < maxColors && !hasBlack)
        colorCount++;

    return colorCount;
}

private unsafe static void ToIndexedTransparentByArgb(Bitmap target, Bitmap source, int transparentIndex)
{
    int sourceBpp = Image.GetPixelFormatSize(source.PixelFormat);
    int targetBpp = Image.GetPixelFormatSize(target.PixelFormat);

    BitmapData dataTarget = target.LockBits(new Rectangle(Point.Empty, target.Size), ImageLockMode.ReadWrite, target.PixelFormat);
    BitmapData dataSource = source.LockBits(new Rectangle(Point.Empty, source.Size), ImageLockMode.ReadOnly, source.PixelFormat);
    try
    {
        byte* lineSource = (byte*)dataSource.Scan0;
        byte* lineTarget = (byte*)dataTarget.Scan0;
        bool is32Bpp = sourceBpp == 32;

        // scanning through the lines
        for (int y = 0; y < dataSource.Height; y++)
        {
            // scanning through the pixels within the line
            for (int x = 0; x < dataSource.Width; x++)
            {
                // testing if pixel is transparent (applies both argb and pargb)
                if (is32Bpp && ((uint*)lineSource)[x] >> 24 == 0
                    || !is32Bpp && ((ulong*)lineSource)[x] >> 48 == 0UL)
                {
                    switch (targetBpp)
                    {
                        case 8:
                            lineTarget[x] = (byte)transparentIndex;
                            break;
                        case 4:
                            // First pixel is the high nibble
                            int pos = x >> 1;
                            byte nibbles = lineTarget[pos];
                            if ((x & 1) == 0)
                            {
                                nibbles &= 0x0F;
                                nibbles |= (byte)(transparentIndex << 4);
                            }
                            else
                            {
                                nibbles &= 0xF0;
                                nibbles |= (byte)transparentIndex;
                            }

                            lineTarget[pos] = nibbles;
                            break;
                        case 1:
                            // First pixel is MSB.
                            pos = x >> 3;
                            byte mask = (byte)(128 >> (x & 7));
                            if (transparentIndex == 0)
                                lineTarget[pos] &= (byte)~mask;
                            else
                                lineTarget[pos] |= mask;
                            break;
                    }
                }
            }

            lineSource += dataSource.Stride;
            lineTarget += dataTarget.Stride;
        }
    }
    finally
    {
        target.UnlockBits(dataTarget);
        source.UnlockBits(dataSource);
    }
}

private static PixelFormat GetPixelFormat(int bpp)
{
    switch (bpp)
    {
        case 1:
            return PixelFormat.Format1bppIndexed;
        case 4:
            return PixelFormat.Format4bppIndexed;
        case 8:
            return PixelFormat.Format8bppIndexed;
        case 16:
            return PixelFormat.Format16bppRgb565;
        case 24:
            return PixelFormat.Format24bppRgb;
        case 32:
            return PixelFormat.Format32bppArgb;
        case 48:
            return PixelFormat.Format48bppRgb;
        case 64:
            return PixelFormat.Format64bppArgb;
        default:
            throw new ArgumentOutOfRangeException("bpp");
    }
}

And the native types and methods:

private const int BI_RGB = 0;
private const int DIB_RGB_COLORS = 0;

[DllImport("gdi32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr CreateDIBSection(IntPtr hdc, [In] ref BITMAPINFO pbmi, int iUsage, out IntPtr ppvBits, IntPtr hSection, uint dwOffset);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr GetDC(IntPtr hWnd);

[DllImport("gdi32.dll", SetLastError = true)]
private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

[DllImport("gdi32.dll", SetLastError = true)]
private static extern IntPtr CreateCompatibleDC(IntPtr hdc);

[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, uint dwRop);

[DllImport("gdi32.dll")]
private static extern bool DeleteDC(IntPtr hdc);

[DllImport("gdi32.dll", SetLastError = true)]
private static extern bool DeleteObject(IntPtr hObject);

[DllImport("user32.dll")]
private static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);

[StructLayout(LayoutKind.Sequential)]
private struct RGBQUAD
{
    internal byte rgbBlue;
    internal byte rgbGreen;
    internal byte rgbRed;
    internal byte rgbReserved;

    internal RGBQUAD(Color color)
    {
        rgbRed = color.R;
        rgbGreen = color.G;
        rgbBlue = color.B;
        rgbReserved = 0;
    }
}

[StructLayout(LayoutKind.Sequential)]
private struct BITMAPINFO
{
    public BITMAPINFOHEADER icHeader;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
    public RGBQUAD[] icColors;
}

[StructLayout(LayoutKind.Sequential)]
private struct BITMAPINFOHEADER
{
    internal uint biSize;
    internal int biWidth;
    internal int biHeight;
    internal ushort biPlanes;
    internal ushort biBitCount;
    internal uint biCompression;
    internal uint biSizeImage;
    internal int biXPelsPerMeter;
    internal int biYPelsPerMeter;
    internal uint biClrUsed;
    internal uint biClrImportant;
}

Update:

My Drawing Libraries are now free to download. It makes a SaveAsGif extension method available on the Image type:

using KGySoft.Drawing;
/// ...

using (var stream = new FileStream(targetPngPath, FileMode.Create))
{
    // You can either use an arbitrary palette,
    myPngBitmap.SaveAsGif(stream, myPngBitmap.GetColors(256));

    // or, you can let the built-in encoder use dithering with a fixed palette.
    // Pixel format is adjusted so transparency will be preserved.
    myPngBitmap.SaveAsGif(stream, allowDithering: true);
}
0
votes

This may help. The Bitmap class does not save correctly with transparency. You need to cast Bitmap to Image.

c# Bitmap.Save transparancy doesn't save in png

There are comments on the internet about .NET not saving Bitmaps with Transparency correctly .

Here is a good link for further reading, too much code to post.

http://forums.asp.net/t/1057792.aspx?ASP+NET+C+Making+an+Image+transparent