Creating Bitmaps from Scratch in C#
This project how to represent bitmap data in a plain old C object (POCO) to create images from scratch using C# and no dependencies. Common graphics libraries like SkiaSharp, ImageSharp, System.Drawing, and Maui.Graphics can read and write bitmaps in memory, so a POCO that stores image data and converts it to a bitmap byte allows creation of platform-agnostic APIs that can be interfaced from any graphics library.
This page demonstrates how to use C# (.NET 6.0) to create bitmap images from scratch. Bitmap images can then be saved to disk and viewed with any image editing program, or they can consumed as a byte array in memory by a graphics library. There are various bitmap image formats (grayscale, indexed colors, 16-bit, 32-bit, transparent, etc.) but code here demonstrates the simplest common case (8-bit RGB color).
Representing Color
The following struct
represents RGB color as 3 byte
values and has helper methods for creating new colors.
public struct RawColor
{
public readonly byte R, G, B;
public RawColor(byte r, byte g, byte b)
{
(R, G, B) = (r, g, b);
}
public static RawColor Random(Random rand)
{
byte r = (byte)rand.Next(256);
byte g = (byte)rand.Next(256);
byte b = (byte)rand.Next(256);
return new RawColor(r, g, b);
}
public static RawColor Gray(byte value)
{
return new RawColor(value, value, value);
}
}
A color class like this could be extended to support additional niceties. Refer to SkiaSharp's SKColor.cs
, System.Drawing's Color.cs
, and Maui.Graphics' Color.cs
for examples and implementation details. I commonly find the following features useful include when writing a color class:
- A static class with named colors e.g.,
RawColors.Blue
- Conversion to/from ARGB e.g.,
RawColor.FromAGRB(123456)
- Conversion to/from HTML e.g.,
RawColor.FromHtml(#003366)
- Conversion between RGB and HSL/HSV
- Helper functions to
Lighten()
andDarken()
- Helper functions to
ShiftHue()
- Extension methods to convert to common other formats like
SKColor
Representing the Bitmap Image
This is the entire image class and it serves a few specific roles:
- Store image data in a byte array arranged identically to how it will be exported in the bitmap
- Provide helper methods to get/set pixel color
- Provide a method to return the image as a bitmap by adding a minimal header
public class RawBitmap
{
public readonly int Width;
public readonly int Height;
private readonly byte[] ImageBytes;
public RawBitmap(int width, int height)
{
Width = width;
Height = height;
ImageBytes = new byte[width * height * 4];
}
public void SetPixel(int x, int y, RawColor color)
{
int offset = ((Height - y - 1) * Width + x) * 4;
ImageBytes[offset + 0] = color.B;
ImageBytes[offset + 1] = color.G;
ImageBytes[offset + 2] = color.R;
}
public byte[] GetBitmapBytes()
{
const int imageHeaderSize = 54;
byte[] bmpBytes = new byte[ImageBytes.Length + imageHeaderSize];
bmpBytes[0] = (byte)'B';
bmpBytes[1] = (byte)'M';
bmpBytes[14] = 40;
Array.Copy(BitConverter.GetBytes(bmpBytes.Length), 0, bmpBytes, 2, 4);
Array.Copy(BitConverter.GetBytes(imageHeaderSize), 0, bmpBytes, 10, 4);
Array.Copy(BitConverter.GetBytes(Width), 0, bmpBytes, 18, 4);
Array.Copy(BitConverter.GetBytes(Height), 0, bmpBytes, 22, 4);
Array.Copy(BitConverter.GetBytes(32), 0, bmpBytes, 28, 2);
Array.Copy(BitConverter.GetBytes(ImageBytes.Length), 0, bmpBytes, 34, 4);
Array.Copy(ImageBytes, 0, bmpBytes, imageHeaderSize, ImageBytes.Length);
return bmpBytes;
}
public void Save(string filename)
{
byte[] bytes = GetBitmapBytes();
File.WriteAllBytes(filename, bytes);
}
}
Generate Images from Scratch
The following code uses the bitmap class and color struct above to create test images
Random Colors
RawBitmap bmp = new(400, 300);
Random rand = new();
for (int x = 0; x < bmp.Width; x++)
for (int y = 0; y < bmp.Height; y++)
bmp.SetPixel(x, y, RawColor.Random(rand));
bmp.Save("random-rgb.bmp");
Rainbow
RawBitmap bmp = new(400, 300);
Random rand = new();
for (int x = 0; x < bmp.Width; x++)
{
for (int y = 0; y < bmp.Height; y++)
{
byte r = (byte)(255.0 * x / bmp.Width);
byte g = (byte)(255.0 * y / bmp.Height);
byte b = (byte)(255 - 255.0 * x / bmp.Width);
RawColor color = new(r, g, b);
bmp.SetPixel(x, y, color);
}
}
bmp.Save("rainbow.bmp");
Rectangles
RawBitmap bmp = new(400, 300);
Random rand = new();
for (int i = 0; i < 1000; i++)
{
int rectX = rand.Next(bmp.Width);
int rectY = rand.Next(bmp.Height);
int rectWidth = rand.Next(50);
int rectHeight = rand.Next(50);
RawColor color = RawColor.Random(rand);
for (int x = rectX; x < rectX + rectWidth; x++)
{
for (int y = rectY; y < rectY + rectHeight; y++)
{
if (x < 0 || x >= bmp.Width) continue;
if (y < 0 || y >= bmp.Height) continue;
bmp.SetPixel(x, y, color);
}
}
}
bmp.Save("rectangles.bmp");
Interfacing Graphics Libraries
The following code demonstrates how to load the bitmap byte arrays generated above into common graphics libraries and save the result as a JPEG file. Although the bitmap byte array can be written directly to disk as a .bmp file, these third-party libraries are required to encode images in additional formats like JPEG.
System.Drawing
using System.Drawing;
static void SaveBitmap(byte[] bytes, string filename = "demo.jpg")
{
using MemoryStream ms = new(bytes);
using Image img = Bitmap.FromStream(ms);
img.Save(filename);
}
SkiaSharp
using SkiaSharp;
static void SaveBitmap(byte[] bytes, string filename = "demo.jpg")
{
using SKBitmap bmp = SKBitmap.Decode(bytes);
using SKFileWStream fs = new(filename);
bmp.Encode(fs, SKEncodedImageFormat.Jpeg, quality: 95);
}
ImageSharp
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
static void SaveBitmap(byte[] bytes, string filename = "demo.jpg")
{
using Image img = Image.Load(bytes);
JpegEncoder encoder = new() { Quality = 95 };
img.Save(filename, encoder);
}
Resources
-
C# Data Visualization - Resources for visualizing data using C# and the .NET platform
--- Title: Creating Bitmaps from Scratch in C# Description: How to create a bitmap and set pixel colors in memory and save the result to disk or convert it to a traditional image format Date: 2022-11-07 18:45PM EST Tags: csharp, graphics --- # Creating Bitmaps from Scratch in C# **This project how to represent bitmap data in a plain old C object (POCO) to create images from scratch using C# and no dependencies.** Common graphics libraries like [SkiaSharp](https://swharden.com/csdv/skiasharp/), [ImageSharp](https://swharden.com/csdv/platforms/imagesharp/), [System.Drawing](https://swharden.com/csdv/system.drawing/), and [Maui.Graphics](https://swharden.com/csdv/maui.graphics/) can read and write bitmaps in memory, so a [POCO](https://en.wikipedia.org/wiki/POCO) that stores image data and converts it to a bitmap byte allows creation of platform-agnostic APIs that can be interfaced from any graphics library. **This page demonstrates how to use C# (.NET 6.0) to create bitmap images from scratch.** Bitmap images can then be saved to disk and viewed with any image editing program, or they can consumed as a byte array in memory by a graphics library. There are various [bitmap image formats](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/advanced/types-of-bitmaps?view=netframeworkdesktop-4.8) (grayscale, indexed colors, 16-bit, 32-bit, transparent, etc.) but code here demonstrates the simplest common case (8-bit RGB color). ## Representing Color The following `struct` represents RGB color as 3 `byte` values and has helper methods for creating new colors. ```cs public struct RawColor { public readonly byte R, G, B; public RawColor(byte r, byte g, byte b) { (R, G, B) = (r, g, b); } public static RawColor Random(Random rand) { byte r = (byte)rand.Next(256); byte g = (byte)rand.Next(256); byte b = (byte)rand.Next(256); return new RawColor(r, g, b); } public static RawColor Gray(byte value) { return new RawColor(value, value, value); } } ``` A color class like this could be extended to support additional niceties. Refer to [SkiaSharp's `SKColor.cs`](https://github.com/mono/SkiaSharp/blob/main/binding/Binding/SKColor.cs), [System.Drawing's `Color.cs`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Drawing.Primitives/src/System/Drawing/Color.cs), and [Maui.Graphics' `Color.cs`](https://github.com/dotnet/maui/blob/main/src/Graphics/src/Graphics/Color.cs) for examples and implementation details. I commonly find the following features useful include when writing a color class: * A static class with named colors e.g., `RawColors.Blue` * Conversion to/from ARGB e.g., `RawColor.FromAGRB(123456)` * Conversion to/from HTML e.g., `RawColor.FromHtml(#003366)` * Conversion between RGB and [HSL/HSV](https://en.wikipedia.org/wiki/HSL_and_HSV) * Helper functions to `Lighten()` and `Darken()` * Helper functions to `ShiftHue()` * Extension methods to convert to common other formats like `SKColor` ## Representing the Bitmap Image This is the entire image class and it serves a few specific roles: * Store image data in a byte array arranged identically to how it will be exported in the bitmap * Provide helper methods to get/set pixel color * Provide a method to return the image as a bitmap by adding a minimal header ```cs public class RawBitmap { public readonly int Width; public readonly int Height; private readonly byte[] ImageBytes; public RawBitmap(int width, int height) { Width = width; Height = height; ImageBytes = new byte[width * height * 4]; } public void SetPixel(int x, int y, RawColor color) { int offset = ((Height - y - 1) * Width + x) * 4; ImageBytes[offset + 0] = color.B; ImageBytes[offset + 1] = color.G; ImageBytes[offset + 2] = color.R; } public byte[] GetBitmapBytes() { const int imageHeaderSize = 54; byte[] bmpBytes = new byte[ImageBytes.Length + imageHeaderSize]; bmpBytes[0] = (byte)'B'; bmpBytes[1] = (byte)'M'; bmpBytes[14] = 40; Array.Copy(BitConverter.GetBytes(bmpBytes.Length), 0, bmpBytes, 2, 4); Array.Copy(BitConverter.GetBytes(imageHeaderSize), 0, bmpBytes, 10, 4); Array.Copy(BitConverter.GetBytes(Width), 0, bmpBytes, 18, 4); Array.Copy(BitConverter.GetBytes(Height), 0, bmpBytes, 22, 4); Array.Copy(BitConverter.GetBytes(32), 0, bmpBytes, 28, 2); Array.Copy(BitConverter.GetBytes(ImageBytes.Length), 0, bmpBytes, 34, 4); Array.Copy(ImageBytes, 0, bmpBytes, imageHeaderSize, ImageBytes.Length); return bmpBytes; } public void Save(string filename) { byte[] bytes = GetBitmapBytes(); File.WriteAllBytes(filename, bytes); } } ``` ## Generate Images from Scratch The following code uses the bitmap class and color struct above to create test images ### Random Colors ```cs RawBitmap bmp = new(400, 300); Random rand = new(); for (int x = 0; x < bmp.Width; x++) for (int y = 0; y < bmp.Height; y++) bmp.SetPixel(x, y, RawColor.Random(rand)); bmp.Save("random-rgb.bmp"); ```  ### Rainbow ```cs RawBitmap bmp = new(400, 300); Random rand = new(); for (int x = 0; x < bmp.Width; x++) { for (int y = 0; y < bmp.Height; y++) { byte r = (byte)(255.0 * x / bmp.Width); byte g = (byte)(255.0 * y / bmp.Height); byte b = (byte)(255 - 255.0 * x / bmp.Width); RawColor color = new(r, g, b); bmp.SetPixel(x, y, color); } } bmp.Save("rainbow.bmp"); ```  ### Rectangles ```cs RawBitmap bmp = new(400, 300); Random rand = new(); for (int i = 0; i < 1000; i++) { int rectX = rand.Next(bmp.Width); int rectY = rand.Next(bmp.Height); int rectWidth = rand.Next(50); int rectHeight = rand.Next(50); RawColor color = RawColor.Random(rand); for (int x = rectX; x < rectX + rectWidth; x++) { for (int y = rectY; y < rectY + rectHeight; y++) { if (x < 0 || x >= bmp.Width) continue; if (y < 0 || y >= bmp.Height) continue; bmp.SetPixel(x, y, color); } } } bmp.Save("rectangles.bmp"); ```  ## Interfacing Graphics Libraries **The following code demonstrates how to load the bitmap byte arrays generated above into common graphics libraries and save the result as a JPEG file.** Although the bitmap byte array can be written directly to disk as a .bmp file, these third-party libraries are required to encode images in additional formats like JPEG. ### System.Drawing ```cs using System.Drawing; static void SaveBitmap(byte[] bytes, string filename = "demo.jpg") { using MemoryStream ms = new(bytes); using Image img = Bitmap.FromStream(ms); img.Save(filename); } ``` ### SkiaSharp ```cs using SkiaSharp; static void SaveBitmap(byte[] bytes, string filename = "demo.jpg") { using SKBitmap bmp = SKBitmap.Decode(bytes); using SKFileWStream fs = new(filename); bmp.Encode(fs, SKEncodedImageFormat.Jpeg, quality: 95); } ``` ### ImageSharp ```cs using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; static void SaveBitmap(byte[] bytes, string filename = "demo.jpg") { using Image img = Image.Load(bytes); JpegEncoder encoder = new() { Quality = 95 }; img.Save(filename, encoder); } ``` # Resources * [C# Data Visualization](https://swharden.com/csdv/) - Resources for visualizing data using C# and the .NET platform * [SkiaSharp: `SKColor.cs`](https://github.com/mono/SkiaSharp/blob/main/binding/Binding/SKColor.cs) * [System.Drawing: `Color.cs`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Drawing.Primitives/src/System/Drawing/Color.cs) * [Maui.Graphics: `Color.cs`](https://github.com/dotnet/maui/blob/main/src/Graphics/src/Graphics/Color.cs)