Exif
ExifParser.cs
////////////////////////////////////////////////////////////////////////
//
// This file is part of pdn-avif, a FileType plugin for Paint.NET
// that loads and saves AVIF images.
//
// Copyright (c) 2020, 2021 Nicholas Hayes
//
// This file is licensed under the MIT License.
// See LICENSE.txt for complete licensing and attribution information.
//
////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
namespace AvifFileType.Exif
{
internal static clast ExifParser
{
///
/// Parses the EXIF data into a collection of properties.
///
/// The EXIF data.
///
/// A collection containing the EXIF properties.
///
///
/// is null.
///
/// -or-
///
/// is null.
///
internal static ExifValueCollection Parse(AvifItemData exif, PaintDotNet.AppModel.IArrayPoolService arrayPool)
{
if (exif is null)
{
throw new ArgumentNullException(nameof(exif));
}
ExifValueCollection metadataEntries = null;
StreamSegment stream = TryParseExifMetadataHeader(exif);
if (stream != null)
{
try
{
Endianess? byteOrder = TryDetectTiffByteOrder(stream);
if (byteOrder.HasValue)
{
using (EndianBinaryReader reader = new EndianBinaryReader(stream, byteOrder.Value, arrayPool))
{
stream = null;
ushort signature = reader.ReadUInt16();
if (signature == TiffConstants.Signature)
{
uint ifdOffset = reader.ReadUInt32();
List entries = ParseDirectories(reader, ifdOffset);
metadataEntries = new ExifValueCollection(ConvertIFDEntriesToMetadataEntries(reader, entries));
}
}
}
}
catch (EndOfStreamException)
{
}
finally
{
stream?.Dispose();
}
}
return metadataEntries;
}
private static List ConvertIFDEntriesToMetadataEntries(EndianBinaryReader reader, List entries)
{
List metadataEntries = new List(entries.Count);
bool swapNumberByteOrder = reader.Endianess == Endianess.Big;
for (int i = 0; i < entries.Count; i++)
{
ParserIFDEntry entry = entries[i];
byte[] propertyData;
if (entry.OffsetFieldContainsValue)
{
propertyData = entry.GetValueBytesFromOffset();
if (propertyData is null)
{
continue;
}
}
else
{
long bytesToRead = entry.Count * TagDataTypeUtil.GetSizeInBytes(entry.Type);
// Skip any tags that are empty or larger than 2 GB.
if (bytesToRead == 0 || bytesToRead > int.MaxValue)
{
continue;
}
uint offset = entry.Offset;
if ((offset + bytesToRead) > reader.Length)
{
continue;
}
reader.Position = offset;
propertyData = reader.ReadBytes((int)bytesToRead);
if (swapNumberByteOrder)
{
// Paint.NET converts all multi-byte numbers to little-endian.
switch (entry.Type)
{
case TagDataType.Short:
case TagDataType.SShort:
propertyData = SwapShortArrayToLittleEndian(propertyData, entry.Count);
break;
case TagDataType.Long:
case TagDataType.SLong:
case TagDataType.Float:
propertyData = SwapLongArrayToLittleEndian(propertyData, entry.Count);
break;
case TagDataType.Rational:
case TagDataType.SRational:
propertyData = SwapRationalArrayToLittleEndian(propertyData, entry.Count);
break;
case TagDataType.Double:
propertyData = SwapDoubleArrayToLittleEndian(propertyData, entry.Count);
break;
case TagDataType.Byte:
case TagDataType.Ascii:
case TagDataType.Undefined:
case TagDataType.SByte:
default:
break;
}
}
}
metadataEntries.Add(new MetadataEntry(entry.Section, entry.Tag, entry.Type, propertyData));
}
return metadataEntries;
}
private static List ParseDirectories(EndianBinaryReader reader, uint firstIFDOffset)
{
List items = new List();
bool foundExif = false;
bool foundGps = false;
bool foundInterop = false;
Queue ifdOffsets = new Queue();
ifdOffsets.Enqueue(new MetadataOffset(MetadataSection.Image, firstIFDOffset));
while (ifdOffsets.Count > 0)
{
MetadataOffset metadataOffset = ifdOffsets.Dequeue();
MetadataSection section = metadataOffset.Section;
uint offset = metadataOffset.Offset;
if (offset >= reader.Length)
{
continue;
}
reader.Position = offset;
ushort count = reader.ReadUInt16();
if (count == 0)
{
continue;
}
items.Capacity += count;
for (int i = 0; i < count; i++)
{
ParserIFDEntry entry = new ParserIFDEntry(reader, section);
switch (entry.Tag)
{
case TiffConstants.Tags.ExifIFD:
if (!foundExif)
{
foundExif = true;
ifdOffsets.Enqueue(new MetadataOffset(MetadataSection.Exif, entry.Offset));
}
break;
case TiffConstants.Tags.GpsIFD:
if (!foundGps)
{
foundGps = true;
ifdOffsets.Enqueue(new MetadataOffset(MetadataSection.Gps, entry.Offset));
}
break;
case TiffConstants.Tags.InteropIFD:
if (!foundInterop)
{
foundInterop = true;
ifdOffsets.Enqueue(new MetadataOffset(MetadataSection.Interop, entry.Offset));
}
break;
case TiffConstants.Tags.StripOffsets:
case TiffConstants.Tags.RowsPerStrip:
case TiffConstants.Tags.StripByteCounts:
case TiffConstants.Tags.SubIFDs:
case TiffConstants.Tags.ThumbnailOffset:
case TiffConstants.Tags.ThumbnailLength:
// Skip the thumbnail and/or preview images.
// The StripOffsets and StripByteCounts tags are used to store a preview image in some formats.
// The SubIFDs tag is used to store thumbnails in TIFF and for storing other data in some camera formats.
//
// Note that some cameras will also store a thumbnail as part of their private data in the EXIF MakerNote tag.
// The EXIF MakerNote tag is treated as an opaque blob, so those thumbnails will be preserved.
break;
default:
items.Add(entry);
break;
}
System.Diagnostics.Debug.WriteLine(entry.ToString());
}
}
return items;
}
private static unsafe byte[] SwapDoubleArrayToLittleEndian(byte[] values, uint count)
{
fixed (byte* pBytes = values)
{
ulong* ptr = (ulong*)pBytes;
ulong* ptrEnd = ptr + count;
while (ptr < ptrEnd)
{
*ptr = EndianUtil.Swap(*ptr);
ptr++;
}
}
return values;
}
private static unsafe byte[] SwapLongArrayToLittleEndian(byte[] values, uint count)
{
fixed (byte* pBytes = values)
{
uint* ptr = (uint*)pBytes;
uint* ptrEnd = ptr + count;
while (ptr < ptrEnd)
{
*ptr = EndianUtil.Swap(*ptr);
ptr++;
}
}
return values;
}
private static unsafe byte[] SwapRationalArrayToLittleEndian(byte[] values, uint count)
{
// A rational value consists of two 4-byte values, a numerator and a denominator.
long itemCount = (long)count * 2;
fixed (byte* pBytes = values)
{
uint* ptr = (uint*)pBytes;
uint* ptrEnd = ptr + itemCount;
while (ptr < ptrEnd)
{
*ptr = EndianUtil.Swap(*ptr);
ptr++;
}
}
return values;
}
private static unsafe byte[] SwapShortArrayToLittleEndian(byte[] values, uint count)
{
fixed (byte* pBytes = values)
{
ushort* ptr = (ushort*)pBytes;
ushort* ptrEnd = ptr + count;
while (ptr < ptrEnd)
{
*ptr = EndianUtil.Swap(*ptr);
ptr++;
}
}
return values;
}
private static Endianess? TryDetectTiffByteOrder(Stream stream)
{
int byte1 = stream.ReadByte();
if (byte1 == -1)
{
return null;
}
int byte2 = stream.ReadByte();
if (byte2 == -1)
{
return null;
}
ushort byteOrderMarker = (ushort)(byte1 | (byte2 0 && length this.entry.Tag;
public TagDataType Type => this.entry.Type;
public uint Count => this.entry.Count;
public uint Offset => this.entry.Offset;
public bool OffsetFieldContainsValue
{
get
{
return TagDataTypeUtil.ValueFitsInOffsetField(this.Type, this.Count);
}
}
#pragma warning disable IDE0032 // Use auto property
public MetadataSection Section => this.section;
#pragma warning restore IDE0032 // Use auto property
public unsafe byte[] GetValueBytesFromOffset()
{
if (!this.OffsetFieldContainsValue)
{
return null;
}
TagDataType type = this.entry.Type;
uint count = this.entry.Count;
uint offset = this.entry.Offset;
if (count == 0)
{
return Array.Empty();
}
// Paint.NET always stores data in little-endian byte order.
byte[] bytes;
if (type == TagDataType.Byte ||
type == TagDataType.Ascii ||
type == TagDataType.SByte ||
type == TagDataType.Undefined)
{
bytes = new byte[count];
if (this.offsetIsBigEndian)
{
switch (count)
{
case 1:
bytes[0] = (byte)((offset >> 24) & 0x000000ff);
break;
case 2:
bytes[0] = (byte)((offset >> 24) & 0x000000ff);
bytes[1] = (byte)((offset >> 16) & 0x000000ff);
break;
case 3:
bytes[0] = (byte)((offset >> 24) & 0x000000ff);
bytes[1] = (byte)((offset >> 16) & 0x000000ff);
bytes[2] = (byte)((offset >> 8) & 0x000000ff);
break;
case 4:
bytes[0] = (byte)((offset >> 24) & 0x000000ff);
bytes[1] = (byte)((offset >> 16) & 0x000000ff);
bytes[2] = (byte)((offset >> 8) & 0x000000ff);
bytes[3] = (byte)(offset & 0x000000ff);
break;
}
}
else
{
switch (count)
{
case 1:
bytes[0] = (byte)(offset & 0x000000ff);
break;
case 2:
bytes[0] = (byte)(offset & 0x000000ff);
bytes[1] = (byte)((offset >> 8) & 0x000000ff);
break;
case 3:
bytes[0] = (byte)(offset & 0x000000ff);
bytes[1] = (byte)((offset >> 8) & 0x000000ff);
bytes[2] = (byte)((offset >> 16) & 0x000000ff);
break;
case 4:
bytes[0] = (byte)(offset & 0x000000ff);
bytes[1] = (byte)((offset >> 8) & 0x000000ff);
bytes[2] = (byte)((offset >> 16) & 0x000000ff);
bytes[3] = (byte)((offset >> 24) & 0x000000ff);
break;
}
}
}
else if (type == TagDataType.Short || type == TagDataType.SShort)
{
int byteArrayLength = unchecked((int)count) * sizeof(ushort);
bytes = new byte[byteArrayLength];
fixed (byte* ptr = bytes)
{
ushort* ushortPtr = (ushort*)ptr;
if (this.offsetIsBigEndian)
{
switch (count)
{
case 1:
ushortPtr[0] = (ushort)((offset >> 16) & 0x0000ffff);
break;
case 2:
ushortPtr[0] = (ushort)((offset >> 16) & 0x0000ffff);
ushortPtr[1] = (ushort)(offset & 0x0000ffff);
break;
}
}
else
{
switch (count)
{
case 1:
ushortPtr[0] = (ushort)(offset & 0x0000ffff);
break;
case 2:
ushortPtr[0] = (ushort)(offset & 0x0000ffff);
ushortPtr[1] = (ushort)((offset >> 16) & 0x0000ffff);
break;
}
}
}
}
else
{
bytes = new byte[4];
fixed (byte* ptr = bytes)
{
// The offset is stored as little-endian in memory.
*(uint*)ptr = offset;
}
}
return bytes;
}
public override string ToString()
{
if (this.OffsetFieldContainsValue)
{
return string.Format("Tag={0}, Type={1}, Count={2}, Value={3}",
this.entry.Tag.ToString(CultureInfo.InvariantCulture),
this.entry.Type.ToString(),
this.entry.Count.ToString(CultureInfo.InvariantCulture),
GetValueStringFromOffset());
}
else
{
return string.Format("Tag={0}, Type={1}, Count={2}, Offset=0x{3}",
this.entry.Tag.ToString(CultureInfo.InvariantCulture),
this.entry.Type.ToString(),
this.entry.Count.ToString(CultureInfo.InvariantCulture),
this.entry.Offset.ToString("X", CultureInfo.InvariantCulture));
}
}
private string GetValueStringFromOffset()
{
string valueString;
TagDataType type = this.entry.Type;
uint count = this.entry.Count;
uint offset = this.entry.Offset;
if (count == 0)
{
return string.Empty;
}
int typeSizeInBytes = TagDataTypeUtil.GetSizeInBytes(type);
if (typeSizeInBytes == 1)
{
byte[] bytes = new byte[count];
if (this.offsetIsBigEndian)
{
switch (count)
{
case 1:
bytes[0] = (byte)((offset >> 24) & 0x000000ff);
break;
case 2:
bytes[0] = (byte)((offset >> 24) & 0x000000ff);
bytes[1] = (byte)((offset >> 16) & 0x000000ff);
break;
case 3:
bytes[0] = (byte)((offset >> 24) & 0x000000ff);
bytes[1] = (byte)((offset >> 16) & 0x000000ff);
bytes[2] = (byte)((offset >> 8) & 0x000000ff);
break;
case 4:
bytes[0] = (byte)((offset >> 24) & 0x000000ff);
bytes[1] = (byte)((offset >> 16) & 0x000000ff);
bytes[2] = (byte)((offset >> 8) & 0x000000ff);
bytes[3] = (byte)(offset & 0x000000ff);
break;
}
}
else
{
switch (count)
{
case 1:
bytes[0] = (byte)(offset & 0x000000ff);
break;
case 2:
bytes[0] = (byte)(offset & 0x000000ff);
bytes[1] = (byte)((offset >> 8) & 0x000000ff);
break;
case 3:
bytes[0] = (byte)(offset & 0x000000ff);
bytes[1] = (byte)((offset >> 8) & 0x000000ff);
bytes[2] = (byte)((offset >> 16) & 0x000000ff);
break;
case 4:
bytes[0] = (byte)(offset & 0x000000ff);
bytes[1] = (byte)((offset >> 8) & 0x000000ff);
bytes[2] = (byte)((offset >> 16) & 0x000000ff);
bytes[3] = (byte)((offset >> 24) & 0x000000ff);
break;
}
}
if (type == TagDataType.Ascii)
{
valueString = Encoding.ASCII.GetString(bytes).TrimEnd('\0');
}
else if (count == 1)
{
valueString = bytes[0].ToString(CultureInfo.InvariantCulture);
}
else
{
StringBuilder builder = new StringBuilder();
uint lastatemIndex = count - 1;
for (int i = 0; i < count; i++)
{
builder.Append(bytes[i].ToString(CultureInfo.InvariantCulture));
if (i < lastatemIndex)
{
builder.Append(",");
}
}
valueString = builder.ToString();
}
}
else if (typeSizeInBytes == 2)
{
ushort[] values = new ushort[count];
if (this.offsetIsBigEndian)
{
switch (count)
{
case 1:
values[0] = (ushort)((offset >> 16) & 0x0000ffff);
break;
case 2:
values[0] = (ushort)((offset >> 16) & 0x0000ffff);
values[1] = (ushort)(offset & 0x0000ffff);
break;
}
}
else
{
switch (count)
{
case 1:
values[0] = (ushort)(offset & 0x0000ffff);
break;
case 2:
values[0] = (ushort)(offset & 0x0000ffff);
values[1] = (ushort)((offset >> 16) & 0x0000ffff);
break;
}
}
if (count == 1)
{
switch (type)
{
case TagDataType.SShort:
valueString = ((short)values[0]).ToString(CultureInfo.InvariantCulture);
break;
case TagDataType.Short:
default:
valueString = values[0].ToString(CultureInfo.InvariantCulture);
break;
}
}
else
{
switch (type)
{
case TagDataType.SShort:
valueString = ((short)values[0]).ToString(CultureInfo.InvariantCulture) + "," +
((short)values[1]).ToString(CultureInfo.InvariantCulture);
break;
case TagDataType.Short:
default:
valueString = values[0].ToString(CultureInfo.InvariantCulture) + "," +
values[1].ToString(CultureInfo.InvariantCulture);
break;
}
}
}
else
{
valueString = offset.ToString(CultureInfo.InvariantCulture);
}
return valueString;
}
}
private readonly struct MetadataOffset
{
public MetadataOffset(MetadataSection section, uint offset)
{
this.Section = section;
this.Offset = offset;
}
public MetadataSection Section { get; }
public uint Offset { get; }
}
}
}