Pixel types
A raster stores one pixel type, chosen when you load or create it. Pick the type your data is in, or the one you will work in: loading straight into it converts row by row while decoding, with no full-size copy in between.
// Load straight into the type you will work in: the decoder converts row by row, with no full-size copy in between.
using Raster<Rgb16> deep = Raster.Load<Rgb16>("duck.png"); // 16 bits a channel, no alpha
using Raster<Alpha8> mask = Raster.Load<Alpha8>("duck.png"); // only the alpha channel
using Raster<RgbaF16> half = Raster.Load<RgbaF16>("duck.png"); // half floats, room for HDR values
// Or convert a raster you have; conversions between integer types are exact wherever the target can hold the value.
using Raster<Bgr565> display = deep.As<Bgr565>();
The types
| Type | Bytes | Holds | Typical use |
|---|---|---|---|
Rgba8, Bgra8 |
4 | 8-bit color, straight alpha | The default working type; Windows and GPU buffers |
Argb8, Abgr8 |
4 | 8-bit color, alpha first | Java and other alpha-first buffers |
Rgb8, Bgr8 |
3 | 8-bit color | Photos without alpha |
Gray8, GrayAlpha8 |
1, 2 | 8-bit gray | Grayscale images |
Alpha8 |
1 | Alpha only | Masks, glyph coverage |
Rgba16, Rgb16 |
8, 6 | 16-bit color, with or without alpha | 16-bit PNG, PPM and TIFF |
Gray16, GrayAlpha16 |
2, 4 | 16-bit gray | 16-bit gray PNG and PAM |
RgbaF32, RgbF32, GrayF32 |
16, 12, 4 | 32-bit float | Precise processing, PFM, depth and height maps |
RgbaF16 |
8 | 16-bit half float | HDR, GPU textures |
Bgr565, Bgra4444, Rgb10A2 |
2, 2, 4 | Packed color | Embedded displays, legacy textures, 10-bit displays |
Bgr555, Bgra5551 |
2, 2 | Packed 5-5-5 color, with a 1-bit alpha | 16-bit BMP and TGA, GDI+ 16-bit bitmaps |
Cmyk8, Cmyk16 |
4, 8 | Ink amounts | CMYK JPEG, TIFF and PSD |
PremulRgba8, PremulBgra8, PremulRgbaF32 |
4, 4, 16 | Color premultiplied by alpha | Compositing, Windows and GPU surfaces |
Packed types are named after their channels from the lowest bit up, as DXGI and Vulkan name formats, so a Direct3D or Vulkan
texture wraps without a swizzle: Bgr565 has blue in bits 0 to 4 and red in 11 to 15, the same memory as Skia's and Android's
RGB 565.
Conversions
Every type converts to every other with raster.As<TPixel>() or, for a span, PixelConvert.Convert. Conversions between
integer types are exact wherever the target can hold the value: 8 to 16 bits multiplies by 257, packed levels round to the
nearest value both ways, and 16 to 8 bits rounds to nearest, so every value of a narrower type survives a round trip through a
wider one. Half floats round as (Half)f does.
Values are sRGB-encoded with straight alpha. Gray types take the Rec. 709 luma in linear light; Alpha8 reads as black with its
alpha, and converting a color to it keeps only alpha.
Half and float types keep values outside 0..1 through conversions, geometry and saving to PFM; filters and color adjustments
work in 0..1 and clip, as they do for RgbaF32.
// Half and float types keep values above 1 through conversions and geometry; filters and color adjustments clip to 0..1.
using Raster<RgbaF16> light = Raster.Create<RgbaF16>(64, 64, RgbaF16.FromScaled(new Vector4(4f, 2f, 0.5f, 1f)));
light.Apply(ops => ops.FlipHorizontal().Pad(80, 80, Rgba8.Black));
// PFM keeps floats as they are: save as RGB floats, in linear light by convention.
light.Save("light.pfm");
using Raster<RgbF32> back = Raster.Load<RgbF32>("light.pfm");
Console.WriteLine($"Red is still {back.Row(40)[40].R} after the round trip");
CMYK
Cmyk8 and Cmyk16 hold ink amounts, 0 meaning no ink. Until Egami applies ICC profiles, they convert to and from RGB with the
plain formula, red = (1 − C)(1 − K) — what the JPEG reader has always used — so print-accurate color needs the file's profile.
// A CMYK JPEG loads as RGB by the plain formula, as before; ask for Cmyk8 to get the file's ink amounts instead.
using Raster<Cmyk8> inks = Raster.Load<Cmyk8>("duck-cmyk.jpg");
Cmyk8 corner = inks.Row(0)[0];
Console.WriteLine($"Cyan {corner.C}, magenta {corner.M}, yellow {corner.Y}, black {corner.K} (0 = no ink)");
// Resizing filters the four inks, so black stays in the black channel; color adjustments go through RGB instead.
inks.Apply(ops => ops.Fit(160, 160));
Moving pixels (crop, pad, flip, turning by 90°) copies inks. Resize, blur, sharpen, convolution and free rotation filter the four
inks as numbers, so black stays in the black channel. Color adjustments are defined on RGB, so they convert to RGB and back and
rebuild black from the formula. DrawImage onto a CMYK raster turns the source's color into inks and blends inks.
Premultiplied alpha
The Premul types store each color channel multiplied by alpha, as Skia and GPU surfaces do. Their conversions still take and
return straight colors, so loading, saving and converting work as for any type. Resize, blur, convolution, rotation and
DrawImage run on the stored values directly, without multiplying and dividing per pixel: on translucent images resize is about
30% and DrawImage about 35% faster than on Rgba8.
// A Windows or GPU surface is premultiplied BGRA: wrap its memory as PremulBgra8 and draw into it directly. Drawing and
// resizing run on the stored values, with no multiply and divide per pixel; conversions still take straight colors.
using Raster<PremulBgra8> surface = Raster.Wrap<PremulBgra8>(surfaceMemory, 320, 240, byteStride: 320 * 4);
using Raster<Rgba8> logo = Raster.Load<Rgba8>("duck.png");
logo.Apply(ops => ops.Fit(120, 120));
surface.Apply(ops => ops.FillColor(new Rgba8(30, 60, 90, 255)).DrawImage(logo, new Point(20, 20), opacity: 0.8f));
Unpremultiplying loses precision where alpha is low (a color at alpha 10 has 11 levels), so keep straight types for images you will edit color by color.
Packed types
// A 16-bit display framebuffer: wrap it as Bgr565 and draw a photo into it; each level rounds to the nearest of 32 or 64.
using Raster<Bgr565> screen = Raster.Wrap<Bgr565>(MemoryMarshal.Cast<ushort, Bgr565>(framebuffer.AsSpan()).ToArray(), 240, 135);
using Raster<Rgba8> photo = Raster.Load<Rgba8>("egret.png");
photo.Apply(ops => ops.Cover(240, 135));
screen.Apply(ops => ops.DrawImage(photo, new Point(0, 0)));
Bgr565 pixel = screen.Row(60)[120];
Console.WriteLine($"Levels: red {pixel.R} of 31, green {pixel.G} of 63, blue {pixel.B} of 31");
Bgr565, Bgra4444, Bgr555, Bgra5551 and Rgb10A2 keep their bits in Value, with level constructors and properties
for each channel. BMP files with 5-6-5, 5-5-5, 1-5-5-5 or 4-4-4-4 bit fields and 15 and 16-bit TGA files read straight into them.
Bgr555 leaves its top bit 0; Bgra5551 keeps alpha in it, 1 for opaque, and alpha 128 and above narrows to opaque.
Your own pixel type
Implement IPixelType<TSelf> on an unmanaged struct. Layout says how it is stored: a packed layout gives its pixel size and
channel widths with new PixelLayoutInfo(order, PixelChannelType.Packed, alpha, bitsPerPixel, widths...), and a layout Egami
has no name for uses PixelChannelOrder.Other. The 16-bit members ToRgba16 and FromRgba16 have default bodies; override them
with exact integer code if your type is wider than 8 bits. See custom operations and formats.
Run it
samples/PixelTypes is a console app that does what this guide
shows on the bundled images: dotnet run --project samples/PixelTypes writes its results to ./output.