Composition d’image : rendre du XAML sous forme d’image [Update 12/2011]

Posted by & filed under Sources.

Voici un morceau de code qui pourront rendre service lors de développements Silverlight (3 et +, Windows Phone 7, …) : si vous avez besoin de générer une image dans votre application : à partir de Silverlight 3, la classe WriteableBitmap est à votre disposition et permet de créer une image pixel par pixel.

Elle est de transformer un arbre XAML en une image, seule ombre au tableau; cette classe est parfaite pour l’affichage à l’écran, en effet le contrôle Image est capable d’afficher un WriteableBitmap mais pour la sauvegarde c’est une autre paire de manche : nativement, Silverlight ne propose pas d’encodeur PNG ou JPEG pour exporter ce paquet de pixels.

Le code qui suit vous permettra de profiter de la praticité de Silverlight pour composer une image (à vous les superpositions alpha, le texte antialiasé positionné comme bon vous semble en quelques lignes, les layouts faciles à utiliser – grid, stackpanel, …) exportable en PNG non compressé (attention quand même du coup, et en passant merci à Joe Stegman pour son PNGEncoder).

Vous pourrez ensuite manipuler ces images comme bon vous semble, car deux formats de sorties sont disponibles, l’une en Stream et l’autre en Byte[]. Aucun problème pour les sérialiser (contrairement aux BitmapImage) 🙂

Code and english version of the post available below

Here’s a piece of code that could help your Silverlight developments (SL3 and newer, Windows Phone 7, …) : if you need to generate an image in your application, starting with Silverlight 3 the WriteableBitmap class is available to create an image and interact with it pixel by pixel.

WriteableBitmap can transform a XAML visual tree into an image (meaning a pixel array); this is perfect for a screen output as the Image control knows how to display it. For saving on the other hand, it’s a different story : Silverlight simply does not provide a PNG or JPEG encoder to export that pixel array.

The following code will enable you to leverage Silverlight’s convenience for composing an image (alpha overlays, antialised text, straightforward containers for your layout, …), and export it as a PNG image (uncompressed) – thanks Joe Stegman for your PNGEncoder –.

You will then be able to manipulate those images the way your want, two outputs are available, one as a Stream and the other one as a Byte[]. Unlike the BitmapImage, you’ll be able to serialize it as a part of your classes :-).

Preview code :

/**********************************************************
 * Control2Png.cs
 * Renders a Silverlight UIElement as an uncompressed PNG
 * stream or byte array. Relies on Joe Stegman's great
 * PNGEncoder classes.
 *
 * Written by : Pierre BELIN

 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Lesser GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the Lesser GNU General Public License
 * along with this program.  If not, see .
 *
 **********************************************************/

using System;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;

namespace Ree7.Utils.Imaging
{
    ///
<summary> /// See http://ree7.fr/blog/2010/10/composition-dimage-rendre-du-xaml-sous-forme-dimage/
 /// </summary>
    public class ControlToPng
    {
        public static Stream RenderAsPNGStream(UIElement e)
        {
            try
            {
                WriteableBitmap wb = new WriteableBitmap(e, null);
                EditableImage edit = new EditableImage(wb.PixelWidth, wb.PixelHeight);

                for (int y = 0; y < wb.PixelHeight; ++y)
                {
                    for (int x = 0; x < wb.PixelWidth; ++x)
                    {
                        byte&#91;&#93; rgba = ExtractRGBAfromPremultipliedARGB(wb.Pixels&#91;wb.PixelWidth * y + x&#93;);
                        edit.SetPixel(x, y, rgba&#91;0&#93;, rgba&#91;1&#93;, rgba&#91;2&#93;, rgba&#91;3&#93;);
                    }
                }

                return edit.GetStream();

            }
            catch (Exception)
            {
                return null;
            }
        }

        public static byte&#91;&#93; RenderAsPNGBytes(UIElement e)
        {
            Stream s = RenderAsPNGStream(e);

            if (s == null)
                return null;

            byte&#91;&#93; bytes = ReadFully(s, (int)s.Length);
            return bytes;
        }

        ///
<summary> /// Convert from premultiplied alpha ARGB to a non-premultiplied RGBA (fix 12/2011)
 /// </summary>
        ///
        private static byte[] ExtractRGBAfromPremultipliedARGB(int pARGB)
        {
            byte[] sourcebytes = new byte[4];
            sourcebytes[0] = (byte)(pARGB >> 24);
            sourcebytes[1] = (byte)((pARGB & 0x00FF0000) >> 16);
            sourcebytes[2] = (byte)((pARGB & 0x0000FF00) >> 8);
            sourcebytes[3] = (byte)(pARGB & 0x000000FF);

            if (pARGB == 0) return sourcebytes; // optimization for images with many transparent pixels

            byte[] destbytes = new byte[4];

            if (sourcebytes[0] == 0 || sourcebytes[0] == 255)
            {
                destbytes[0] = sourcebytes[1];
                destbytes[1] = sourcebytes[2];
                destbytes[2] = sourcebytes[3];
                destbytes[3] = sourcebytes[0];
            }
            else
            {
                double factor = 255.0 / sourcebytes[0];
                double r = sourcebytes[1] * factor;
                double g = sourcebytes[2] * factor;
                double b = sourcebytes[3] * factor;

                destbytes[0] = Convert.ToByte(Math.Min(Byte.MaxValue, r));
                destbytes[1] = Convert.ToByte(Math.Min(Byte.MaxValue, g));
                destbytes[2] = Convert.ToByte(Math.Min(Byte.MaxValue, b));
                destbytes[3] = sourcebytes[0];
            }

            return destbytes;
        }

        ///
<summary> /// Reads data from a stream until the end is reached. The
 /// data is returned as a byte array. An IOException is
 /// thrown if any of the underlying IO calls fail.
 /// </summary>
        ///The stream to read data from
        ///The initial buffer length
        private static byte[] ReadFully(Stream stream, int initialLength)
        {
            // If we've been passed an unhelpful initial length, just
            // use 32K.
            if (initialLength < 1)
            {
                initialLength = 32768;
            }

            byte&#91;&#93; buffer = new byte&#91;initialLength&#93;;
            int read = 0;

            int chunk;
            while ((chunk = stream.Read(buffer, read, buffer.Length - read)) > 0)
            {
                read += chunk;

                // If we've reached the end of our buffer, check to see if there's
                // any more information
                if (read == buffer.Length)
                {
                    int nextByte = stream.ReadByte();

                    // End of stream? If so, we're done
                    if (nextByte == -1)
                    {
                        return buffer;
                    }

                    // Nope. Resize the buffer, put in the byte we've just
                    // read, and continue
                    byte[] newBuffer = new byte[buffer.Length * 2];
                    Array.Copy(buffer, newBuffer, buffer.Length);
                    newBuffer[read] = (byte)nextByte;
                    buffer = newBuffer;
                    read++;
                }
            }
            // Buffer is now too big. Shrink it.
            byte[] ret = new byte[read];
            Array.Copy(buffer, ret, read);
            return ret;
        }
    }
}

Update 01/12/2011 : Fixed the error for translucent pixels that were not looking right, I forgot to offset the color values as WritableBitmap gives premultiplied alpha pixel values.

2 Responses to “Composition d’image : rendre du XAML sous forme d’image [Update 12/2011]”