Dustin Horne

Developing for fun...

Getting Color Data for a Texture2D in Silverlight 5 XNA

I decided to finish out the year by converting my XNA Terrain series to work with Silverlight 5 and XNA but during the process of converting I ran into an interesting issue.  The Texture2D.GetData() method is oddly missing from the Silverlight 5 XNA implementation.  I came across several posts around the web asking for a workaround but found no solution so I decided to develop my own.

There are a couple pieces involved in making this work.  I wanted to create my own ImageData class that contains my width, height and color information.  I needed to reference this object from inside my Silverlight application like so:  contentManager.Load<ImageData>("heightmap");  Here I ran into a couple of issues.  I'm sure there is a way to make it work, however I just worked around it.  Since I have no access to the pixel data in a Texture2D I needed to get the raw image data and I did so by extending the content pipeline.  The issue with this is that I cannot reference my Silverlight class library project from within the content pipeline as it's an unsupported type and I can't reference the content pipeline inside my Silveright 3D application because it's not a Silverlight project.  In the end, I'm decided to return the raw byte data from the image and use a utility class to convert it to the type I need.

What You Will Need

Before you begin, you'll need to make sure you have the proper bits installed.  In order to do XNA in Silverlight you'll need the Silverlight 5 Tools and developer runtime.  In order to extnd the content pipeline and use the built in templates you'll need the Silverlight 5 Toolkit, and in order to build and work with the Content projects you'll need to make sure you have XNA Game Studio 4 installed.  While I'm sure if you're reading this you already have these components, here are the links to find them:

XNA Game Studio 4
Silverlight 5 Tools (Make sure you also have Visual Studio 2010 SP1)
Silverlight Toolkit (I used the December 2011 release)

Extending the Content Pipeline

The first thing you'll need to do is create a new content pipeline extension to handle your images.  Go ahead and create a new project in Visual Studio.  Under Installed Templates choose XNA Game Studio 4.0 and choose the Content Pipeline Extension Library.  Let's name it ImageDataPipeline and click OK.  By default, the template will create a project with a content processor called ContentProcessor1.cs.  While you could use the ContentProcessor to perform any additional actions on your image data, we're only interested in getting the raw byte data from the image so go ahead and delete ContentProcessor1.cs.

Right click on your project and choose Add --> New Item.  Again choose XNA Game Studio 4.0 from the Installed Templates list and choose Content Importer for the type.  Let's call it ImageDataImporter.cs.  Now you should have a nice but very busy template for your content importer.  The first comment in the file is asking us to replace TImport with the type we want to import.  Since we're only interested in returning a byte array, go ahead and delete both the comment and the using TImport line.

Next we need to update the definition for our class.  The template includes a file extension, an display name and a default processor.  Let's pick an extension that can be used for our files.  In my example I just use .bid (for binary image data).  I also set a friendly name for hte importer and removed the default processor.  Your attribute line should look like this:
[ContentImporter(".bid", DisplayName = "Raw Image Data Importer")]

Next, replace the remaining references to TImport with byte[] to signify we're returning a byte array.  And finally, we'll setup our Import method to open the file and read the data stream into a byte array.  It should also be noted that, while we're using this to demonstrate loading image content, this content importer could be used to read any raw binary from a file in the content project.  In fact, you could create different processors for different types of content files and handle the raw byte data differently.  For now we're just going to assume the input file is an image.  We'll be handling the raw data in a different class.  Your entire content importer should now look like this:

using Microsoft.Xna.Framework.Content.Pipeline;

namespace ImageDataPipeline
{
    [ContentImporter(".bid", DisplayName = "Raw Image Data Importer")]
    public class ImageDataImporter : ContentImporter<byte[]>
    {
        public override byte[] Import(string filename, ContentImporterContext context)
        {
            byte[] result;

            var totalBytes = 0;

            using (var imgStream = System.IO.File.OpenRead(filename))
            {
                result = new byte[imgStream.Length];

                while (totalBytes < imgStream.Length)
                {
                    var bytesRemaining = imgStream.Length - totalBytes;
                    var readCount = bytesRemaining > (long)256 ? 256 : (int)bytesRemaining;
                    var bytesRead = imgStream.Read(result, totalBytes, readCount);
                    totalBytes += bytesRead;
                }
            }

            return result;
        }
    }
}

And that's it, we now have a content importer that will read our raw image data into a byte array.  The next piece of the puzzle is to convert that byte data into something more usable.  Right click on your solution and add a new project.  We're going to choose Silverlight 3D Library.  This allows us to return the XNA Color type.  Go ahead and name the project Imaging and click OK.  Then right click the new project and add a new class.  Call the new class ImageData.

We've chosen to use a class instead of a struct because it will contain a large amount of data and we want to make sure we're using a reference type.  To summarize what we're going to do, the ImageData class contains a private constructor that accepts a byte array with the raw image data.  We want to make sure all of our fields are initialized properly so we're using a static FromBytes method which accepts the byte array and constructs a new ImageData object for us.

Since the WritableBitmap stores each pixel as an integer value we're converting each of those integers into individual bytes for the red, green, blue and alpha values.  We're also implementing a utility method called ToTexture2D which accepts the graphics device (used to construct the Texture2D) and converts the colors back and creates a new Texture2D object.

The entire code for your ImageData class should look like this:

using System.Windows.Media.Imaging;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Imaging
{
    public class ImageData
    {
        public Color[] Colors { get; private set; }
        public int Width { get; private set; }
        public int Height { get; private set; }

        private ImageData(int[] pixelData, int width, int height)
        {
            SetColorsFromPixelData(pixelData);
            Width = width;
            Height = height;
        }

        private ImageData() { }

        public static ImageData FromBytes(byte[] rawImageData)
        {
            using (var memstream = new System.IO.MemoryStream(rawImageData))
            {
                var wbm = new WriteableBitmap(0, 0);
                wbm.SetSource(memstream);
                return new ImageData(wbm.Pixels, wbm.PixelWidth, wbm.PixelHeight);
            }
        }

        public void GetData(Color[] colorArray)
        {
            Colors.CopyTo(colorArray, 0);
        }

        private void SetColorsFromPixelData(int[] pixelData)
        {
            Colors = new Color[pixelData.Length];

            for (var i = 0; i < pixelData.Length; i++)
            {
                var pxl = pixelData[i];
                var r = (pxl) & 0xFF;
                var g = (pxl >> 8) & 0xFF;
                var b = (pxl >> 16) & 0xFF;
                var a = (pxl >> 24) & 0xFF;

                Colors[i] = new Color(r, g, b, a);
            }
        }

        public Texture2D ToTexture2D(GraphicsDevice graphicsDevice)
        {
            var result = new Texture2D(graphicsDevice, Width, Height);
            var wbm = new WriteableBitmap(Width, Height);

            for (int i = 0; i < Colors.Length; i++)
            {
                //PackedValue is an unsigned int, convert to a signed int
                wbm.Pixels[i] = (int)Colors[i].PackedValue;
            }

            wbm.CopyTo(result);

            return result;
        }

    }
}

And That's It!

You now have a content processor that's capable of loading binary image data and a class that returns the color values.  The Colors array of the ImageData class contains XNA Framework colors with RGBA values.  The ToTexture2D method will return the ImageData class as a Texture2D if you need it (so you don't have to reload the content).  One thing to note is that supported file formats appear to differ between the Silverlight and full XNA versions.  When testing this code I used a bitmap that I use for my XNA projects and the WritableBitmap class failed to load it properly.  I did test with PNG's however and it loaded them just fine.

If you want to test, the easiest is to add a new project to your solution and choose the Silverlight 3D Application template (this is part of the Silverlight Toolkit).  This will generate a default spinning cube application.  In the 3DAppContent project, add a reference to the Content Pipleline project we created and recompile.  Add a PNG image and look in the Content Importer (under properties for the image) dropdown.  It will be set to Texture by defult... If you've recompiled you should also see Raw Image Data Importer in that list.  Choose it for your PNG.  For the Content Processor, just set it to No Processing Required (If you leave it at the default it will fail). 

Now in your Silverlight 3D Application project, add a reference to the Imaging project and open the default Scene.cs file.  Find where it loads the content (it loads a default effect file) and add the following line:
var imgData = ImageData.FromBytes(contentManager.Load<byte[]>("<your filename here>"));

This should return an ImageData object with all of your color data.

I hope you enjoyed this article.  If so, please leave some feedback and feel free to share the link with others.  If you find any errors or inconsistencies in the sample code please let me know and I'll get them fixed ASAP.