save PNG (transparent picture)

Sep 30, 2011 at 10:56 PM

Any idea how to add the functionality so i can save the image as transparent PNG? i am able to load the png and change the image by writing some text etc. No I would like to save the image as PNG so I can use it as tile in my WP7 Mango app.

Any ideas are appreciated.

Coordinator
Oct 1, 2011 at 6:21 AM

Such format functionality is not supported by this library. WBX is more about content creation.

The ImageTools lib might support PNG writing.

 

- Rene Schulte

Oct 1, 2011 at 6:43 AM

yes, Imagetools lib is what I checked out, i wrote an extensionmethod (like WriteTga) for WritableBitmapEx which saves to PNG. (see below) based on that for the people who need it. Supports compression as well (make sure you add the SharpZipLib.WindowsPhone7)

using System;
using System.Globalization;
using System.IO;
using ICSharpCode.SharpZipLib.Checksums;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;

namespace System.Windows.Media.Imaging
{

    static class PngChunkTypes
    {
        /// <summary>
        /// The first chunk in a png file. Can only exists once. Contains 
        /// common information like the width and the height of the image or
        /// the used compression method.
        /// </summary>
        public const string Header = "IHDR";
        /// <summary>
        /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
        /// series in the RGB format.
        /// </summary>
        public const string Palette = "PLTE";
        /// <summary>
        /// The IDAT chunk contains the actual image data. The image can contains more
        /// than one chunk of this type. All chunks together are the whole image.
        /// </summary>
        public const string Data = "IDAT";
        /// <summary>
        /// This chunk must appear last. It marks the end of the PNG datastream. 
        /// The chunk's data field is empty. 
        /// </summary>
        public const string End = "IEND";
        /// <summary>
        /// This chunk specifies that the image uses simple transparency: 
        /// either alpha values associated with palette entries (for indexed-color images) 
        /// or a single transparent color (for grayscale and truecolor images). 
        /// </summary>
        public const string PaletteAlpha = "tRNS";
        /// <summary>
        /// Textual information that the encoder wishes to record with the image can be stored in 
        /// tEXt chunks. Each tEXt chunk contains a keyword and a text string.
        /// </summary>
        public const string Text = "tEXt";
        /// <summary>
        /// This chunk specifies the relationship between the image samples and the desired 
        /// display output intensity.
        /// </summary>
        public const string Gamma = "gAMA";
        /// <summary>
        /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. 
        /// </summary>
        public const string Physical = "pHYs";
    }

    sealed class PngHeader
    {
        /// <summary>
        /// The dimension in x-direction of the image in pixels.
        /// </summary>
        public int Width;
        /// <summary>
        /// The dimension in y-direction of the image in pixels.
        /// </summary>
        public int Height;
        /// <summary>
        /// Bit depth is a single-byte integer giving the number of bits per sample 
        /// or per palette index (not per pixel). Valid values are 1, 2, 4, 8, and 16, 
        /// although not all values are allowed for all color types. 
        /// </summary>
        public byte BitDepth;
        /// <summary>
        /// Color type is a integer that describes the interpretation of the 
        /// image data. Color type codes represent sums of the following values: 
        /// 1 (palette used), 2 (color used), and 4 (alpha channel used).
        /// </summary>
        public byte ColorType;
        /// <summary>
        /// Indicates the method  used to compress the image data. At present, 
        /// only compression method 0 (deflate/inflate compression with a sliding 
        /// window of at most 32768 bytes) is defined.
        /// </summary>
        public byte CompressionMethod;
        /// <summary>
        /// Indicates the preprocessing method applied to the image 
        /// data before compression. At present, only filter method 0 
        /// (adaptive filtering with five basic filter types) is defined.
        /// </summary>
        public byte FilterMethod;
        /// <summary>
        /// Indicates the transmission order of the image data. 
        /// Two values are currently defined: 0 (no interlace) or 1 (Adam7 interlace).
        /// </summary>
        public byte InterlaceMethod;
    }


    public static partial class WriteableBitmapExtensions
    {
        private const int MaxBlockSize = 0xFFFF;


        private static Stream _stream;
        private static WriteableBitmap _image;

        private const double DefaultDensityX = 75;
        private const double DefaultDensityY = 75;



        private static bool IsWritingUncompressed = false;
        private static bool IsWritingGamma = true;
        private static double Gamma = 2.2f;

        public static void WritePNG(this WriteableBitmap bmp, System.IO.Stream destination)
        {
            Encode(bmp, destination);
        }

        public static void Encode(WriteableBitmap image, Stream stream)
        {


            _image = image;

            _stream = stream;

            // Write the png header.
            stream.Write(
                new byte[] 
                { 
                    0x89, 0x50, 0x4E, 0x47, 
                    0x0D, 0x0A, 0x1A, 0x0A 
                }, 0, 8);

            PngHeader header = new PngHeader();
            header.Width = image.PixelWidth;
            header.Height = image.PixelHeight;
            header.ColorType = 6;
            header.BitDepth = 8;
            header.FilterMethod = 0;
            header.CompressionMethod = 0;
            header.InterlaceMethod = 0;

            WriteHeaderChunk(header);

            WritePhysicsChunk();
            WriteGammaChunk();

            if (IsWritingUncompressed)
            {
                WriteDataChunksFast();
            }
            else
            {
                WriteDataChunks();
            }
            WriteEndChunk();

            stream.Flush();
        }


        private static void WritePhysicsChunk()
        {
            int dpmX = (int)Math.Round(DefaultDensityX * 39.3700787d);
            int dpmY = (int)Math.Round(DefaultDensityY * 39.3700787d);

            byte[] chunkData = new byte[9];

            WriteInteger(chunkData, 0, dpmX);
            WriteInteger(chunkData, 4, dpmY);

            chunkData[8] = 1;

            WriteChunk(PngChunkTypes.Physical, chunkData);
        }

        private static void WriteGammaChunk()
        {
            if (IsWritingGamma)
            {
                int gammeValue = (int)(Gamma * 100000f);

                byte[] fourByteData = new byte[4];

                byte[] size = BitConverter.GetBytes(gammeValue);
                fourByteData[0] = size[3]; fourByteData[1] = size[2]; fourByteData[2] = size[1]; fourByteData[3] = size[0];

                WriteChunk(PngChunkTypes.Gamma, fourByteData);
            }
        }

        private static void WriteDataChunksFast()
        {
            byte[] pixels = _image.ToByteArray();

            // Convert the pixel array to a new array for adding
            // the filter byte.
            // --------------------------------------------------
            byte[] data = new byte[_image.PixelWidth * _image.PixelHeight * 4 + _image.PixelHeight];

            int rowLength = _image.PixelWidth * 4 + 1;

            for (int y = 0; y < _image.PixelHeight; y++)
            {
                data[y * rowLength] = 0;

                Array.Copy(pixels, y * _image.PixelWidth * 4, data, y * rowLength + 1, _image.PixelWidth * 4);
            }
            // --------------------------------------------------

            Adler32 adler32 = new Adler32();
            adler32.Update(data);

            using (MemoryStream tempStream = new MemoryStream())
            {
                int remainder = data.Length;

                int blockCount;
                if ((data.Length % MaxBlockSize) == 0)
                {
                    blockCount = data.Length / MaxBlockSize;
                }
                else
                {
                    blockCount = (data.Length / MaxBlockSize) + 1;
                }

                // Write headers
                tempStream.WriteByte(0x78);
                tempStream.WriteByte(0xDA);

                for (int i = 0; i < blockCount; i++)
                {
                    // Write the length
                    ushort length = (ushort)((remainder < MaxBlockSize) ? remainder : MaxBlockSize);

                    if (length == remainder)
                    {
                        tempStream.WriteByte(0x01);
                    }
                    else
                    {
                        tempStream.WriteByte(0x00);
                    }

                    tempStream.Write(BitConverter.GetBytes(length), 0, 2);

                    // Write one's compliment of length
                    tempStream.Write(BitConverter.GetBytes((ushort)~length), 0, 2);

                    // Write blocks
                    tempStream.Write(data, (int)(i * MaxBlockSize), length);

                    // Next block
                    remainder -= MaxBlockSize;
                }

                WriteInteger(tempStream, (int)adler32.Value);

                tempStream.Seek(0, SeekOrigin.Begin);

                byte[] zipData = new byte[tempStream.Length];
                tempStream.Read(zipData, 0, (int)tempStream.Length);

                WriteChunk(PngChunkTypes.Data, zipData);
            }
        }



        private static void WriteDataChunks()
        {
            byte[] pixels = _image.ToByteArray();

            byte[] data = new byte[_image.PixelWidth * _image.PixelHeight * 4 + _image.PixelHeight];

            int rowLength = _image.PixelWidth * 4 + 1;

            for (int y = 0; y < _image.PixelHeight; y++)
            {
                byte compression = 0;
                if (y > 0)
                {
                    compression = 2;
                }
                data[y * rowLength] = compression;

                for (int x = 0; x < _image.PixelWidth; x++)
                {
                    // Calculate the offset for the new array.
                    int dataOffset = y * rowLength + x * 4 + 1;

                    // Calculate the offset for the original pixel array.
                    int pixelOffset = (y * _image.PixelWidth + x) * 4;

                    data[dataOffset + 0] = pixels[pixelOffset + 0];
                    data[dataOffset + 1] = pixels[pixelOffset + 1];
                    data[dataOffset + 2] = pixels[pixelOffset + 2];
                    data[dataOffset + 3] = pixels[pixelOffset + 3];

                    if (y > 0)
                    {
                        int lastOffset = ((y - 1) * _image.PixelWidth + x) * 4;

                        data[dataOffset + 0] -= pixels[lastOffset + 0];
                        data[dataOffset + 1] -= pixels[lastOffset + 1];
                        data[dataOffset + 2] -= pixels[lastOffset + 2];
                        data[dataOffset + 3] -= pixels[lastOffset + 3];
                    }
                }
            }

            byte[] buffer = null;
            int bufferLength = 0;

            MemoryStream memoryStream = null;
            try
            {
                memoryStream = new MemoryStream();

                using (DeflaterOutputStream zStream = new DeflaterOutputStream(memoryStream))
                {
                    zStream.Write(data, 0, data.Length);
                    zStream.Flush();
                    zStream.Finish();

                    bufferLength = (int)memoryStream.Length;
                    buffer = memoryStream.GetBuffer();
                }
            }
            finally
            {
                if (memoryStream != null)
                {
                    memoryStream.Dispose();
                }
            }

            int numChunks = bufferLength / MaxBlockSize;

            if (bufferLength % MaxBlockSize != 0)
            {
                numChunks++;
            }

            for (int i = 0; i < numChunks; i++)
            {
                int length = bufferLength - i * MaxBlockSize;

                if (length > MaxBlockSize)
                {
                    length = MaxBlockSize;
                }

                WriteChunk(PngChunkTypes.Data, buffer, i * MaxBlockSize, length);
            }
        }

        private static void WriteEndChunk()
        {
            WriteChunk(PngChunkTypes.End, null);
        }

        private static void WriteHeaderChunk(PngHeader header)
        {
            byte[] chunkData = new byte[13];

            WriteInteger(chunkData, 0, header.Width);
            WriteInteger(chunkData, 4, header.Height);

            chunkData[8] = header.BitDepth;
            chunkData[9] = header.ColorType;
            chunkData[10] = header.CompressionMethod;
            chunkData[11] = header.FilterMethod;
            chunkData[12] = header.InterlaceMethod;

            WriteChunk(PngChunkTypes.Header, chunkData);
        }


        private static void WriteChunk(string type, byte[] data)
        {
            WriteChunk(type, data, 0, data != null ? data.Length : 0);
        }

        private static void WriteChunk(string type, byte[] data, int offset, int length)
        {
            WriteInteger(_stream, length);

            byte[] typeArray = new byte[4];
            typeArray[0] = (byte)type[0];
            typeArray[1] = (byte)type[1];
            typeArray[2] = (byte)type[2];
            typeArray[3] = (byte)type[3];

            _stream.Write(typeArray, 0, 4);

            if (data != null)
            {
                _stream.Write(data, offset, length);
            }

            Crc32 crc32 = new Crc32();
            crc32.Update(typeArray);

            if (data != null)
            {
                crc32.Update(data, offset, length);
            }

            WriteInteger(_stream, (uint)crc32.Value);
        }



        private static void WriteInteger(byte[] data, int offset, int value)
        {
            byte[] buffer = BitConverter.GetBytes(value);

            Array.Reverse(buffer);
            Array.Copy(buffer, 0, data, offset, 4);
        }

        private static void WriteInteger(Stream stream, int value)
        {
            byte[] buffer = BitConverter.GetBytes(value);

            Array.Reverse(buffer);

            stream.Write(buffer, 0, 4);
        }

        private static void WriteInteger(Stream stream, uint value)
        {
            byte[] buffer = BitConverter.GetBytes(value);

            Array.Reverse(buffer);

            stream.Write(buffer, 0, 4);
        }


 
    }

}

Oct 31, 2011 at 2:53 PM

Matthijs, thanks for this extension method to save to PNG. However, I've noticed the images I am trying to convert change color when using it. They all come out with a blueish tint which may indicate some channel encoding error.

Are you by any chance aware of this issue ? I haven't had time to look into it just yet.

Nov 11, 2011 at 7:14 AM

I used the code in this library to hack the extensionmethod together: http://imagetools.codeplex.com/

Dec 8, 2011 at 2:34 AM
I used code above and it works as advertised. I wonder if we can include it in the library itself, so i don't have to keep two separate dll to save to PNG.
i know i can get the source and put two together, its for the same of the users, that just want to use it out of the box :)

i use it with WP7.1 to create transparent tiles with background agent.

Thank you matthijshoekstra

 

Coordinator
Dec 8, 2011 at 7:52 AM

I talked with Matthijs about the integration of that PNG code. I want to keep the WriteableBitmpaEx library small and easy portable without any external dependencies. The PNG code above has a dependency on the SharpZipLib library that's why I won't include it as it is. If the license of the other projects is OK, we can think about to extract the needed functionality. Contributions are always welcome. :)

 

- Rene Schulte

Feb 24, 2012 at 12:48 AM

I had the same issue as guillembk with the color shift so I investigated and found Windows Phone returns the pixel in a different byte order than PNG expects.  I've rewritten the code, removed the reliance on both WriteableBitmapEx and SharpZipLib (though it obviously looses the compression of the PNG file) and rewitten most of the code to optimize the code down to a single copy of the image data in memory at any time and added a dedection function for the byte order being used by WirteableBitmap.

You can download the code here: http://toolstack.com/libraries/pngwriter

Apr 7, 2012 at 3:41 PM

I am going to store bytes from your Encode method as bytes not as png file. How do I decode them back into WriteableBitmap?

May 25, 2012 at 5:35 AM
Edited May 25, 2012 at 5:36 AM

have you tried using PngBitmapEncoder?

 

 

 WriteableBitmap wb = new WriteableBitmap(image);
                    PngBitmapEncoder encoder = new PngBitmapEncoder();
                    encoder.Interlace = PngInterlaceOption.Off;
                    encoder.Frames.Add(BitmapFrame.Create(wb));
                    using (FileStream fs = new FileStream(filename,FileMode.Create)) {
                        encoder.Save(fs);
                        fs.Close();
                    }

(assuming this exists on wp)

May 25, 2012 at 9:55 AM

Thank you ! That what I was looking for. But I had to use something else to resolve the problem at the time.

Oct 3, 2012 at 9:26 AM
JamiePate wrote:

have you tried using PngBitmapEncoder?

(assuming this exists on wp)

No it doesn't.

Oct 17, 2012 at 6:20 PM
Edited Oct 17, 2012 at 6:21 PM

Strange thing.

I have a module that creates a writablebitmap then does some text rendering:

         void RenderText(ref WriteableBitmap wbm, int index, int ptIndex, VCI_Point_Data vf)
        {
            if (wbm.Pixels.Count() > 0)
            {
                TextBlock textBlock = new TextBlock();
                textBlock.Text = vf.Pt_Value.ToString();
                wbm.Render(textBlock, new TranslateTransform() { Y = dyns[index].dyn_location.Y, X = dyns[index].dyn_location.X });
            }
        }

 

 

then does some flood filling:

       // copied from http://forums.silverlight.net/t/177905.aspx/1
        //
         public static void SafeFloodFill( ref WriteableBitmap bm, int x, int y, int new_color, 
                                          ref int[] edges)

 

then renders the writeablebitmap back to a bitmapimage:

                BitmapImage bi = new BitmapImage();   
                
                
                 using (MemoryStream ms = new MemoryStream())
                 {                     
                     wbm.WritePNG(ms);
                     bi.SetSource(ms); 
                     ms.Close();
                 }
                d.BackgroundImage = bi;
                 
          
                /*
                 * ExtendedImage eim = wbm.ToImage();
                Stream wbmms = eim.ToStreamByExtension(".png");
                bi.SetSource(wbmms);

                d.BackgroundImage = bi ;
                 * */

 

Note there are two ways shown above to convert the writeablebitmap back to a bitmapimage.

If I use the first approach, I get my floodfills but not my RenderTexts.

If I use the second approach, I get my RenderTexts but not my floodfills.

Any words of advice?

 

Boyd

 

 

 

 

 

 

Feb 14, 2013 at 2:53 PM
Does anyone tried WritableBitmapEx with PNG file save in WinRT?
I download ICSharpCode source code and dig in, but Tga to Png conversion in WinRT looks so hard than I expected (especially DeflaterOutputStream in ICSharpCode)!!
Coordinator
Feb 14, 2013 at 4:45 PM
You should try to use the BitmapEncoder class. You can find some more hints how to use it in the WinRT XAML Toolkit: http://winrtxamltoolkit.codeplex.com/SourceControl/changeset/view/f7a69dc2a5d9#WinRTXamlToolkit/Imaging/WriteableBitmapSaveExtensions.cs
  • Rene
Feb 15, 2013 at 3:02 PM
Rene, Thanks for your tip! :)
May 9, 2013 at 1:23 AM
Hi,

Can you please advise how I should use the code, such that when I save a png file on a windows 7 machine, I can open the png file in any image editor on the machine. Currently I can save silverlight writeablebitmap in a png file, and see the image in NT explorer but not in other applications.
Jul 1, 2013 at 10:12 AM
To fix the pixel shift or discoloration issue search for
               data[dataOffset + 0] = pixels[pixelOffset + 0];
                data[dataOffset + 1] = pixels[pixelOffset + 1];
                data[dataOffset + 2] = pixels[pixelOffset + 2];
                data[dataOffset + 3] = pixels[pixelOffset + 3];

                if (y > 0)
                {
                    int lastOffset = ((y - 1) * _image.PixelWidth + x) * 4;

                    data[dataOffset + 0] -= pixels[lastOffset + 0];
                    data[dataOffset + 1] -= pixels[lastOffset + 1];
                    data[dataOffset + 2] -= pixels[lastOffset + 2];
                    data[dataOffset + 3] -= pixels[lastOffset + 3];
                }
and replace with
               data[dataOffset + 0] = pixels[pixelOffset + 2];
                data[dataOffset + 1] = pixels[pixelOffset + 1];
                data[dataOffset + 2] = pixels[pixelOffset + 0];
                data[dataOffset + 3] = pixels[pixelOffset + 3];

                if (y > 0)
                {
                    int lastOffset = ((y - 1) * _image.PixelWidth + x) * 4;

                    data[dataOffset + 0] -= pixels[lastOffset + 2];
                    data[dataOffset + 1] -= pixels[lastOffset + 1];
                    data[dataOffset + 2] -= pixels[lastOffset + 0];
                    data[dataOffset + 3] -= pixels[lastOffset + 3];
                }
Dec 19, 2013 at 5:17 AM
Edited Dec 19, 2013 at 5:19 AM
Thanks for the very helpful extension! I've been working with trying to save a WriteableBitmap to a PNG and this was just the thing. I did find what I think is a bug in the implementation. I'm using this code to save png files that are used for Windows Phone Live Tiles. After adding the code from BubbaRichard it was working great, but I found that I was getting OutOfMemoryExceptions when run in the phone memory constrained ScheduledTask environment. In many cases the process is limited to 11MB (WP8). In this case I was seeing much higher memory consumption than expected. I finally was able to live within the memory limits, but only after modifying the WritePNG() function.
        public static void WritePNG(this WriteableBitmap bmp, System.IO.Stream destination)
        {
            Encode(bmp, destination);
        }
changed to
        public static void WritePNG(this WriteableBitmap bmp, System.IO.Stream destination)
        {
            Encode(bmp, destination);
            _image = null;
        }
I think that because that variable is static and WriteableBitmap doesn't implement IDisposable, that it doesn't get released and there is no way for the caller to release it. There isn't the same issue with the Stream because the caller can call Dispose() to free it.

I hope this helps save someone else avoid some pain.

Even with this change I had to manually call the GC to live in the small memory footprint allowed.

Thanks again for the code!