HEIC

edit

HEIC stores HEVC (H.265) images in the HEIF container: it is the format of the photos of Apple devices and of many cameras. The library has its own HEVC decoder and encoder. HeicEncoder is lossless by default, and lossless files are large and opened by fewer readers: choose lossy 4:2:0 output for files that other devices must open.

What is read and written

ReadingWriting
ImagesStill images and image sequences, grids (the tiles of phone photos)Still images and image sequences, grids
Samples8 to 12 bits; 4:4:4, 4:2:2, 4:2:0 and monochrome8, 10 or 12 bits; 4:4:4, 4:2:2, 4:2:0 and monochrome
AlphaStraight alphaLossless alpha
GeometryClean aperture applied; orientation reportedOrientation of still images
MetadataICC profile, EXIF, XMP, CICP color description, content light level, mastering display, pixel aspect ratioThe same

8-bit files load as Rgb24, Rgba32 with alpha, or Gray8; deeper files load as Rgb48, Rgba64 or Gray16, their samples scaled to 16 bits exactly.

Load a phone photo

A camera stores the image as the sensor reads it, and the rotation as a property of the file: it is reported in Metadata.Orientation, and AutoOrient applies it. The ICC profile (Display P3 on recent phones) and the EXIF block are kept:

public static string ConvertHeic(string heicPath, string jpegPath)
{
    using var image = Image.Load(heicPath);
    var description = $"{image.Width}x{image.Height} {image.PixelFormat}, orientation: {image.Metadata.Orientation}";

    // A camera stores the image as its sensor reads it, and the rotation to apply as a property of the file: the
    // decoder reports it as the orientation of the image and moves no pixel. AutoOrient applies it, so that the
    // JPEG file needs no orientation.
    image.AutoOrient();

    // The ICC profile of the photo (Display P3 for a recent phone) and its EXIF block are stored in the JPEG file
    image.Save(jpegPath, new JpegEncoder { Quality = 90 });
    return description;
}

See Handling phone photos. The thumbnails, depth maps, mattes and gain maps of the photo are listed by AuxiliaryImageCollection, which decodes them on request; the other photos of a burst are not read.

Save a HEIC file

public static void SaveHeic(string photoPath, string heicPath, string losslessPath, string animatedPath, string animatedHeicPath)
{
    using (var photo = Image.Load(photoPath))
    {
        // Lossy 4:2:0 with 8 bits is the Main Still Picture profile of HEVC, which every HEIC reader decodes.
        // The alpha plane, when the image has one, is always stored losslessly.
        photo.Save(heicPath, new HeicEncoder
        {
            Compression = HeicCompression.Lossy,
            Quality = 80, // 0-100: size versus fidelity
            Effort = 4,   // 0-9: encoding time versus size
            AllowBitDepthReduction = true, // 16-bit sources are written with 12 bits
        });

        // The default is lossless: every sample of an 8-bit image is decoded back exactly. The R'G'B' samples are
        // coded as they are (4:4:4), which is a profile of the range extensions of HEVC: fewer readers decode it.
        photo.Save(losslessPath, new HeicEncoder { AllowBitDepthReduction = true });
    }

    // An animation is written as an image sequence, with its exact durations and its play count. It needs a seekable
    // destination (the index of the images is written last): a path, a FileStream or a MemoryStream.
    using var animation = Image.Load(animatedPath);
    animation.Save(animatedHeicPath, new HeicEncoder { Compression = HeicCompression.Lossy });
}

Lossy 4:2:0 output of 8 or 10 bits uses the profiles of HEVC that every HEIC reader decodes. Everything else (lossless output, 4:4:4 and 4:2:2, 12 bits, gray images and alpha planes) uses the range extensions profiles, which fewer readers decode.

Options

OptionWhat it doesDefault
CompressionLossless or LossyLossless
Quality0 (smallest) to 100 (best) for lossy output75
OptimizeQuantizationSearches several quantization rounding offsets per lossy transform block, balancing reconstructed error against estimated coded size; costs more encoding timefalse
Effort0 (fastest, largest) to 9 (slowest, smallest): trades encoding time for size, never changes lossless pixels5
ChromaSubsamplingAuto (4:2:0), Ratio420, Ratio422 or Ratio444, for lossy outputAuto
BitDepth8, 10 or 12; null keeps the depth of the pixel format. A value for a 16-bit pixel format allows rounding to itnull
AllowBitDepthReductionLets 16-bit pixel formats be rounded to 12 bits when BitDepth is not setfalse
ClearTransparentColorsReplaces the color of fully transparent pixels with black, which makes the file smallerfalse
GridTileSizeCuts a still image larger than a tile into tiles coded separately (64 to 65,528 pixels per side); cameras use 512 x 512null
KeyFrameIntervalThe number of images from a key frame to the next in a sequence; 0 makes the first image the only key frame0
DurationRoundingRequireExact stores durations exactly or throws; RoundToNearest rounds them to microseconds when they need itRequireExact
MetadataHandlingWhat to do with metadata HEIC cannot store (text, resolution)Strict

The quality scale is not the one of AvifEncoder: at the same number, a HEIC file is closer to its source and larger.

OptimizeQuantization is opt-in and does not change the quality-to-quantizer scale. It can change decoded lossy pixels, but never lossless pixels or alpha values. It searches block rounding offsets, not a coefficient trellis, and does not provide a target bit rate or file size. Image sequences still use I and P pictures, without B pictures.

The HEVC profile follows the sample format and coding tools. The level is the lowest one of the Main tier that accommodates the coded dimensions and the largest coded picture, subject to the profile's coded-picture-buffer and minimum-compression constraints. Only if no Main-tier level suffices does the encoder try the High tier, then unrestricted level 8.5. A high level can therefore be necessary even for small dimensions, especially for lossless noise. A still-image grid bounds the dimensions of each coded picture; changing the level label alone would not make a constrained decoder compatible. Sequence playback rates come from frame durations in the container; this picture-based level selection does not impose a bit-rate or luma-sample-rate limit on playback.

Pitfalls

  • Save("photo.heic") is lossless: pass a HeicEncoder with Compression = HeicCompression.Lossy for photos.
  • HEIC stores at most 12 bits: Rgba64 and Gray16 images throw unless BitDepth or AllowBitDepthReduction is set.
  • An image sequence needs a seekable destination and a known frame count, and stores no orientation. A still image goes to any stream.
  • Encoding a large image takes time: a lower Effort encodes faster. With GridTileSize and ImageConfiguration.MaxDegreeOfParallelism above 1, the tiles are coded concurrently, and the file is identical.
  • A side is at most 65,528 pixels, and a still image at most 4 GiB.
  • Premultiplied alpha, bit depths above 12 bits and the screen content, multilayer and 3D extensions of HEVC are rejected with an UnsupportedImageFeatureException. Only the base layer of a layered stream is decoded.
  • HEVC is covered by patents in many countries: check the licensing terms that apply to what you distribute.