将透明PNG保存为透明GIF

3

我正在尝试调整透明的png图像大小,并将其保存为单帧gif图像。 跳过调整大小部分,当您尝试将透明的png保存为gif时,输出gif中会出现黑色背景:

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

是的,我可以将黑色背景改成白色,但这并不是我想要的。即使使用 MakeTransparent 方法去除黑色颜色,它也会移除图像中的所有黑色颜色,导致我们没有标准透明的图像。

我们也可以通过一个技巧来保存gif图像,我们保留文件名的扩展名,但将其保存为PNG格式,就像这样:

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

但这并不是标准做法。那么有没有安全的方法将带透明度的PNG图片保存为带透明度的GIF图片呢?
PNG = enter image description here GIF = enter image description here 使用Photoshop保存的GIF = enter image description here

输出的 GIF 图像背景为黑色 - 是由加载、调整大小还是保存引起的? - Sinatr
保存后会出现黑色背景。 - Inside Man
3
技术上,GIF 的透明度意味着一个颜色(最多256种中的一种)被视为透明。PNG 的透明度则不同——它包含一个 alpha(透明)层。如果您能找到 GIF 中未使用的颜色,则可以用该颜色替换 PNG 透明部分,使其透明并保存,那么您就可以成功了。 - Arvo
1
那里的“技巧”实际上并不是什么技巧,因为它在任何方面都仍然是一个PNG。 - Jon Hanna
@JonHanna 是的,我知道它完全是一个PNG文件,这是因为我把它命名为“技巧” :) - Inside Man
显示剩余5条评论
2个回答

10

这是因为内置的GIF编码器无法很好地处理源文件,除非它已经是一个8 bpp图像。您必须先将PNG图像转换为256色图像,然后才能使用GIF编码器正确保存它。

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");
    }
}

以及原生类型和方法:

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;
}

更新:

我的绘图库现在可以免费下载。它提供了一个SaveAsGif扩展方法,可用于Image类型:

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);
}

1
请参见:https://dev59.com/42Mm5IYBdhLWcg3wZ-MX#17650474 如果您的项目不允许使用不安全代码,您可以用性能较差的解决方案替换它们。第一个不安全块可以通过始终使用GetPixel方法的回退来避免(非常慢)。在不安全的方法中,您可以通过Marshal.Copy将位图内容复制到常规字节数组中。在操作byte[]之后,您必须将其复制回去。 - György Kőszeg
很棒的代码,非常感谢。我还有另一个问题,也许你可以帮忙解决,我会非常感激。我能要你的电子邮件吗?或者我该如何联系你? - Inside Man
1
如果我错过了您的问题,请在此处提出公开问题并留下评论。但是我不能保证提供任何帮助。 :) - György Kőszeg
1
我目前没有空闲的资源来做那件事。但是我已经开始实现一些绘图扩展和库,它们完成后将被移动到GitHub上。 - György Kőszeg
1
只有在省略 GetColors 的结果时,才会使用默认调色板。然而,此 GetColors 实现会收集源图像的前 256 种不同颜色(因此方法上方有 TODO),但如果源具有高达 256 种颜色但像素格式是 hi-color,则这是可以接受的。 - György Kőszeg
显示剩余8条评论

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接