// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // namespace ImageProcessorCore { using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; internal sealed class ExifReader { private delegate TDataType ConverterMethod(byte[] data); private byte[] data; private Collection invalidTags = new Collection(); private uint index; private bool isLittleEndian; private uint exifOffset; private uint gpsOffset; private uint startIndex; public uint ThumbnailLength { get; private set; } public uint ThumbnailOffset { get; private set; } private int RemainingLength { get { if (this.index >= this.data.Length) return 0; return this.data.Length - (int)this.index; } } public Collection Read(byte[] data) { Collection result = new Collection(); this.data = data; if (GetString(4) == "Exif") { if (GetShort() != 0) return result; this.startIndex = 6; } else { this.index = 0; } this.isLittleEndian = GetString(2) == "II"; if (GetShort() != 0x002A) return result; uint ifdOffset = GetLong(); AddValues(result, ifdOffset); uint thumbnailOffset = GetLong(); GetThumbnail(thumbnailOffset); if (this.exifOffset != 0) AddValues(result, this.exifOffset); if (this.gpsOffset != 0) AddValues(result, this.gpsOffset); return result; } public IEnumerable InvalidTags { get { return this.invalidTags; } } private void AddValues(Collection values, uint index) { this.index = this.startIndex + index; ushort count = GetShort(); for (ushort i = 0; i < count; i++) { ExifValue value = CreateValue(); if (value == null) continue; bool duplicate = false; foreach (ExifValue val in values) { if (val.Tag == value.Tag) { duplicate = true; break; } } if (duplicate) continue; if (value.Tag == ExifTag.SubIFDOffset) { if (value.DataType == ExifDataType.Long) this.exifOffset = (uint)value.Value; } else if (value.Tag == ExifTag.GPSIFDOffset) { if (value.DataType == ExifDataType.Long) this.gpsOffset = (uint)value.Value; } else values.Add(value); } } private object ConvertValue(ExifDataType dataType, byte[] data, uint numberOfComponents) { if (data == null || data.Length == 0) return null; switch (dataType) { case ExifDataType.Unknown: return null; case ExifDataType.Ascii: return ToString(data); case ExifDataType.Byte: if (numberOfComponents == 1) return ToByte(data); else return data; case ExifDataType.DoubleFloat: if (numberOfComponents == 1) return ToDouble(data); else return ToArray(dataType, data, ToDouble); case ExifDataType.Long: if (numberOfComponents == 1) return ToLong(data); else return ToArray(dataType, data, ToLong); case ExifDataType.Rational: if (numberOfComponents == 1) return ToRational(data); else return ToArray(dataType, data, ToRational); case ExifDataType.Short: if (numberOfComponents == 1) return ToShort(data); else return ToArray(dataType, data, ToShort); case ExifDataType.SignedByte: if (numberOfComponents == 1) return ToSignedByte(data); else return ToArray(dataType, data, ToSignedByte); case ExifDataType.SignedLong: if (numberOfComponents == 1) return ToSignedLong(data); else return ToArray(dataType, data, ToSignedLong); case ExifDataType.SignedRational: if (numberOfComponents == 1) return ToSignedRational(data); else return ToArray(dataType, data, ToSignedRational); case ExifDataType.SignedShort: if (numberOfComponents == 1) return ToSignedShort(data); else return ToArray(dataType, data, ToSignedShort); case ExifDataType.SingleFloat: if (numberOfComponents == 1) return ToSingle(data); else return ToArray(dataType, data, ToSingle); case ExifDataType.Undefined: if (numberOfComponents == 1) return ToByte(data); else return data; default: throw new NotImplementedException(); } } private ExifValue CreateValue() { if (RemainingLength < 12) return null; ExifTag tag = ToEnum(GetShort(), ExifTag.Unknown); ExifDataType dataType = ToEnum(GetShort(), ExifDataType.Unknown); object value = null; if (dataType == ExifDataType.Unknown) return new ExifValue(tag, dataType, value, false); uint numberOfComponents = (uint)GetLong(); uint size = numberOfComponents * ExifValue.GetSize(dataType); byte[] data = GetBytes(4); if (size > 4) { uint oldIndex = this.index; this.index = ToLong(data) + this.startIndex; if (RemainingLength < size) { this.invalidTags.Add(tag); this.index = oldIndex; return null; } value = ConvertValue(dataType, GetBytes(size), numberOfComponents); this.index = oldIndex; } else { value = ConvertValue(dataType, data, numberOfComponents); } bool isArray = value != null && numberOfComponents > 1; return new ExifValue(tag, dataType, value, isArray); } private TEnum ToEnum(int value, TEnum defaultValue) where TEnum : struct { TEnum enumValue = (TEnum)(object)value; if (Enum.GetValues(typeof(TEnum)).Cast().Any(v => v.Equals(enumValue))) return enumValue; return defaultValue; } private byte[] GetBytes(uint length) { if (this.index + length > (uint)this.data.Length) return null; byte[] data = new byte[length]; Array.Copy(this.data, (int)this.index, data, 0, (int)length); this.index += length; return data; } private uint GetLong() { return ToLong(GetBytes(4)); } private ushort GetShort() { return ToShort(GetBytes(2)); } private string GetString(uint length) { return ToString(GetBytes(length)); } private void GetThumbnail(uint offset) { Collection values = new Collection(); AddValues(values, offset); foreach (ExifValue value in values) { if (value.Tag == ExifTag.JPEGInterchangeFormat && (value.DataType == ExifDataType.Long)) ThumbnailOffset = (uint)value.Value + this.startIndex; else if (value.Tag == ExifTag.JPEGInterchangeFormatLength && value.DataType == ExifDataType.Long) ThumbnailLength = (uint)value.Value; } } private static TDataType[] ToArray(ExifDataType dataType, Byte[] data, ConverterMethod converter) { int dataTypeSize = (int)ExifValue.GetSize(dataType); int length = data.Length / dataTypeSize; TDataType[] result = new TDataType[length]; byte[] buffer = new byte[dataTypeSize]; for (int i = 0; i < length; i++) { Array.Copy(data, i * dataTypeSize, buffer, 0, dataTypeSize); result.SetValue(converter(buffer), i); } return result; } private static byte ToByte(byte[] data) { return data[0]; } private double ToDouble(byte[] data) { if (!ValidateArray(data, 8)) return default(double); return BitConverter.ToDouble(data, 0); } private uint ToLong(byte[] data) { if (!ValidateArray(data, 4)) return default(uint); return BitConverter.ToUInt32(data, 0); } private ushort ToShort(byte[] data) { if (!ValidateArray(data, 2)) return default(ushort); return BitConverter.ToUInt16(data, 0); } private float ToSingle(byte[] data) { if (!ValidateArray(data, 4)) return default(float); return BitConverter.ToSingle(data, 0); } private static string ToString(byte[] data) { string result = Encoding.UTF8.GetString(data, 0, data.Length); int nullCharIndex = result.IndexOf('\0'); if (nullCharIndex != -1) result = result.Substring(0, nullCharIndex); return result; } private double ToRational(byte[] data) { if (!ValidateArray(data, 8, 4)) return default(double); uint numerator = BitConverter.ToUInt32(data, 0); uint denominator = BitConverter.ToUInt32(data, 4); return numerator / (double)denominator; } private sbyte ToSignedByte(byte[] data) { return unchecked((sbyte)data[0]); } private int ToSignedLong(byte[] data) { if (!ValidateArray(data, 4)) return default(int); return BitConverter.ToInt32(data, 0); } private double ToSignedRational(byte[] data) { if (!ValidateArray(data, 8, 4)) return default(double); int numerator = BitConverter.ToInt32(data, 0); int denominator = BitConverter.ToInt32(data, 4); return numerator / (double)denominator; } private short ToSignedShort(byte[] data) { if (!ValidateArray(data, 2)) return default(short); return BitConverter.ToInt16(data, 0); } private bool ValidateArray(byte[] data, int size) { return ValidateArray(data, size, size); } private bool ValidateArray(byte[] data, int size, int stepSize) { if (data == null || data.Length < size) return false; if (this.isLittleEndian == BitConverter.IsLittleEndian) return true; for (int i = 0; i < data.Length; i += stepSize) { Array.Reverse(data, i, stepSize); } return true; } } }