Pixel formats

edit

Every image stores its pixels in one pixel format, such as Rgba32 or Gray16. This page lists the formats, tells which one a file loads as, and shows how to convert between them without losing information by accident.

The formats

FormatBytes per pixelHolds
Rgba3248-bit red, green, blue and alpha: the usual choice for color images
Bgra324The same, in blue, green, red, alpha order, for APIs that expect it
Rgb2438-bit color without alpha
Rgba64816-bit color and alpha
Rgb48616-bit color without alpha
Gray818-bit gray levels
Gray16216-bit gray levels
La1628-bit gray and alpha
La32416-bit gray and alpha
A81An alpha mask: no color, its pixels read as black with their alpha
Argb3248-bit color and alpha in alpha, red, green, blue order, for APIs that expect it
Abgr3248-bit color and alpha in alpha, blue, green, red order, for APIs that expect it
Bgr56525-bit red and blue and 6-bit green packed in 16 bits, for display buffers
Rgba1010102410-bit color and 2-bit alpha packed in 32 bits, for GPU and HDR display buffers
RgbaHalf8Half-precision floating-point color and alpha, in linear light
RgbaFloat16Single-precision floating-point color and alpha, in linear light

Alpha is always straight (not premultiplied), and 16-bit samples and packed values are in the native byte order of the machine. PixelFormats describes each format (HasAlpha, HasColor, IsGrayscale, IsFloatingPoint, GetBytesPerPixel, …).

Image and Image<TPixel>

Image.Load returns an Image whose PixelFormat is the smallest format that holds the file without loss. It is an Image<TPixel> underneath, which you can test with a type pattern. Examples:

SourceUntyped Load gives
8-bit PNG with alpha, palettes, GIFRgba32
16-bit PNGGray16, Rgb48 or Rgba64
Gray PNG with alphaLa16 or La32
JPEGGray8 or Rgb24 (Gray16 or Rgb48 for samples of more than 8 bits)
OpenEXR, floating-point TIFF pagesRgbaHalf or RgbaFloat

Image.Identify tells the format before decoding (ImageInfo.PixelFormat): see Identifying images.

When your code works on pixels of a known type, load in that type with Image.Load<TPixel>, or convert with CloneAs<TPixel>. A type that is not one of the sixteen formats throws a NotSupportedException before anything is read.

Convert between formats

CloneAs<TPixel> returns a converted copy, and Load<TPixel> converts while loading:

public static void ConvertToGray16(string inputPath, string outputPath)
{
    using var image = Image.Load<Rgba64>(inputPath);
    image.AutoOrient();
    using var gray = image.CloneAs<Gray16>(new PixelConversionOptions
    {
        BackgroundColor = new Rgba64(65535, 65535, 65535),
        DiscardIncompatibleColorProfile = true,
    });
    gray.Save(outputPath, new PngEncoder());
}
  • 8 to 16 bits is exact; 16 to 8 bits rounds to the nearest value. A 16-bit conversion never goes through 8 bits.
  • Color to gray uses the Rec. 709 luma of the encoded values.
  • Gray to RGBA and back, opaque Rgb48 to Rgba64 and back, and an alpha mask to any format with alpha and back are exact.
  • A conversion never converts colors between color spaces: the ICC profile of the image is kept when it can still describe the pixels. DiscardIncompatibleColorProfile drops a profile that cannot, such as an RGB profile on gray pixels. To convert colors, see Color management.

Nothing is lost silently

An operation that would lose information throws an UnsupportedImageFeatureException, unless you say how to convert:

LossControl
Alpha, when converting or loading into a format without alphaPixelConversionOptions.BackgroundColor: the pixels are composited over it. Opaque pixels never need one.
Alpha, when saving JPEG, BMP in a layout other than 32-bit, plain Netpbm or a portable bitmapThe BackgroundColor of the encoder
Partial alpha, when saving GIF (one transparent color)GifEncoder.AlphaMode: a threshold (the default) or compositing over a background
16-bit precision, when saving JPEG, WebP, QOI, BMP or TGAAllowBitDepthReduction on the encoder
16-bit precision, when saving AVIF or HEIC (at most 12 bits)BitDepth or AllowBitDepthReduction on the encoder
16-bit precision, when loading or convertingRequest the narrower type explicitly (Load<Rgba32>, CloneAs<Gray8>)
Colors beyond the palette, when saving an indexed PNG or BMPAllowColorReduction on the encoder. GIF always reduces each frame to 256 colors, deterministically: see Color reduction
Floating-point samples outside the range of the destinationPixelConversionOptions.OutOfRangeSamples or ToneMapping
Floating-point samples, when saving a format that stores integersImageEncoder.FloatingPointConversion
Metadata the output cannot storeImageEncoder.MetadataHandling: see Saving images

For example, a JPEG stores neither alpha nor 16-bit samples nor every kind of metadata, so exporting a frame of any image to JPEG sets the three controls:

public static void ExportFrameAsJpeg(string animatedPath, int frameIndex, string jpegPath)
{
    using var animation = Image.Load(animatedPath);

    // The frame is saved as a still image, without copying its pixels
    animation.Frames[frameIndex].Save(jpegPath, new JpegEncoder
    {
        Quality = 85,
        BackgroundColor = new Rgba32(255, 255, 255),
        AllowBitDepthReduction = true,
        MetadataHandling = MetadataHandling.DiscardUnsupported,
    });
}

Compact formats

La16, La32, Rgb48 and A8 hold what many images need in less memory: a gray image with alpha in half the memory of Rgba32 or Rgba64, 16-bit color without alpha in three quarters of the memory of Rgba64, an alpha mask in one byte per pixel. A conversion to A8 keeps the alpha of the source and drops its color:

public static Image<A8> ExtractAlphaMask(string inputPath, string maskPath)
{
    // Untyped loads pick the smallest lossless format: La16 for a gray image with alpha (2 bytes per pixel instead of
    // the 4 of Rgba32), Rgb48 for 16-bit color without alpha
    using var image = Image.Load(inputPath);

    // An alpha mask keeps the alpha of the image in 1 byte per pixel; its pixels read as black
    var mask = image.CloneAs<A8>();

    // PNG stores the mask as gray with alpha, which loads back as La16
    mask.Save(maskPath, new PngEncoder());
    return mask;
}

Every processing operation and the drawing canvas accept these formats, except Flatten on an A8 mask, which has no color to hold the result. The PNG and TIFF encoders store them as they are (a mask as black gray with alpha); the other encoders receive a copy widened without loss to Rgba32 or Rgba64, then apply their own rules, such as the background color a JPEG needs for pixels that are not opaque.

Formats for other APIs

Argb32, Abgr32, Bgr565 and Rgba1010102 exist to exchange pixel buffers with graphics and display APIs without copying them in another layout. Bgr565 has blue in its low bits (DXGI_FORMAT_B5G6R5_UNORM, Android RGB_565), and Rgba1010102 red in its low bits and alpha in its two high bits (DXGI_FORMAT_R10G10B10A2_UNORM, Android RGBA_1010102). No file loads in these formats: import a buffer with Image.ImportPixelData, or convert an image with CloneAs.

public static Image<Rgba1010102> ConvertDisplayBuffer(ReadOnlySpan<Bgr565> frameBuffer, int width, int height)
{
    // Bgr565 is the 16-bit layout of display buffers (blue in the low bits): the pixels are copied as they are
    using var image = Image.ImportPixelData<Bgr565>(frameBuffer, width, height);

    // Every operation works on it like on Rgb24 and stores the result back in 5, 6 and 5 bits
    image.Flip(FlipMode.Horizontal);

    // The components convert to the nearest samples of any other format, here 10-bit colors and a 2-bit alpha
    return image.CloneAs<Rgba1010102>();
}
  • A packed component converts to the nearest sample of the other formats, and back exactly from 8 bits (Bgr565) or 16 bits (Rgba1010102). Converting to a packed format keeps fewer levels: 32 or 64 per color for Bgr565, 1024 per color and 4 for the alpha for Rgba1010102.
  • Every operation, the drawing canvas and every encoder accept them. They work on Rgba32 (for Argb32 and Abgr32), Rgb24 (for Bgr565) or Rgba64 (for Rgba1010102) and store the result back, in a temporary copy of each frame, so prefer one of these formats for a long series of edits.

Floating-point and high dynamic range

RgbaHalf and RgbaFloat hold linear light, where 1 is the reference white: a component above 1 is a highlight, a negative one is outside the gamut of the primaries, and alpha is a coverage from 0 to 1. OpenEXR files and the floating-point pages of TIFF files load in these formats; Load<RgbaFloat> converts any other file.

Integer formats cannot hold values outside [0, 1], so a conversion to them throws on such samples unless you compress the highlights with a tone mapping or clip them. Encoders work the same way: only OpenEXR and TIFF store floating-point samples, and the others need FloatingPointConversion:

public static Image<Rgba64> ToneMapHighlights(Image<RgbaFloat> hdr, string pngPath)
{
    // Integer pixels cannot store the highlights: compress them with a tone mapping, or clip them
    var toneMapped = hdr.CloneAs<Rgba64>(new PixelConversionOptions
    {
        ToneMapping = new HdrToneMappingOptions { Operator = HdrToneMappingOperator.Reinhard },
    });

    // Encoders store integers too: the encoder says how to reduce the samples
    hdr.Save(pngPath, new PngEncoder
    {
        FloatingPointConversion = new PixelConversionOptions { OutOfRangeSamples = OutOfRangeSampleHandling.Clip },
        MetadataHandling = MetadataHandling.DiscardUnsupported,
    });
    return toneMapped;
}
ConversionResult
Integer to RgbaFloatExact, and back to the same integer for every sample
Integer to RgbaHalfExact back for 8-bit samples, not for 16-bit ones (a half has 11 significant bits)
RgbaHalf to RgbaFloatExact
RgbaFloat to RgbaHalfNearest; a finite sample beyond ±65,504 is rejected unless OutOfRangeSamples is Clip
Floating point to integerA sample outside [0, 1] is rejected unless ToneMapping is set or OutOfRangeSamples is Clip; a sample that is not a number is always rejected

Every check runs on every frame before anything is allocated. A conversion keeps the transfer function label of its source: an sRGB Rgba64 image becomes sRGB-encoded floats, not linear light. See Color management and OpenEXR.

See also

  • Color values: named colors, hexadecimal notations and the other color types.