Saving images

edit

Save encodes an image to a file or a stream. This page shows how the format is chosen, what is checked before anything is written, and how to tell the library what to do with what the format cannot store.

Choose the format from the extension

image.Save(path) chooses the encoder from the extension of the file, case-insensitively, with default settings:

ExtensionFormat
.pngPNG, or APNG when the image is animated
.apngAPNG, always
.gifGIF
.jpg, .jpegJPEG
.webpWebP, lossless
.avif, .avifsAVIF, lossless
.heic, .heif, .heics, .heifsHEIC, lossless
.jxlJPEG XL, lossless
.jp2JPEG 2000 file, lossless
.j2k, .j2c, .jpcJPEG 2000 codestream, lossless
.jph, .jhcHTJ2K (high-throughput JPEG 2000) file or codestream, lossless
.tif, .tiffTIFF
.bmp, .dibBMP
.tga, .icb, .vda, .vstTGA
.qoiQOI
.pnm, .pam, .ppm, .pgmNetpbm
.pbmNetpbm portable bitmap, from black and white pixels only
.icoIcon
.curCursor
.aniAnimated cursor
.exrOpenEXR, ZIP compression

Any other extension throws an ArgumentException. The JPEG 2000 extensions .jpx and .jpf are read, not written.

To choose the format from a name or a request rather than from the path of the file:

Choose the format and its settings

Pass an encoder to choose the format and its options. The encoder decides the format, whatever the extension:

using var image = Image.Load("animation.gif");          // every displayed frame, exact timing
image.Resize(new ResizeOptions(320, 240));               // all frames, atomically
image.Save("thumbnail.png", new PngEncoder());           // APNG, because the image is animated

image.Frames[0].Save("first.jpg",                         // JPEG stores one frame: export it explicitly
    new JpegEncoder { Quality = 85, BackgroundColor = new Rgba32(255, 255, 255) });

The encoders are in the Meziantou.Framework.Imaging.Formats namespace. They are immutable: create one and reuse it for every image, from any thread. Each format page lists the settings of its encoder: see Supported formats.

What is checked before writing

A save checks the whole image against the output before writing anything: the animation, the poster frame, the durations, the alpha, the precision and the metadata. When the format cannot store the image as it is, the save throws an UnsupportedImageFeatureException and writes nothing:

public static bool TrySaveAsJpeg(Image image, string path)
{
    try
    {
        image.Save(path, new JpegEncoder { Quality = 90 });
        return true;
    }
    catch (UnsupportedImageFeatureException)
    {
        // JPEG cannot store this image as it is (an animation, transparency, 16-bit samples, metadata...).
        // Nothing was written: a file that already exists at path is left untouched
        return false;
    }
}

The exception names the format and the feature (Format, Feature). The settings that allow a conversion are on the encoder:

The image hasThe output cannot store itSetting
Several framesJPEG, a static PNG, and the other still formatsNone: save one frame with image.Frames[i].Save(...)
A poster frameEvery format but animated PNGNone: remove it with RemovePosterFrame
A duration the format cannot representGIF, APNG, ANI, …DurationRounding
TransparencyJPEG, BMP layouts other than 32-bit, …BackgroundColor
16-bit samplesJPEG, WebP, QOI, BMP, TGA, …AllowBitDepthReduction
Floating-point samplesEvery format but OpenEXR and TIFFFloatingPointConversion
MetadataDepends on the formatMetadataHandling

The complete list is in Pixel formats.

Metadata

ImageEncoder.MetadataHandling decides what happens to metadata the output format cannot store, such as an ICC profile in a GIF file, an EXIF orientation in a BMP file or a cursor hotspot in a PNG file:

ValueEffect
MetadataHandling.Strict (default)Throws an UnsupportedImageFeatureException
MetadataHandling.DiscardUnsupportedDrops what the format cannot store, writes the rest
MetadataHandling.StripWrites no optional metadata

The animation settings and the frame durations are never affected. A malformed EXIF, ICC or XMP payload that you set yourself is rejected when you save. Which format stores what is described in Metadata and orientation.

Floating-point images

RgbaFloat and RgbaHalf images are stored by the OpenEXR encoder with the sample type and alpha conversion of OpenEXR, and by the TIFF encoder as 32-bit floats (with TiffPixelLayout.Auto and a compression other than JPEG). Every other encoder rejects them before writing, unless ImageEncoder.FloatingPointConversion gives the options of their reduction to 16-bit integers: a tone mapping for the highlights, or clipping. See Pixel formats.

Files are published atomically

A save to a path writes a temporary file in the destination directory, and renames it to the destination only once the encoding succeeded. On a failure or a cancellation, an existing file at the destination is left untouched and the temporary file is deleted, so a reader of the destination never sees a half-written image. The library does not flush the file to the disk (fsync): the publication is atomic, but it is not guaranteed to survive a crash of the machine.

The same holds for SaveAsync, for ImageCollection.Save and for the writers that write to a path.

Save to a stream

A stream has no extension, so Save(stream, encoder) requires an encoder:

public static async Task<byte[]> EncodeAsPngAsync(Image image, CancellationToken cancellationToken)
{
    using var stream = new MemoryStream();

    // Writing starts at the current position of the stream, which is left open
    await image.SaveAsync(stream, new PngEncoder(), cancellationToken);
    return stream.ToArray();
}
  • Writing starts at the current position of the stream, and the stream is left open.
  • A TIFF file needs a stream that can seek: the encoder writes the pixels first, then goes back to write the offsets of the header. A stream that cannot seek is rejected before anything is written.
  • A stream is written as the encoding goes: after a failure, the bytes already written remain, and nothing is finalized. Write to a MemoryStream or a temporary file first when the destination must not receive a partial image.
  • SaveAsync writes with asynchronous I/O and accepts a CancellationToken.