Formats
Egami reads and writes PNG, APNG, JPEG, GIF, WebP, BMP, TGA, QOI and the Netpbm formats (PBM, PGM, PPM, PAM and the PFM float map), TIFF, Radiance .hdr, OpenEXR, JPEG XL, AVIF, and Windows icons and cursors, and reads JBIG2, raw CCITT fax data, JPEG 2000, Windows animated cursors (ANI) and Photoshop's PSD and PSB, layers included. HEIC phone photos load and save through the separate, opt-in Egami.Heic package; core Egami detects and probes them. SVG and Windows metafiles (WMF, EMF) load through the separate Egami.Svg and Egami.Metafiles packages, and PCX, SGI, Sun raster, XBM, XPM, WBMP Farbfeld, JPEG-LS, DDS and KTX textures, DPX and Cineon film frames, FITS, camera RAW (DNG and the camera makers' formats, with DNG written) and, as a preview, DICOM images and datasets through Egami.Formats.Extended. The Package column says which package holds each format. Every reader checks sizes against the safety limits before it allocates, and every reader has been fuzzed.
Each format has its own page with its options and what it keeps from the file; the table below links them. VP8 and AV1 video frames decode
through Vp8StreamDecoder and Av1StreamDecoder, VP8 and AV1 frames encode through Vp8StreamEncoder and Av1StreamEncoder, and the boxes of MP4, QuickTime and HEIF files can be walked with IsoBoxReader. This page covers what all formats share: natural pixel types, detection, files with several images, and what writers drop.
At a glance
| Format | Reads | Writes | Natural pixel type | Package |
|---|---|---|---|---|
| PNG | Every color type and bit depth, interlaced, transparency, text, EXIF, XMP, ICC | Every color type and bit depth, adaptive filtering, built palettes, Adam7 interlacing, compression on several cores | The file's own: Gray8, Gray16, GrayAlpha8, Rgb8, Rgba8 or Rgba16 |
Egami |
| APNG | Every PNG color type and depth, every disposal and blend | Any raster with more than one frame, changed rectangles only | Rgba8, or Rgba16 for 16-bit |
Egami |
| JPEG | Baseline, extended and progressive, Huffman or arithmetic, 8 and 12-bit, lossless at 2 to 16 bits, any subsampling, gray, CMYK, YCCK, scaled decode, EXIF, XMP, ICC | Baseline or progressive, 8 or 12-bit, quality 1 to 100, 4:4:4, 4:2:2, 4:2:0, gray, CMYK or YCCK, optimized Huffman tables or arithmetic coding; lossless at 2 to 16 bits | Gray8 or Rgb8; Gray16, Rgb16 or Cmyk16 above 8 bits |
Egami |
| GIF | 87a and 89a, every disposal, interlaced, transparency, loop count, comments, XMP, ICC | 89a, exact or quantized palettes, global or per frame, dithering, frame optimization | Rgba8 |
Egami |
| WebP | Lossless (VP8L) with every transform; lossy (VP8) with alpha, as RGB or as YUV planes; ICC, EXIF, XMP | Lossless at effort 0 to 6, exact transparent colors; ICC, EXIF with resolution, XMP | Rgb8, or Rgba8 with alpha |
Egami |
| BMP | Core, info, V2 to V5 and OS/2 2.x headers; 1 to 32 bits; RLE4, RLE8, RLE24, OS/2 Huffman 1D; bit fields; embedded PNG and JPEG; V5 ICC | 24-bit, 32-bit with alpha, 1, 2, 4 or 8-bit palettes, RLE4 and RLE8; resolution and ICC | Bgr8, or Bgra8 with alpha |
Egami |
| TGA | Color-mapped, true-color and gray, raw and RLE, 8 to 32 bits, all origins, TGA 2.0 footer and extension area | 8-bit gray, 24 and 32-bit, 8-bit palette, raw or RLE, TGA 2.0 footer | Bgr8, Bgra8, Gray8 or GrayAlpha8 |
Egami |
| QOI | The whole format, 3 or 4 channels, both colorspaces | 3 or 4 channels, byte-identical to the reference encoder | Rgb8 or Rgba8 |
Egami |
| TIFF | Every page, strips and tiles, chunky and planar, classic and BigTIFF; None, LZW, Deflate, PackBits, CCITT G3 and G4, JPEG, old-style JPEG, ThunderScan, SGILog, WebP, JPEG XL, Zstd, LZMA, lossless JPEG at 2 to 16 bits; gray, palette, RGB, CMYK, YCbCr, Lab and LogLuv at 1 to 64 bits | Every pixel type at its own depth, strips or tiles, classic or BigTIFF; None, LZW, Deflate, Zstd, LZMA, PackBits, CCITT G3 and G4, JPEG (lossless at Quality 100), WebP |
The page's own: Gray8, Gray16, Rgb8, Rgb16, Cmyk8, Cmyk16, float types and more |
Egami |
| CCITT | Raw Group 3 and Group 4 data (T.4, T.6) with PDF's eight CCITTFaxDecode parameters, byte-aligned rows included | No | Gray8, black 0 and white 255 |
Egami |
| JBIG2 | Every T.88 region and dictionary but the colour extension; standalone files (sequential and random-access, many pages) and the embedded form a PDF holds, with its globals | No | Gray8, black 0 and white 255 |
Egami |
| JPEG 2000 | All of Part 1 (T.800): every progression order, code-block style, tiling and precinct size, reversible and irreversible; JP2 and JPX boxes, palettes, channel definitions, sYCC, CIELab, ICC; reduced and region decodes | No | Gray8, Rgb8, Rgba8, Cmyk8 or their 16-bit forms, by depth and color space |
Egami |
| ICO, CUR | Every entry: PNG, and BMP at 1 to 32 bits with the AND mask; the best entry, or one by MinimumSize or index; cursor hotspots |
One entry a raster, or sets resized to favicon or Windows sizes; PNG at 256 and 32-bit BMP below; hotspots | Rgba8 |
Egami |
| ICNS | Every entry: PNG, JPEG 2000, RLE ARGB, RLE RGB with its 8-bit mask, and the old 1, 4 and 8-bit icons with their 1-bit mask; the best entry, or one by MinimumSize or index |
One PNG entry a raster, or sets resized to the macOS sizes 16 to 1024 | Rgba8 |
Egami |
| ANI | Animated cursors: every step as a frame with its delay, one size for all frames or every size with LoadAll; cursor, icon and raw-bitmap frames; hotspots, title and artist | Yes: frames stored once as cursors, at their own size or a set of sizes, with hotspots, delays, title and artist | Rgba8 |
Egami |
| PSD, PSB | The flattened image in every color mode (bitmap, gray, indexed, RGB, CMYK, Lab, duotone, multichannel) at 1 to 32 bits, raw, PackBits and Deflate; every layer with its pixels and mask (PsdImage.ReadLayers); resources as metadata |
No | By mode and depth: Gray8, Rgb8, Rgba8, Cmyk8, their 16-bit forms, float types |
Egami |
| Radiance | RGBE and XYZE, every scan order, flat, old repeat-pixel and per-channel run-length scanlines | RGBE, run-length encoded or flat, optional EXPOSURE line | RgbF32, linear |
Egami |
| OpenEXR | Scanline and tiled, single and multi-part, every OpenEXR 3 compression but HTJ2K, half, float and uint channels, luminance-chroma; any part, layer or channel | One scanline part, R, G, B and A, ZIP, ZIPS, RLE, PIZ or none, in half or float | RgbaF16 when the channels are half, RgbaF32 otherwise, GrayF32 for luminance alone; linear |
Egami |
| JPEG XL | Every still, lossless Modular and lossy VarDCT or Modular, at 1 to 31 bits and float, with alpha, CMYK, extra channels, animations and layers, previews, PQ and HLG, gain maps, and recompressed JPEGs (given back byte for byte) | Lossless in the raster's own precision, or lossy VarDCT in XYB, at cjxl's effort and quality scales; JPEGs recompressed losslessly | By bit depth: Rgb8, Rgba8, Gray8, their 16-bit forms, RgbaF16 for 16-bit float, RgbF32, RgbaF32 or GrayF32 beyond |
Egami |
| AVIF | Still images: AV1 intra frames with every coding tool, equal to dav1d's decode and converted to RGB as libavif does; alpha, grids, overlays, clean aperture, orientation, ICC, CICP, HDR, gain maps, every item, and animated sequences | Yes: lossy and lossless still images and animations with alpha, 8, 10 and 12 bits, 4:2:0, 4:2:2, 4:4:4 and 4:0:0 | By bit depth: Rgb8 or Rgba8 for 8-bit files, Rgb16 or Rgba16 for 10 and 12 bits, Gray8 or Gray16 (with alpha, GrayAlpha8 or GrayAlpha16) for monochrome |
Egami |
| HEIC | With Egami.Heic: HEVC pictures up to 12 bits, grids, alpha, gain maps, depth maps, mattes and image sequences with inter frames; core Egami probes them | With Egami.Heic: lossy and lossless stills, 8 to 12 bits, 4:0:0 to 4:4:4, alpha, grids, gain maps, thumbnails | Rgb8 for 8-bit files, Rgb16 for 10 and 12 bits, with alpha when the file has it, gray for monochrome |
Egami.Heic |
| PNM | P1 to P6 plain and binary, maxval 1 to 65,535, P7 PAM with alpha, PF and Pf float maps | P1 to P7, plain or binary; PF and Pf | Gray8, Gray16, GrayAlpha8, Rgb8, Rgba8 or Rgba16; RgbF32 or GrayF32 for PFM |
Egami |
| PCX | 1 to 8-bit indexed, EGA planes, 24 and 32-bit, coded or not | 8-bit gray or 24-bit, run-length coded | Gray8, Rgb8 or Rgba8 |
Egami.Formats.Extended |
| SGI | 1 to 4 channels of 8 or 16 bits, plain or run-length coded | The raster's channels and depth, coded or not | Gray8, GrayAlpha8, Rgb8, Rgba8 or their 16-bit forms |
Egami.Formats.Extended |
| Sun raster | 1, 8, 24 and 32 bits, colormaps, byte-encoded | 8-bit gray or 24-bit, plain or byte-encoded | Gray8 or Rgb8 |
Egami.Formats.Extended |
| XBM | X11 and X10 bitmaps | X11 bitmaps | Gray8, black 0 and white 255 |
Egami.Formats.Extended |
| XPM | Version 3, any color keys, None, hex colors and X11 names | Version 3 with every color of the raster | Rgba8 |
Egami.Formats.Extended |
| WBMP | Type 0 | Type 0 | Gray8, black 0 and white 255 |
Egami.Formats.Extended |
| Farbfeld | The whole format | 16-bit RGBA | Rgba16 |
Egami.Formats.Extended |
| JPEG-LS | T.87 lossless and near-lossless, 2 to 16 bits, every interleave mode, restart markers, mapping tables, subsampling and HP colour transforms | Lossless and near-lossless, 2 to 16 bits, 1 to 4 components, HP colour transforms | Gray8, GrayAlpha8, Rgb8, Rgba8 or their 16-bit forms |
Egami.Formats.Extended |
| DDS | Legacy and DX10 headers, BC1 to BC7, every DXGI format DirectXTex reads; mips, cube maps, arrays and volumes | Uncompressed formats and BC1 to BC7, with mips, arrays and cube maps | The nearest to the stored format: Rgba8, Gray8, Rgb8, Rgba16, RgbaF16, RgbaF32 and others |
Egami.Formats.Extended |
| KTX | KTX 1.1 and KTX 2.0, uncompressed or Zstandard and zlib supercompressed; BC1 to BC7 and the uncompressed Vulkan and OpenGL formats; mips, cube maps, arrays and volumes | No | As DDS: Rgba8, Gray8, Rgb8, Rgba16, RgbaF16, RgbaF32 and others |
Egami.Formats.Extended |
| DPX and Cineon | DPX 1.0 to 3.0 and Cineon 4.5: 1 to 16-bit integer and 32 and 64-bit float samples, every packing and byte order, luma, RGB, RGBA and CbYCr, run-length coding; every image element; log to linear on request | DPX: gray, RGB or RGBA at 8, 10, 12 or 16 bits; Cineon: 10-bit RGB | The stored code values: Rgb16, Rgba16, Gray16 or their 8-bit forms; RgbF32 and others for float or linearized |
Egami.Formats.Extended |
| FITS | Image HDUs and cube planes, BITPIX 8 to 64 and floats, BSCALE, BZERO and BLANK, tile-compressed images (Rice, gzip, PLIO); stretched for viewing or as physical values | BITPIX 8, 16 or -32, gray or a three-plane colour cube, with the header cards | GrayF32, or RgbF32 for a colour cube |
Egami.Formats.Extended |
| Camera RAW | DNG 1.0 to 1.7, Canon CR2, Nikon NEF and NRW, Pentax PEF, Sony ARW and SR2, Olympus ORF, Panasonic RW2 and Fujifilm RAF, developed on load (white balance, demosaicing, noise reduction, opcodes, camera profiles, lens corrections), or their previews or sensor mosaic; CR3 previews and metadata | DNG: any RAW file converted with its sensor data unchanged (RawFormat.ConvertToDng), or any raster |
Rgb16 sRGB when developed; Gray16 for the mosaic |
Egami.Formats.Extended |
| DICOM | Preview: the dataset of PS3.10 files (DicomFile), every native transfer syntax and deflated, sequences, character sets, frames decoded as a viewer shows them (rescale, window, VOI LUT, palette, YBR) from native, RLE, JPEG, JPEG-LS, JPEG 2000, HTJ2K and JPEG XL pixel data |
Preview: Secondary Capture from rasters; files saved again in native, RLE, JPEG, JPEG-LS or JPEG XL | — | Egami.Formats.Extended |
Raster.Load returns the natural type; Raster.Load<TPixel> decodes straight into the type you ask for, converting row by row.
using Raster image = Raster.Load("duck.png");
switch (image)
{
case Raster<Rgb8> rgb:
Console.WriteLine($"8-bit color, {rgb.Width} × {rgb.Height}");
break;
case Raster<Rgba8> rgba:
Console.WriteLine($"8-bit color with alpha, {rgba.Width} × {rgba.Height}");
break;
default:
Console.WriteLine($"{image.Layout.ChannelOrder}, {image.Layout.ChannelType}");
break;
}
Detection
Loading never trusts the file extension: the format comes from the first bytes, in the order PNG, JPEG, GIF, WebP, BMP, QOI, PNM, TIFF, JBIG2, JPEG 2000, ICO, CUR, ANI, ICNS, PSD, Radiance, OpenEXR, JPEG XL (FF 0A or its signature box), AVIF (an ftyp box with the avif or avis brand), HEIC (an ftyp box with a HEVC brand such as heic or heix), then TGA, then the formats a plugin package registers, such as Egami.Formats.Extended's. TGA has no signature, so it is matched only when its whole 18-byte header is consistent. No file of another format in Egami's test corpora is taken for TGA, and neither is any of a million random headers. When you know the format, register only that one:
// TGA has no signature, so it is detected last. When you know a file is TGA, register only that format.
EgamiContext tgaOnly = EgamiContext.Default with { Formats = [TgaFormat.Info] };
using Raster image = Raster.Load("duck.tga", tgaOnly);
Data without a signature of its own, such as the JBIG2 or CCITT stream of a PDF image, loads with its format named, which skips detection.
Load, LoadAll, Probe and RasterDecoder.Open all take the format this way; naming the wrong one throws
CorruptDataException, as damaged data of that format would.
// Data without a signature of its own (a TGA here; a JBIG2 stream from a PDF) loads with its format named,
// which skips detection. Probe, LoadAll and RasterDecoder.Open take the format the same way.
using Raster image = Raster.Load(data, TgaFormat.Info);
RasterInfo info = Raster.Probe(data, TgaFormat.Info);
Saving to a path picks the writer from the extension; an animated raster saved as .png writes an APNG. Saving to a stream
takes the format's Info.
Several images in one file
Frames are animation and share one size. A TIFF's pages are separate images, each with its own size and pixel type, so they come
as a RasterList from Raster.LoadAll, which owns them; RasterInfo.ImageCount says how many there are. Every other format
gives its one image, so LoadAll works on any file. Raster.SaveAll writes several rasters to a format that holds several, and
one raster to any format.
// Every page at its own size and pixel type. The list owns the rasters.
using RasterList pages = Raster.LoadAll("scan.tif");
foreach (Raster page in pages)
{
Console.WriteLine($"{page.Width} × {page.Height}");
}
// Keep one page after the list is gone.
using Raster first = pages.Detach(0);
What each writer drops
Writers never change the raster. When a format cannot hold something, it is left out:
- Alpha: JPEG (unless
JpegWriterOptions.Backgroundblends it onto a color), BMP and TGA withBgr24, PPM, QOI withIncludeAlpha = false. - Frames after the first: JPEG, BMP, TGA, QOI, PNM and HEIC.
- Metadata: every writer keeps what its format can carry;
StripMetadata = trueon any writer options drops it all.
See the API reference for every option.
What the file said
Every reader fills RasterMetadata.FormatDetails with the header's own terms: PngDetails, JpegDetails, GifDetails,
BmpDetails, TgaDetails, QoiDetails or PnmDetails. A writer of the same format uses them to keep what it can, and ignores
another format's details.
// Each reader fills in what the file's header says, in the format's own terms.
using Raster png = Raster.Load("duck.png");
if (png.Metadata.FormatDetails is PngDetails pngDetails)
{
Console.WriteLine($"PNG {pngDetails.ColorType}, {pngDetails.BitDepth}-bit, interlaced: {pngDetails.Interlaced}");
foreach (PngTextSource source in pngDetails.TextSources)
{
Console.WriteLine($"Text from a {source.ChunkType} chunk, compressed: {source.Compressed}");
}
}
using Raster bmp = Raster.Load("duck.bmp");
BmpDetails bmpDetails = (BmpDetails)bmp.Metadata.FormatDetails!;
Console.WriteLine($"BMP {bmpDetails.BitsPerPixel}-bit, {bmpDetails.Compression}, {bmpDetails.HeaderSize}-byte header");
using Raster tga = Raster.Load("duck.tga");
TgaDetails tgaDetails = (TgaDetails)tga.Metadata.FormatDetails!;
Console.WriteLine($"TGA type {tgaDetails.ImageType}, origin {tgaDetails.Origin}, alpha {tgaDetails.AlphaType}");
using Raster qoi = Raster.Load("duck.qoi");
QoiDetails qoiDetails = (QoiDetails)qoi.Metadata.FormatDetails!;
Console.WriteLine($"QOI {qoiDetails.Channels} channels, linear: {qoiDetails.Linear}");
using Raster pgm = Raster.Load("duck.pgm");
PnmDetails pnmDetails = (PnmDetails)pgm.Metadata.FormatDetails!;
Console.WriteLine($"PNM {pnmDetails.Kind}, maxval {pnmDetails.MaxValue}, plain text: {pnmDetails.PlainText}");
Each format is described by a FormatInfo: its name, extensions, MIME types, and how to recognize it from its first bytes.
// Every built-in format, with its name, extensions and MIME types; Raster.Save picks one by extension.
foreach (FormatInfo format in new[] { PngFormat.Info, JpegFormat.Info, GifFormat.Info, BmpFormat.Info, TgaFormat.Info, QoiFormat.Info, PnmFormat.Info })
{
Console.WriteLine($"{format.Name}: {string.Join(", ", format.Extensions)} ({string.Join(", ", format.MimeTypes)})");
}
// A SignatureMatcher decides from the first SignatureLength bytes whether a file is in the format.
SignatureMatcher isPng = PngFormat.Info.Matches;
bool png = isPng(File.ReadAllBytes("duck.png").AsSpan(0, PngFormat.Info.SignatureLength));
Stored samples
A container that keeps its own pixel description, such as DICOM, needs the values a codestream stores rather than pixels.
SampleImage.Load decodes JPEG (baseline, extended 12-bit and lossless at 2 to 16 bits), JPEG 2000 and JPEG XL (integer
samples up to 16 bits) that way: one int a sample, interleaved, at each component's own precision and sign
(SampleComponent), with no color conversion (SampleColor says what the components are), no widening and no offset. Other
formats throw NotSupportedException, as do Region, MinimumSize and ConvertColorTo. Chroma at less than full resolution
is brought to the image's size.
SampleImage, SampleComponent and SampleColor are a preview, like DICOM, which uses them: they carry .NET's
[Experimental] attribute with the diagnostic EGAMIX001, so suppress that id to call them, and they may change in any minor
release.
// The values the codestream stores: no color conversion, no widening to 8 or 16 bits, signed samples kept signed.
SampleImage image = SampleImage.Load("duck.jpg");
Console.WriteLine($"{image.Color}, {image.Components.Count} components of {image.Components[0].Precision} bits");
// A YCbCr JPEG stays YCbCr: luma, then the two chroma samples of each pixel, at full resolution.
Span<int> row = image.Row(0);
int luma = row[0], cb = row[1], cr = row[2];
A codestream taken out of a container is read as the format the container names, without detection:
// A codestream taken out of a container, such as a DICOM pixel data fragment, is read as the format the container names.
SampleImage image = SampleImage.Load(fragment, JpegFormat.Info);
SampleComponent gray = image.Components[0];
int first = image.Samples.Span[0]; // 0 to 4095 for 12 unsigned bits
Recipes
Short answers to common tasks in this area. Each one compiles and runs in Egami's test run.
Batch convert a folder
// Convert a folder of images to QOI, a few at a time. Each image runs on one thread, so the files run side by side.
EgamiContext oneThread = EgamiContext.Default with { MaxParallelism = 1 };
Directory.CreateDirectory("out");
Parallel.ForEach(Directory.GetFiles("in"), new ParallelOptions { MaxDegreeOfParallelism = 4 }, path =>
{
try
{
using Raster image = Raster.Load(path, oneThread);
image.Save(Path.Combine("out", Path.GetFileNameWithoutExtension(path) + ".qoi"));
}
catch (EgamiException e)
{
Console.WriteLine($"{path}: {e.Message}");
}
});
Read the size without decoding
RasterInfo info = Raster.Probe("sideways.jpg");
bool sideways = info.Orientation is >= Egami.Metadata.ImageOrientation.Transpose;
(int width, int height) = sideways ? (info.Height, info.Width) : (info.Width, info.Height);
Console.WriteLine($"{info.Format.Name}, shown as {width} × {height}, about {info.EstimatedBytes:N0} bytes to load");
Probe reads only the header, and reports the EXIF orientation, so you know how the image will be shown.
Run it
samples/Formats is a console app that does what this guide shows on the bundled
images: dotnet run --project samples/Formats writes its results to ./output.