Blake O'Hare .com

Saving an Image in Silverlight to File

I created a nifty drawing program in Silverlight. Unfortunately there doesn't seem to be any native way to save a BitmapSource or FrameworkElement to an actual bitmap and save it without sending a cheesy web request to a rendering service hosted elsewhere. However, using the SaveFileDialog class, it is possible to save a byte array to the local user's hard drive. All that's missing is something that can convert images or elements within silverlight to a byte array.

After reading up on the 24-bit BMP file format, I am proud to present a C# bitmap renderer class that's Silverlight-friendly:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace BmpSaveExample
{
    public class BitmapEncoder
    {
        private WriteableBitmap bmp;

        public BitmapEncoder(FrameworkElement element)
        {
            this.bmp = new WriteableBitmap((int)element.ActualWidth, (int)element.ActualHeight);
            this.bmp.Render(element, new ScaleTransform());
            this.bmp.Invalidate();
        }

        public BitmapEncoder(BitmapSource bitmap)
        {
            this.bmp = new WriteableBitmap(bitmap);
        }

        public byte[] GetBitmapData()
        {
            List<byte> header = new List<byte>();
            List<byte> data = new List<byte>();
            
            this.WriteRowsToBytes(data);
            int dataLength = data.Count;
            int headerLength = 0x36;

            // bitmaps begin with "BM"
            header.Add(0x42); // B
            header.Add(0x4D); // M

            // size of the BMP file
            header.AddRange(this.GenerateLittleEndianNumberOfLength(headerLength + dataLength, 4));

            // unused header fields
            header.AddRange(this.GenerateLittleEndianNumberOfLength(0, 4));

            // byte offset of where bitmap data occurs
            header.AddRange(this.GenerateLittleEndianNumberOfLength(0x36, 4));

            // number of bytes in this header
            header.AddRange(this.GenerateLittleEndianNumberOfLength(0x28, 4));

            // width of the bitmap in pixels
            header.AddRange(this.GenerateLittleEndianNumberOfLength(this.bmp.PixelWidth, 4));

            // height of the bitmap in pixels
            header.AddRange(this.GenerateLittleEndianNumberOfLength(this.bmp.PixelHeight, 4));

            // number of planes
            header.AddRange(this.GenerateLittleEndianNumberOfLength(1, 2));

            // number of bits per pixel
            header.AddRange(this.GenerateLittleEndianNumberOfLength(24, 2));

            // no compression is used
            header.AddRange(this.GenerateLittleEndianNumberOfLength(0, 4));

            // size of the pixel data itself
            header.AddRange(this.GenerateLittleEndianNumberOfLength(dataLength, 4));

            // horizontal resolution (pixels/meter)
            header.AddRange(this.GenerateLittleEndianNumberOfLength(2835, 4));

            // vertical resolution (pixels/meter)
            header.AddRange(this.GenerateLittleEndianNumberOfLength(2835, 4));

            // palette information (none)
            header.AddRange(this.GenerateLittleEndianNumberOfLength(0, 8));

            header.AddRange(data);

            return header.ToArray();
        }

        private void WriteRowsToBytes(List<byte> bytes)
        {
            for (int y = this.bmp.PixelHeight - 1; y >= 0; --y)
            {
                this.WriteRowToBytes(y, bytes);
            }
        }

        private void WriteRowToBytes(int y, List<byte> bytes)
        {
            int width = this.bmp.PixelWidth;
            byte red, green, blue;
            int colorValue;

            for (int x = 0; x < width; ++x)
            {
                colorValue = this.bmp.Pixels[width * y + x];
                red = (byte)((colorValue >> 16) & 0xFF);
                green = (byte)((colorValue >> 8) & 0xFF);
                blue = (byte)(colorValue & 0xFF);

                bytes.Add(blue);
                bytes.Add(green);
                bytes.Add(red);
            }

            int bytePadding = (4 - (width * 3 % 4)) % 4;
            bytes.AddRange(this.GenerateBlankBytes(bytePadding));
        }

        private IList<byte> GenerateBlankBytes(int length)
        {
            List<byte> bytes = new List<byte>();
            for (int i = 0; i < length; ++i)
            {
                bytes.Add(0);
            }
            return bytes;
        }

        private IList<byte> GenerateLittleEndianNumberOfLength(int value, int length)
        {
            IList<byte> bytes = new List<byte>();

            for (int i = 0; i < length && value != 0; ++i)
            {
                byte digitValue = (byte)(value & 0xFF);
                value = value >> 8;
                bytes.Add(digitValue);
            }

            while (bytes.Count < length)
            {
                bytes.Add(0);
            }

            return bytes;
        }
    }
}