123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using System.IO;
- using System.Text;
- namespace MediaBrowser.Providers.Photos
- {
- /// <summary>
- /// A class for reading Exif data from a JPEG file. The file will be open for reading for as long as the class exists.
- /// <seealso cref="http://gvsoft.homedns.org/exif/Exif-explanation.html"/>
- /// </summary>
- public class ExifReader : IDisposable
- {
- private readonly FileStream fileStream = null;
- private readonly BinaryReader reader = null;
- /// <summary>
- /// The catalogue of tag ids and their absolute offsets within the
- /// file
- /// </summary>
- private Dictionary<ushort, long> catalogue;
- /// <summary>
- /// Indicates whether to read data using big or little endian byte aligns
- /// </summary>
- private bool isLittleEndian;
- /// <summary>
- /// The position in the filestream at which the TIFF header starts
- /// </summary>
- private long tiffHeaderStart;
- public ExifReader(string fileName)
- {
- // JPEG encoding uses big endian (i.e. Motorola) byte aligns. The TIFF encoding
- // found later in the document will specify the byte aligns used for the
- // rest of the document.
- isLittleEndian = false;
- try
- {
- // Open the file in a stream
- fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- reader = new BinaryReader(fileStream);
- // Make sure the file's a JPEG.
- if (ReadUShort() != 0xFFD8)
- throw new Exception("File is not a valid JPEG");
- // Scan to the start of the Exif content
- ReadToExifStart();
- // Create an index of all Exif tags found within the document
- CreateTagIndex();
- }
- catch (Exception)
- {
- // If instantiation fails, make sure there's no mess left behind
- Dispose();
- throw;
- }
- }
- #region TIFF methods
- /// <summary>
- /// Returns the length (in bytes) per component of the specified TIFF data type
- /// </summary>
- /// <returns></returns>
- private byte GetTIFFFieldLength(ushort tiffDataType)
- {
- switch (tiffDataType)
- {
- case 1:
- case 2:
- case 6:
- return 1;
- case 3:
- case 8:
- return 2;
- case 4:
- case 7:
- case 9:
- case 11:
- return 4;
- case 5:
- case 10:
- case 12:
- return 8;
- default:
- throw new Exception(string.Format("Unknown TIFF datatype: {0}", tiffDataType));
- }
- }
- #endregion
- #region Methods for reading data directly from the filestream
- /// <summary>
- /// Gets a 2 byte unsigned integer from the file
- /// </summary>
- /// <returns></returns>
- private ushort ReadUShort()
- {
- return ToUShort(ReadBytes(2));
- }
- /// <summary>
- /// Gets a 4 byte unsigned integer from the file
- /// </summary>
- /// <returns></returns>
- private uint ReadUint()
- {
- return ToUint(ReadBytes(4));
- }
- private string ReadString(int chars)
- {
- return Encoding.ASCII.GetString(ReadBytes(chars));
- }
- private byte[] ReadBytes(int byteCount)
- {
- return reader.ReadBytes(byteCount);
- }
- /// <summary>
- /// Reads some bytes from the specified TIFF offset
- /// </summary>
- /// <param name="tiffOffset"></param>
- /// <param name="byteCount"></param>
- /// <returns></returns>
- private byte[] ReadBytes(ushort tiffOffset, int byteCount)
- {
- // Keep the current file offset
- long originalOffset = fileStream.Position;
- // Move to the TIFF offset and retrieve the data
- fileStream.Seek(tiffOffset + tiffHeaderStart, SeekOrigin.Begin);
- byte[] data = reader.ReadBytes(byteCount);
- // Restore the file offset
- fileStream.Position = originalOffset;
- return data;
- }
- #endregion
- #region Data conversion methods for interpreting datatypes from a byte array
- /// <summary>
- /// Converts 2 bytes to a ushort using the current byte aligns
- /// </summary>
- /// <returns></returns>
- private ushort ToUShort(byte[] data)
- {
- if (isLittleEndian != BitConverter.IsLittleEndian)
- Array.Reverse(data);
- return BitConverter.ToUInt16(data, 0);
- }
- /// <summary>
- /// Converts 8 bytes to an unsigned rational using the current byte aligns.
- /// </summary>
- /// <param name="data"></param>
- /// <returns></returns>
- /// <seealso cref="ToRational"/>
- private double ToURational(byte[] data)
- {
- var numeratorData = new byte[4];
- var denominatorData = new byte[4];
- Array.Copy(data, numeratorData, 4);
- Array.Copy(data, 4, denominatorData, 0, 4);
- uint numerator = ToUint(numeratorData);
- uint denominator = ToUint(denominatorData);
- return numerator / (double)denominator;
- }
- /// <summary>
- /// Converts 8 bytes to a signed rational using the current byte aligns.
- /// </summary>
- /// <remarks>
- /// A TIFF rational contains 2 4-byte integers, the first of which is
- /// the numerator, and the second of which is the denominator.
- /// </remarks>
- /// <param name="data"></param>
- /// <returns></returns>
- private double ToRational(byte[] data)
- {
- var numeratorData = new byte[4];
- var denominatorData = new byte[4];
- Array.Copy(data, numeratorData, 4);
- Array.Copy(data, 4, denominatorData, 0, 4);
- int numerator = ToInt(numeratorData);
- int denominator = ToInt(denominatorData);
- return numerator / (double)denominator;
- }
- /// <summary>
- /// Converts 4 bytes to a uint using the current byte aligns
- /// </summary>
- /// <returns></returns>
- private uint ToUint(byte[] data)
- {
- if (isLittleEndian != BitConverter.IsLittleEndian)
- Array.Reverse(data);
- return BitConverter.ToUInt32(data, 0);
- }
- /// <summary>
- /// Converts 4 bytes to an int using the current byte aligns
- /// </summary>
- /// <returns></returns>
- private int ToInt(byte[] data)
- {
- if (isLittleEndian != BitConverter.IsLittleEndian)
- Array.Reverse(data);
- return BitConverter.ToInt32(data, 0);
- }
- private double ToDouble(byte[] data)
- {
- if (isLittleEndian != BitConverter.IsLittleEndian)
- Array.Reverse(data);
- return BitConverter.ToDouble(data, 0);
- }
- private float ToSingle(byte[] data)
- {
- if (isLittleEndian != BitConverter.IsLittleEndian)
- Array.Reverse(data);
- return BitConverter.ToSingle(data, 0);
- }
- private short ToShort(byte[] data)
- {
- if (isLittleEndian != BitConverter.IsLittleEndian)
- Array.Reverse(data);
- return BitConverter.ToInt16(data, 0);
- }
- private sbyte ToSByte(byte[] data)
- {
- // An sbyte should just be a byte with an offset range.
- return (sbyte)(data[0] - byte.MaxValue);
- }
- /// <summary>
- /// Retrieves an array from a byte array using the supplied converter
- /// to read each individual element from the supplied byte array
- /// </summary>
- /// <param name="data"></param>
- /// <param name="elementLengthBytes"></param>
- /// <param name="converter"></param>
- /// <returns></returns>
- private Array GetArray<T>(byte[] data, int elementLengthBytes, ConverterMethod<T> converter)
- {
- Array convertedData = Array.CreateInstance(typeof(T), data.Length / elementLengthBytes);
- var buffer = new byte[elementLengthBytes];
- // Read each element from the array
- for (int elementCount = 0; elementCount < data.Length / elementLengthBytes; elementCount++)
- {
- // Place the data for the current element into the buffer
- Array.Copy(data, elementCount * elementLengthBytes, buffer, 0, elementLengthBytes);
- // Process the data and place it into the output array
- convertedData.SetValue(converter(buffer), elementCount);
- }
- return convertedData;
- }
- /// <summary>
- /// A delegate used to invoke any of the data conversion methods
- /// </summary>
- /// <param name="data"></param>
- /// <returns></returns>
- private delegate T ConverterMethod<out T>(byte[] data);
- #endregion
- #region Stream seek methods - used to get to locations within the JPEG
- /// <summary>
- /// Scans to the Exif block
- /// </summary>
- private void ReadToExifStart()
- {
- // The file has a number of blocks (Exif/JFIF), each of which
- // has a tag number followed by a length. We scan the document until the required tag (0xFFE1)
- // is found. All tags start with FF, so a non FF tag indicates an error.
- // Get the next tag.
- byte markerStart;
- byte markerNumber = 0;
- while (((markerStart = reader.ReadByte()) == 0xFF) && (markerNumber = reader.ReadByte()) != 0xE1)
- {
- // Get the length of the data.
- ushort dataLength = ReadUShort();
- // Jump to the end of the data (note that the size field includes its own size)!
- reader.BaseStream.Seek(dataLength - 2, SeekOrigin.Current);
- }
- // It's only success if we found the 0xFFE1 marker
- if (markerStart != 0xFF || markerNumber != 0xE1)
- throw new Exception("Could not find Exif data block");
- }
- /// <summary>
- /// Reads through the Exif data and builds an index of all Exif tags in the document
- /// </summary>
- /// <returns></returns>
- private void CreateTagIndex()
- {
- // The next 4 bytes are the size of the Exif data.
- ReadUShort();
- // Next is the Exif data itself. It starts with the ASCII "Exif" followed by 2 zero bytes.
- if (ReadString(4) != "Exif")
- throw new Exception("Exif data not found");
- // 2 zero bytes
- if (ReadUShort() != 0)
- throw new Exception("Malformed Exif data");
- // We're now into the TIFF format
- tiffHeaderStart = reader.BaseStream.Position;
- // What byte align will be used for the TIFF part of the document? II for Intel, MM for Motorola
- isLittleEndian = ReadString(2) == "II";
- // Next 2 bytes are always the same.
- if (ReadUShort() != 0x002A)
- throw new Exception("Error in TIFF data");
- // Get the offset to the IFD (image file directory)
- uint ifdOffset = ReadUint();
- // Note that this offset is from the first byte of the TIFF header. Jump to the IFD.
- fileStream.Position = ifdOffset + tiffHeaderStart;
- // Catalogue this first IFD (there will be another IFD)
- CatalogueIFD();
- // There's more data stored in the subifd, the offset to which is found in tag 0x8769.
- // As with all TIFF offsets, it will be relative to the first byte of the TIFF header.
- uint offset;
- if (!GetTagValue(0x8769, out offset))
- throw new Exception("Unable to locate Exif data");
- // Jump to the exif SubIFD
- fileStream.Position = offset + tiffHeaderStart;
- // Add the subIFD to the catalogue too
- CatalogueIFD();
- // Go to the GPS IFD and catalogue that too. It's an optional
- // section.
- if (GetTagValue(0x8825, out offset))
- {
- // Jump to the GPS SubIFD
- fileStream.Position = offset + tiffHeaderStart;
- // Add the subIFD to the catalogue too
- CatalogueIFD();
- }
- }
- #endregion
- #region Exif data catalog and retrieval methods
- public bool GetTagValue<T>(ExifTags tag, out T result)
- {
- return GetTagValue((ushort)tag, out result);
- }
- /// <summary>
- /// Retrieves an Exif value with the requested tag ID
- /// </summary>
- /// <param name="tagID"></param>
- /// <param name="result"></param>
- /// <returns></returns>
- public bool GetTagValue<T>(ushort tagID, out T result)
- {
- ushort tiffDataType;
- uint numberOfComponents;
- byte[] tagData = GetTagBytes(tagID, out tiffDataType, out numberOfComponents);
- if (tagData == null)
- {
- result = default(T);
- return false;
- }
- byte fieldLength = GetTIFFFieldLength(tiffDataType);
- // Convert the data to the appropriate datatype. Note the weird boxing via object.
- // The compiler doesn't like it otherwise.
- switch (tiffDataType)
- {
- case 1:
- // unsigned byte
- if (numberOfComponents == 1)
- result = (T)(object)tagData[0];
- else
- result = (T)(object)tagData;
- return true;
- case 2:
- // ascii string
- string str = Encoding.ASCII.GetString(tagData);
- // There may be a null character within the string
- int nullCharIndex = str.IndexOf('\0');
- if (nullCharIndex != -1)
- str = str.Substring(0, nullCharIndex);
- // Special processing for dates.
- if (typeof(T) == typeof(DateTime))
- {
- result =
- (T)(object)DateTime.ParseExact(str, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture);
- return true;
- }
- result = (T)(object)str;
- return true;
- case 3:
- // unsigned short
- if (numberOfComponents == 1)
- result = (T)(object)ToUShort(tagData);
- else
- result = (T)(object)GetArray(tagData, fieldLength, ToUShort);
- return true;
- case 4:
- // unsigned long
- if (numberOfComponents == 1)
- result = (T)(object)ToUint(tagData);
- else
- result = (T)(object)GetArray(tagData, fieldLength, ToUint);
- return true;
- case 5:
- // unsigned rational
- if (numberOfComponents == 1)
- result = (T)(object)ToURational(tagData);
- else
- result = (T)(object)GetArray(tagData, fieldLength, ToURational);
- return true;
- case 6:
- // signed byte
- if (numberOfComponents == 1)
- result = (T)(object)ToSByte(tagData);
- else
- result = (T)(object)GetArray(tagData, fieldLength, ToSByte);
- return true;
- case 7:
- // undefined. Treat it as an unsigned integer.
- if (numberOfComponents == 1)
- result = (T)(object)ToUint(tagData);
- else
- result = (T)(object)GetArray(tagData, fieldLength, ToUint);
- return true;
- case 8:
- // Signed short
- if (numberOfComponents == 1)
- result = (T)(object)ToShort(tagData);
- else
- result = (T)(object)GetArray(tagData, fieldLength, ToShort);
- return true;
- case 9:
- // Signed long
- if (numberOfComponents == 1)
- result = (T)(object)ToInt(tagData);
- else
- result = (T)(object)GetArray(tagData, fieldLength, ToInt);
- return true;
- case 10:
- // signed rational
- if (numberOfComponents == 1)
- result = (T)(object)ToRational(tagData);
- else
- result = (T)(object)GetArray(tagData, fieldLength, ToRational);
- return true;
- case 11:
- // single float
- if (numberOfComponents == 1)
- result = (T)(object)ToSingle(tagData);
- else
- result = (T)(object)GetArray(tagData, fieldLength, ToSingle);
- return true;
- case 12:
- // double float
- if (numberOfComponents == 1)
- result = (T)(object)ToDouble(tagData);
- else
- result = (T)(object)GetArray(tagData, fieldLength, ToDouble);
- return true;
- default:
- throw new Exception(string.Format("Unknown TIFF datatype: {0}", tiffDataType));
- }
- }
- /// <summary>
- /// Gets the data in the specified tag ID, starting from before the IFD block.
- /// </summary>
- /// <param name="tiffDataType"></param>
- /// <param name="numberOfComponents">The number of items which make up the data item - i.e. for a string, this will be the
- /// number of characters in the string</param>
- /// <param name="tagID"></param>
- private byte[] GetTagBytes(ushort tagID, out ushort tiffDataType, out uint numberOfComponents)
- {
- // Get the tag's offset from the catalogue and do some basic error checks
- if (fileStream == null || reader == null || catalogue == null || !catalogue.ContainsKey(tagID))
- {
- tiffDataType = 0;
- numberOfComponents = 0;
- return null;
- }
- long tagOffset = catalogue[tagID];
- // Jump to the TIFF offset
- fileStream.Position = tagOffset;
- // Read the tag number from the file
- ushort currentTagID = ReadUShort();
- if (currentTagID != tagID)
- throw new Exception("Tag number not at expected offset");
- // Read the offset to the Exif IFD
- tiffDataType = ReadUShort();
- numberOfComponents = ReadUint();
- byte[] tagData = ReadBytes(4);
- // If the total space taken up by the field is longer than the
- // 2 bytes afforded by the tagData, tagData will contain an offset
- // to the actual data.
- var dataSize = (int)(numberOfComponents * GetTIFFFieldLength(tiffDataType));
- if (dataSize > 4)
- {
- ushort offsetAddress = ToUShort(tagData);
- return ReadBytes(offsetAddress, dataSize);
- }
- // The value is stored in the tagData starting from the left
- Array.Resize(ref tagData, dataSize);
- return tagData;
- }
- /// <summary>
- /// Records all Exif tags and their offsets within
- /// the file from the current IFD
- /// </summary>
- private void CatalogueIFD()
- {
- if (catalogue == null)
- catalogue = new Dictionary<ushort, long>();
- // Assume we're just before the IFD.
- // First 2 bytes is the number of entries in this IFD
- ushort entryCount = ReadUShort();
- for (ushort currentEntry = 0; currentEntry < entryCount; currentEntry++)
- {
- ushort currentTagNumber = ReadUShort();
- // Record this in the catalogue
- catalogue[currentTagNumber] = fileStream.Position - 2;
- // Go to the end of this item (10 bytes, as each entry is 12 bytes long)
- reader.BaseStream.Seek(10, SeekOrigin.Current);
- }
- }
- #endregion
- #region IDisposable Members
- public void Dispose()
- {
- // Make sure the file handle is released
- if (reader != null)
- reader.Close();
- if (fileStream != null)
- fileStream.Close();
- }
- #endregion
- }
- }
|