2
votes

I've been trying to figure out a way to resize a PNG image without losing color data from pixels that are completely transparent. Here is the code that I'm using to achieve this:

//sourceImage is also a bitmap read in earlier
using (var scaledBitmap = new Bitmap(desiredWidth, desiredHeight, PixelFormat.Format32bppArgb)){
    using (var g = Graphics.FromImage(scaledBitmap)){

        var attr = new ImageAttributes();
        attr.SetWrapMode(WrapMode.TileFlipXY);
        g.CompositingQuality = CompositingQuality.HighQuality;
        g.CompositingMode    = CompositingMode.SourceCopy;
        g.InterpolationMode  = InterpolationMode.HighQualityBicubic;
        g.SmoothingMode      = SmoothingMode.AntiAlias;
        g.PixelOffsetMode    = PixelOffsetMode.HighQuality;

        g.DrawImage(sourceImage, new Rectangle(0, 0, desiredWidth, desiredHeight), 0, 0, sourceImage.Width, sourceImage.Height, GraphicsUnit.Pixel, attr);
    }
    scaledBitmap.Save(memoryStream, ImageFormat.Png);
   //use new bitmap here
}

But the problem with this example is that anywhere that a pixel's alpha value is zero, the pixels become black.

My reputation is too low to post these images inline sadly.

Images

First Image is the image I've been using to test with. For quick reference, I've saved a copy without the alpha channel (Second Image). The entire image is a solid color and the alpha masks part of the image to create a circle.

Third Image is the resulting image from the code above. Again I've saved two copies. In Fourth Image, it is very clear that data is being lost during the conversion. What I'd expect to see is the same single color filling the whole image.

Doing some reading I discovered this in the PNG file specification:

The alpha channel can be regarded either as a mask that temporarily hides transparent parts of the image, or as a means for constructing a non-rectangular image. In the first case, the colour values of fully transparent pixels should be preserved for future use. In the second case, the transparent pixels carry no useful data and are simply there to fill out the rectangular image area required by PNG. In this case, fully transparent pixels should all be assigned the same colour value for best compression.

So it looks like it's up to the PNG encoder to decide how to handle color values where the alpha is zero. Is there any way to tell GDI+'s encoder to not zero out the color data?

The reason I need to keep the color data is for further, dynamic resizing of the image. Without a homogeneous color at the edges of the visible portion of the image, they are terribly prone to ringing artifacts. There's also a post on the Unity forums about this issue.

Please let my know if anyone else has encountered this and how you dealt with it. Also if anyone knows of any libraries that implement a C# PNG encoder that is less draconian it would be greatly appreciated.

Thanks!

2
Have you determined at which point this occurs? Is on Save or DrawImage?Blorgbeard
It's actually happening during both. I've modified my code to resize the alpha layer and color channels separately and I can combine them to get a data representation that is correct, but then the Save drops the data a second time.Ryan McDonald

2 Answers

2
votes

While I realize this may not be sufficient for some, in my case the answer was to switch to using ImageMagick's .NET wrapper. I was also able to find a thread on their forums with the exact problem I was experiencing.

It appears that this is a common bug when resizing images and ImageMagick has fixed this issue in their library.

using ImageMagick;

using (var sourceImage = new MagickImage(texture)){ //texture is of type Stream
    sourceImage.Resize(desiredWidth, desiredHeight);
    sourceImage.Write(memoryStream, MagickFormat.Png);
}
// use memoryStream here

As you can see, this also greatly simplified my code due to ImageMagick's built in support for resize operations.

0
votes

Without ImageMagick, you can simply use the following code and it works, don't change CompositingQuality or any other Graphics properties :

Bitmap NewImage = new Bitmap(OriginalImage.Width, OriginalImage.Height, OriginalImage.PixelFormat);
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(NewImage)) {
    g.DrawImage(OriginalImage, new Rectangle(0, 0, NewWidth, NewHeight), 0, 0, OriginalImage.Width, OriginalImage.Height, GraphicsUnit.Pixel);
}
NewImage.Save("myTransparent.png");