Table of Contents

Metadata

raster.Metadata holds what a file says beside its pixels. It is kept apart from the pixels, survives operations unless an operation makes it wrong, and is written back by every format that can carry it.

Item Read from Written to
Exif (ExifData) JPEG, PNG (eXIf, and the legacy "Raw profile type" chunks) JPEG, PNG
Iptc (IptcData) JPEG (APP13), PNG ("Raw profile type iptc") JPEG, PNG
Xmp, XmpData JPEG (with extended XMP), PNG, GIF JPEG, PNG, GIF
IccProfile JPEG, PNG, GIF, BMP (V5) JPEG, PNG, GIF, BMP
Text PNG text chunks, GIF comments, PNM comments, TGA author and comments The same
Resolution PNG, JPEG, BMP PNG, JPEG, BMP
ColorSpace, Gamma PNG, QOI, TGA PNG, QOI
LoopCount, frame Delay GIF, APNG GIF, APNG
FormatDetails Every format: what only it has The same format, for round trips
Warnings Every reader: what a lenient read recovered from Never
RasterMetadata metadata = image.Metadata;
metadata.Text.Add(new TextEntry("Comment", "Made with Egami"));
metadata.Resolution = new Resolution(300, 300, ResolutionUnit.PerInch);
Console.WriteLine($"{metadata.ColorSpace}, ICC {metadata.IccProfile.Length} bytes, XMP {metadata.Xmp.Length} bytes");
foreach (EgamiDiagnostic warning in metadata.Warnings)
{
    Console.WriteLine($"Recovered from: {warning.Message} ({warning.Id})");
}

image.Save("duck-300dpi.png");

EXIF

if (image.Metadata.Exif is ExifData exif)
{
    Console.WriteLine($"{exif.CameraMake} {exif.CameraModel}, {exif.DateTaken}, {exif.Location}");
    if (exif.TryGet(ExifKey.ExposureTime, out ExifEntry? exposure))
    {
        Console.WriteLine($"1/{1 / exposure.AsDouble():0} s");
    }

    exif.Set(ExifKey.Copyright, "© 2026 Example");
    exif.Remove(ExifKey.GpsLatitude);
}

Common values have typed properties (Orientation, DateTaken, CameraMake, CameraModel, Location); any tag can be read with TryGet and written with Set. A block you do not edit is saved back byte for byte; an edited one is laid out again, in tag order. A damaged block keeps its raw bytes and sets IsDamaged, and the pixels still load. ExifData.Parse reads a block on its own, capped by SafetyLimits.MaxMetadataBytes.

What operations do to metadata

  • Rotating, flipping and AutoOrient set the EXIF orientation to Normal and drop the embedded thumbnail, which would otherwise show sideways.
  • Resizing and cropping, including your own operations that change the size, update the EXIF pixel dimensions and drop the embedded thumbnail, which no longer matches.
  • Every geometry operation moves the EXIF SubjectArea and SubjectLocation with the picture: shifted by crops and pads, scaled by resizes, mirrored and turned with flips and rotations, clipped to a crop and dropped when the subject leaves it. Their XMP copies, and the XMP tiff:Orientation, exif:PixelXDimension and exif:PixelYDimension, follow the same rules. A tag the file does not have is never added.
  • Color operations change nothing in the metadata.

Removing metadata

// Every writer's options have StripMetadata: the pixels are written, EXIF, XMP, ICC and text are not.
image.Save("clean.jpg", new JpegWriterOptions { StripMetadata = true });

Typed EXIF values

using Raster photo = Raster.Load("sideways.jpg");
ExifData exif = photo.Metadata.Exif ?? new ExifData();

// Typed values go in with their EXIF type; fractions are exact numerator / denominator pairs.
exif.Set(ExifKey.ExposureTime, new ExifFraction(1, 250));
exif.Set(ExifKey.Artist, "A. Photographer");
if (exif.TryGet(ExifKey.ExposureTime, out ExifEntry? entry) && entry.Type == ExifType.Rational)
{
    Console.WriteLine($"Exposure {entry.AsFraction()} s = {entry.AsFraction().ToDouble()} s, in the {entry.Key.Ifd} IFD");
}

if (exif.Location is GeoPosition where)
{
    Console.WriteLine($"Taken at {where.Latitude:F5}, {where.Longitude:F5}");
}

The IFD1 thumbnail decodes as a raster, and a new one is made from any raster. Crops, resizes, flips and turns drop the thumbnail, since it no longer shows the image:

using Raster photo = Raster.Load("sideways.jpg");

// The thumbnail as stored, if the file has one; ReadThumbnail returns null otherwise.
using (Raster<Rgb8>? stored = photo.Metadata.Exif?.ReadThumbnail())
{
    Console.WriteLine(stored is null ? "No thumbnail" : $"Thumbnail {stored.Width} x {stored.Height}");
}

// Turning the image drops the thumbnail, which would show sideways; make a new one from the result.
photo.Apply(ops => ops.AutoOrient());
photo.Metadata.Exif?.SetThumbnail(photo);
photo.Save("upright.jpg");

The Windows XP tags (ExifKey.XPTitle, XPComment, XPAuthor, XPKeywords, XPSubject) and the encoded strings (ExifKey.UserComment, GpsProcessingMethod, GpsAreaInformation) read and write as text through ExifEntry.AsString and ExifData.Set, in the encodings Windows Explorer and cameras use.

EXIF stored somewhere else, such as a database, parses on its own:

// EXIF from somewhere other than an image file (a database column, a sidecar), with a cap on the bytes of values read.
ExifData exif = ExifData.Parse(tiff.Span, SafetyLimits.Default with { MaxMetadataBytes = 64 * 1024 });
foreach (string warning in exif.Warnings)
{
    Console.WriteLine($"EXIF: {warning}");
}

IPTC

IptcData holds IPTC-IIM datasets, as news agencies, stock libraries and Lightroom write them. Each IptcEntry has an IptcKey (record and dataset, such as 2:25 for keywords), and typed getters cover the common ones. Values longer than the IIM specification allows are refused on Set and Add; long text belongs in XMP. Text that is not ASCII switches the block to UTF-8.

using Raster photo = Raster.Load("sideways.jpg");
IptcData iptc = photo.Metadata.Iptc ?? new IptcData();
iptc.Headline = "Harbour at dawn";
iptc.Keywords = [.. iptc.Keywords, "harbour", "Bergen"];

// Any dataset by its key; Add appends to the repeatable ones.
iptc.Set(IptcKey.City, "Bergen");

// Text that is not ASCII sets the coded character set (1:90) to UTF-8, so other programs read it right.
iptc.Caption = "Fishing boats at Bryggen, Bergen — first light";
foreach (IptcEntry entry in iptc.Entries)
{
    Console.WriteLine($"{entry.Key} {entry.AsString()}");
}

// JPEG saves it in its Photoshop resource block (APP13), with the digest Photoshop and Lightroom check.
photo.Metadata.Iptc = iptc;
photo.Save("captioned.jpg");

IPTC stored somewhere else parses on its own:

// IPTC datasets from somewhere other than an image file, with a cap on the bytes of values read.
IptcData iptc = IptcData.Parse(iim.Span, SafetyLimits.Default with { MaxMetadataBytes = 64 * 1024 });
Console.WriteLine($"{iptc.Get(IptcKey.ObjectName)}: {iptc.Entries.Count} datasets, damaged: {iptc.IsDamaged}");

XMP

XmpData is a parsed XMP packet. Properties are addressed by XmpName (a namespace URI from XmpNamespaces and a local name), never by prefix. A property is a simple value, an XmpArray (bag, sequence or language alternatives, an XmpArrayKind) or an XmpStruct; typed properties cover the Dublin Core, XMP Basic, Photoshop and TIFF values apps use most. An edited packet is written as canonical RDF/XML with padding; properties Egami does not know are kept.

XmpData xmp = new()
{
    Title = "Harbour at dawn",
    Creators = ["A. Photographer"],
    Keywords = ["harbour", "Bergen"],
    Rating = 4,
};

// Any property, by namespace URI and local name; structures and arrays nest.
XmpStruct contact = new();
contact.Set(new XmpName(XmpNamespaces.IptcCore, "CiEmailWork"), "photo@example.com");
xmp.Set(new XmpName(XmpNamespaces.IptcCore, "CreatorContactInfo"), contact);
if (xmp.TryGet(new XmpName(XmpNamespaces.DublinCore, "title"), out object? title) && title is XmpArray languages)
{
    languages.SetLanguage("nb-NO", "Havn ved daggry");
}

ReadOnlyMemory<byte> packet = xmp.ToBytes();   // canonical RDF/XML with padding

RasterMetadata.XmpData is a live view of RasterMetadata.Xmp: it parses the packet on first use, and once edited, the edit is the packet every format saves. JPEG, PNG (an iTXt chunk) and GIF carry XMP.

using Raster photo = Raster.Load("sideways.jpg");

// The parsed view of the packet; it is null when the file has none, so start one.
XmpData xmp = photo.Metadata.XmpData ??= new XmpData();
xmp.Keywords = [.. xmp.Keywords, "harbour"];
xmp.Rating = 5;

// The edit is the packet now: Metadata.Xmp returns it and every format saves it.
photo.Save("rated.png");

Lightroom, Bridge and Windows read the keywords and rating from XMP. For news and stock workflows, set the same keywords in IPTC too.

// An XMP packet from a sidecar file or a database; DTDs are refused and nothing outside it is read.
XmpData xmp = XmpData.Parse(packet.Span, SafetyLimits.Default with { MaxMetadataBytes = 1024 * 1024 });
Console.WriteLine(xmp.IsDamaged ? string.Join(" ", xmp.Warnings) : $"Label: {xmp.Label}");

Color space

RasterMetadata.ColorSpace says what the file declared about its colors. Egami keeps the declaration and the profile bytes; it does not convert between color spaces.

using Raster image = Raster.Load("duck.png");
string note = image.Metadata.ColorSpace switch
{
    ColorSpaceHint.Srgb or ColorSpaceHint.Unknown => "sRGB: shown as is",
    ColorSpaceHint.Linear => "linear light: encode before showing",
    ColorSpaceHint.Gamma => "a file gamma other than sRGB",
    ColorSpaceHint.EmbeddedIcc => "an ICC profile describes the colors",
    _ => "unknown",
};

Run it

samples/Metadata is a console app that does what this guide shows on the bundled images: dotnet run --project samples/Metadata writes its results to ./output.