Table of Contents

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.