Loading DDS files as WPF ImageSources using XNA 3.1
WPF uses "Windows Imaging Component," or WIC, (see https://msdn.microsoft.com/en-us/library/ee719655) to decode images. By default, WIC supports many image formats, such as .bmp, .jpg, .png, but also can be extended to support new image formats via "codecs." Out of the box, WIC does not support DDS files. A common case for wanting to display a DDS file in a WPF application is for game content authoring tools. Arguably, the best way to display new image formats (such as DDS) in WPF is to implement a WIC codec. Commercial solutions doing exactly this are available. The benefit of this is that all applications that use WIC (Windows Explorer, Windows Live Photo Gallery, virtually all WPF-based applications, etc.) will be able to decode and display the new image format. However, this is a system wide change that must be registered with WIC, probably via an installer. An alternative approach is to provide the DDS decoding logic in the application code (or a shared library). This approach lacks the benefits of WIC, but with the help of XNA is very simple and can be implemented entirely in managed code.
The general idea is to use XNA's Texture2D class to load the DDS, then do a per pixel conversion to a WPF BitmapSource. First, I'll implement the core method for doing the conversion from a Stream to an ImageSource:
public static ImageSource Convert(Stream
stream)
{
// Create the graphics device
using (var
graphicsDevice = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.NullReference, IntPtr.Zero,
new PresentationParameters()))
{
// Setup the texture creation parameters
var textureCreationParameters = new TextureCreationParameters()
{
Width = -1,
Height = -1,
Depth = 1,
TextureUsage = TextureUsage.None,
Format = SurfaceFormat.Color,
Filter = FilterOptions.None,
MipFilter = FilterOptions.None,
MipLevels = 1
};
// Load the texture
using (var
texture = Texture2D.FromFile(graphicsDevice,
stream, textureCreationParameters))
{
// Get the pixel data
var pixelColors = new
Microsoft.Xna.Framework.Graphics.Color[texture.Width
* texture.Height];
texture.GetData(pixelColors);
// Copy the pixel colors into a byte array
var bytesPerPixel = 3;
var stride = texture.Width * bytesPerPixel;
var pixelData = new
byte[pixelColors.Length * bytesPerPixel];
for (var i = 0;
i < pixelColors.Length; i++)
{
pixelData[i * bytesPerPixel + 0] = pixelColors[i].R;
pixelData[i * bytesPerPixel + 1] = pixelColors[i].G;
pixelData[i * bytesPerPixel + 2] = pixelColors[i].B;
}
// Create a bitmap source
return BitmapSource.Create(texture.Width,
texture.Height, 96, 96, PixelFormats.Rgb24, null, pixelData, stride);
}
}
}
There are a few things worth noting here. First, note that only mip level one is being used as it is the highest resolution; the other mip levels are ignored. Second, note that alpha is being ignored entirely. There may be scenarios where this is important, but in many cases RGB data alone is sufficient and often desired. If alpha data is necessary, adding it should be a relatively minimal change.
Since the method above is intended for WPF applications, the DDS will likely be bound to a property of a WPF class, such as Image.Source. The DDS source may be a file path (e.g. loading from disk), may be a stream (e.g. loading from game content pack), may be a byte array (e.g. embedded resource), etc. This is another area where a WIC codec would provide a cleaner solution. The WPF ImageSource class already has a TypeConverter that handles converting from strings, streams, URIs, byte arrays, etc. by decoding the image using WIC. Since the XNA-based implementation provided here does not go through WIC, the binding must explicitly specify a converter. To support this, a custom IValueConverter can be used:
public class DDSConverter : IValueConverter
{
private static
readonly DDSConverter
defaultInstace = new DDSConverter();
public static
DDSConverter Default
{
get
{
return DDSConverter.defaultInstace;
}
}
public object
Convert(object value, Type
targetType, object parameter, CultureInfo culture)
{
if (value == null)
{
throw new
ArgumentNullException("value");
}
else if
(value is Stream)
{
return DDSConverter.Convert((Stream)value);
}
else if
(value is string)
{
return DDSConverter.Convert((string)value);
}
else if
(value is byte[])
{
return DDSConverter.Convert((byte[])value);
}
else
{
throw new
NotSupportedException(string.Format("{0}
cannot convert from {1}.", this.GetType().FullName,
value.GetType().FullName));
}
}
public object
ConvertBack(object value, Type targetType, object
parameter, CultureInfo culture)
{
throw new
NotSupportedException(string.Format("{0}
does not support converting back.", this.GetType().FullName));
}
public static
ImageSource Convert(string
filePath)
{
using (var
fileStream = File.OpenRead(filePath))
{
return DDSConverter.Convert(fileStream);
}
}
public static
ImageSource Convert(byte[]
imageData)
{
using (var
memoryStream = new MemoryStream(imageData))
{
return DDSConverter.Convert(memoryStream);
}
}
public static
ImageSource Convert(Stream
stream)
{
...
}
}
Following is a simple usage example:
<Image Source="{Binding Source=Test.dds, Converter={x:Static local:DDSConverter.Default}}" />
My experience with XNA is limited (the XNA parts of the implementation above was achieved with the help of a former coworker), so it is entirely possible that the code provided could be improved. Again, the solution provided here is a quick, simple way to get DDS images displayed in WPF writing only managed code. As previously mentioned, implementing a DDS WIC codec would almost certainly be a more robust, long term solution, though also likely a heavier solution. I have no experience implementing WIC codecs, so if anyone wants to chime in on this I'd love to hear about it.