using System; using System.Collections; using System.Text; using System.IO; using com.drew.metadata; using com.drew.imaging.jpg; using com.drew.lang; using com.utils; /// /// This class based upon code from Jhead, a C program for extracting and /// manipulating the Exif data within files written by Matthias Wandel. /// http://www.sentex.net/~mwandel/jhead/ /// /// Jhead is public domain software - that is, you can do whatever /// you want with it, and include it software that is licensed under /// the GNU or the BSD license, or whatever other licence you choose, /// including proprietary closed source licenses. Similarly, I release /// this Java version under the same license, though I do ask that you /// leave this header in tact. /// /// This class was first written by Drew Noakes in Java. /// /// This is public domain software - that is, you can do whatever you want /// with it, and include it software that is licensed under the GNU or the /// BSD license, or whatever other licence you choose, including proprietary /// closed source licenses. I do ask that you leave this header in tact. /// /// If you make modifications to this code that you think would benefit the /// wider community, please send me a copy and I'll post it on my site. /// /// If you make use of this code, Drew Noakes will appreciate hearing /// about it: drew@drewnoakes.com /// /// Latest Java version of this software kept at /// http://drewnoakes.com/ /// /// Created on 28 April 2002, 23:54 /// Modified 04 Aug 2002 /// - Renamed constants to be inline with changes to ExifTagValues interface /// - Substituted usage of JDK 1.4 features (java.nio package) /// Modified 29 Oct 2002 (v1.2) /// - Proper traversing of Exif file structure and complete refactor & tidy of /// the codebase (a few unnoticed bugs removed) /// - Reads makernote data for 6 families of camera (5 makes) /// - Tags now stored in directories... use the IFD_* constants to refer to the /// image file directory you require (Exif, Interop, GPS and Makernote*) /// -- this avoids collisions where two tags share the same code /// - Takes componentCount of unknown tags into account /// - Now understands GPS tags (thanks to Colin Briton for his help with this) /// - Some other bug fixes, pointed out by users around the world. Thanks! /// Modified 27 Nov 2002 (v2.0) /// - Renamed to ExifReader /// - Moved to new package com.drew.metadata.exif /// /// The C# class was made by Ferret Renaud: /// renaud91@free.fr /// If you find a bug in the C# code, feel free to mail me. /// namespace com.drew.metadata.exif { /// /// Extracts Exif data from a JPEG header segment, providing information about /// the camera/scanner/capture device (if available). /// Information is encapsulated in an Metadata object. /// public class ExifReader : MetadataReader { /// /// The JPEG segment as an array of bytes. /// private byte[] _data; /// /// Represents the native byte ordering used in the JPEG segment. /// If true, then we're using Motorolla ordering (Big endian), else /// we're using Intel ordering (Little endian). /// private bool _isMotorollaByteOrder; /// /// Bean instance to store information about the image and camera/scanner/capture device. /// private Metadata _metadata; /// /// The number of bytes used per format descriptor. /// private static readonly int[] BYTES_PER_FORMAT = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 }; /// /// The number of formats known. /// private static readonly int MAX_FORMAT_CODE = 12; // the format enumeration // TODO use the new DataFormat enumeration instead of these values private static readonly int FMT_BYTE = 1; private static readonly int FMT_STRING = 2; private static readonly int FMT_USHORT = 3; private static readonly int FMT_ULONG = 4; private static readonly int FMT_URATIONAL = 5; private static readonly int FMT_SBYTE = 6; private static readonly int FMT_UNDEFINED = 7; private static readonly int FMT_SSHORT = 8; private static readonly int FMT_SLONG = 9; private static readonly int FMT_SRATIONAL = 10; private static readonly int FMT_SINGLE = 11; private static readonly int FMT_DOUBLE = 12; public const int TAG_EXIF_OFFSET = 0x8769; public const int TAG_INTEROP_OFFSET = 0xA005; public const int TAG_GPS_INFO_OFFSET = 0x8825; public const int TAG_MAKER_NOTE = 0x927C; // NOT READONLY public static int TIFF_HEADER_START_OFFSET = 6; /// /// Constructor of the object /// /// the file to read public ExifReader(FileInfo file) : this( new JpegSegmentReader(file).ReadSegment( JpegSegmentReader.SEGMENT_APP1)) { } /** * Creates an ExifReader for the given JPEG header segment. */ /// /// Constructor of the object /// /// the data public ExifReader(byte[] data) { _data = data; } /// /// Performs the Exif data extraction, returning a new instance of Metadata. /// /// a new instance of Metadata public Metadata Extract() { return Extract(new Metadata()); } /// /// Performs the Exif data extraction, adding found values to the specified instance of Metadata. /// /// where to add meta data /// the metadata public Metadata Extract(Metadata metadata) { _metadata = metadata; if (_data == null) { return _metadata; } // once we know there's some data, create the directory and start working on it Directory directory = _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.ExifDirectory")); if (_data.Length <= 14) { directory.AddError("Exif data segment must contain at least 14 bytes"); return _metadata; } if (!"Exif\0\0".Equals(Utils.Decode(_data, 0, 6, false))) { directory.AddError("Exif data segment doesn't begin with 'Exif'"); return _metadata; } // this should be either "MM" or "II" string byteOrderIdentifier = Utils.Decode(_data, 6, 2, false); if (!SetByteOrder(byteOrderIdentifier)) { directory.AddError("Unclear distinction between Motorola/Intel byte ordering"); return _metadata; } // Check the next two values for correctness. if (Get16Bits(8) != 0x2a) { directory.AddError("Invalid Exif start - should have 0x2A at offSet 8 in Exif header"); return _metadata; } int firstDirectoryOffSet = Get32Bits(10) + TIFF_HEADER_START_OFFSET; // David Ekholm sent an digital camera image that has this problem if (firstDirectoryOffSet >= _data.Length - 1) { directory.AddError("First exif directory offSet is beyond end of Exif data segment"); // First directory normally starts 14 bytes in -- try it here and catch another error in the worst case firstDirectoryOffSet = 14; } // 0th IFD (we merge with Exif IFD) ProcessDirectory(directory, firstDirectoryOffSet); // after the extraction process, if we have the correct tags, we may be able to extract thumbnail information ExtractThumbnail(directory); return _metadata; } /// /// Extract Thumbnail /// /// where to take the information private void ExtractThumbnail(Directory exifDirectory) { if (!(exifDirectory is ExifDirectory)) { return; } if (!exifDirectory.ContainsTag(ExifDirectory.TAG_THUMBNAIL_LENGTH) || !exifDirectory.ContainsTag(ExifDirectory.TAG_THUMBNAIL_OFFSET)) { return; } try { int offSet = exifDirectory.GetInt(ExifDirectory.TAG_THUMBNAIL_OFFSET); int Length = exifDirectory.GetInt(ExifDirectory.TAG_THUMBNAIL_LENGTH); byte[] result = new byte[Length]; for (int i = 0; i < result.Length; i++) { result[i] = _data[TIFF_HEADER_START_OFFSET + offSet + i]; } exifDirectory.SetObject( ExifDirectory.TAG_THUMBNAIL_DATA, result); } catch (Exception e) { exifDirectory.AddError("Unable to extract thumbnail: " + e.Message); } } /// /// Sets Motorolla byte order /// /// the Motorolla byte order identifier (MM=true, II=false) /// private bool SetByteOrder(string byteOrderIdentifier) { if ("MM".Equals(byteOrderIdentifier)) { _isMotorollaByteOrder = true; } else if ("II".Equals(byteOrderIdentifier)) { _isMotorollaByteOrder = false; } else { return false; } return true; } /// /// Process one of the nested Tiff IFD directories. /// 2 bytes: number of tags for each tag /// 2 bytes: tag type /// 2 bytes: format code /// 4 bytes: component count /// /// the directory /// where to start private void ProcessDirectory(Directory directory, int dirStartOffSet) { if (dirStartOffSet >= _data.Length || dirStartOffSet < 0) { directory.AddError("Ignored directory marked to start outside data segement"); return; } // First two bytes in the IFD are the tag count int dirTagCount = Get16Bits(dirStartOffSet); if (!isDirectoryLengthValid(dirStartOffSet)) { directory.AddError("Illegally sized directory"); return; } // Handle each tag in this directory for (int dirEntry = 0; dirEntry < dirTagCount; dirEntry++) { int dirEntryOffSet = CalculateDirectoryEntryOffSet(dirStartOffSet, dirEntry); int tagType = Get16Bits(dirEntryOffSet); // Console.WriteLine("TagType="+tagType); int formatCode = Get16Bits(dirEntryOffSet + 2); if (formatCode < 0 || formatCode > MAX_FORMAT_CODE) { directory.AddError("Invalid format code: " + formatCode); continue; } // 4 bytes indicating number of formatCode type data for this tag int componentCount = Get32Bits(dirEntryOffSet + 4); int byteCount = componentCount * BYTES_PER_FORMAT[formatCode]; int tagValueOffSet = CalculateTagValueOffSet(byteCount, dirEntryOffSet); if (tagValueOffSet < 0) { directory.AddError("Illegal pointer offSet aValue in EXIF"); continue; } // Calculate the aValue as an offSet for cases where the tag represents directory int subdirOffSet = TIFF_HEADER_START_OFFSET + Get32Bits(tagValueOffSet); if (tagType == TAG_EXIF_OFFSET) { // Console.WriteLine("TagType is TAG_EXIF_OFFSET ("+tagType+")"); ProcessDirectory( _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.ExifDirectory")), subdirOffSet); continue; } else if (tagType == TAG_INTEROP_OFFSET) { // Console.WriteLine("TagType is TAG_INTEROP_OFFSET ("+tagType+")"); ProcessDirectory( _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.ExifInteropDirectory")), subdirOffSet); continue; } else if (tagType == TAG_GPS_INFO_OFFSET) { // Console.WriteLine("TagType is TAG_GPS_INFO_OFFSET ("+tagType+")"); ProcessDirectory( _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.GpsDirectory")), subdirOffSet); continue; } else if (tagType == TAG_MAKER_NOTE) { // Console.WriteLine("TagType is TAG_MAKER_NOTE ("+tagType+")"); ProcessMakerNote(tagValueOffSet); continue; } else { // Console.WriteLine("TagType is ???? ("+tagType+")"); ProcessTag( directory, tagType, tagValueOffSet, componentCount, formatCode); } } // At the end of each IFD is an optional link to the next IFD. This link is after // the 2-byte tag count, and after 12 bytes for each of these tags, hence int nextDirectoryOffSet = Get32Bits(dirStartOffSet + 2 + 12 * dirTagCount); if (nextDirectoryOffSet != 0) { nextDirectoryOffSet += TIFF_HEADER_START_OFFSET; if (nextDirectoryOffSet >= _data.Length) { // Last 4 bytes of IFD reference another IFD with an address that is out of bounds // Note this could have been caused by jhead 1.3 cropping too much return; } // the next directory is of same type as this one ProcessDirectory(directory, nextDirectoryOffSet); } } /// /// Determine the camera model and makernote format /// /// the sub offset dir private void ProcessMakerNote(int subdirOffSet) { // Console.WriteLine("ProcessMakerNote value="+subdirOffSet); // Determine the camera model and makernote format Directory exifDirectory = _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.ExifDirectory")); if (exifDirectory == null) { return; } string cameraModel = exifDirectory.GetString(ExifDirectory.TAG_MAKE); if ("OLYMP".Equals(Utils.Decode(_data, subdirOffSet, 5, false))) { // Olympus Makernote ProcessDirectory( _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.OlympusMakernoteDirectory")), subdirOffSet + 8); } else if ( cameraModel != null && cameraModel.Trim().ToUpper().StartsWith("NIKON")) { if ("Nikon".Equals(Utils.Decode(_data, subdirOffSet, 5, false))) { // There are two scenarios here: // Type 1: // :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon........... // :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................ // Type 3: // :0000: 4E 69 6B 6F 6E 00 02 00-00 00 4D 4D 00 2A 00 00 Nikon....MM.*... // :0010: 00 08 00 1E 00 01 00 07-00 00 00 04 30 32 30 30 ............0200 if (_data[subdirOffSet + 6] == 1) { // Nikon type 1 Makernote ProcessDirectory( _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.NikonType1MakernoteDirectory")), subdirOffSet + 8); } else if (_data[subdirOffSet + 6] == 2) { // Nikon type 3 Makernote // TODO at this point we're assuming that the MM ordering is continuous // with the rest of the file // (this seems to be the case, but I don't have many sample images) // TODO shouldn't be messing around with this static variable (not threadsafe) // instead, should pass an additional offSet to the ProcessDirectory method int oldHeaderStartOffSet = TIFF_HEADER_START_OFFSET; TIFF_HEADER_START_OFFSET = subdirOffSet + 10; ProcessDirectory( _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.NikonType3MakernoteDirectory")), subdirOffSet + 18); TIFF_HEADER_START_OFFSET = oldHeaderStartOffSet; } else { exifDirectory.AddError( "Unsupported makernote data ignored."); } } else { // Nikon type 2 Makernote ProcessDirectory( _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.NikonType2MakernoteDirectory")), subdirOffSet); } } else if ("Canon".ToUpper().Equals(cameraModel.ToUpper())) { // Console.WriteLine("CanonMakernoteDirectory is found"); // Canon Makernote ProcessDirectory( _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.CanonMakernoteDirectory")), subdirOffSet); } else if ("Casio".ToUpper().Equals(cameraModel.ToUpper())) { // Casio Makernote ProcessDirectory( _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.CasioMakernoteDirectory")), subdirOffSet); } else if ( "FUJIFILM".Equals(Utils.Decode(_data, subdirOffSet, 8, false)) || "Fujifilm".ToUpper().Equals(cameraModel.ToUpper())) { // Fujifile Makernote bool byteOrderBefore = _isMotorollaByteOrder; // bug in fujifilm makernote ifd means we temporarily use Intel byte ordering _isMotorollaByteOrder = false; // the 4 bytes after "FUJIFILM" in the makernote point to the start of the makernote // IFD, though the offSet is relative to the start of the makernote, not the TIFF // header (like everywhere else) int ifdStart = subdirOffSet + Get32Bits(subdirOffSet + 8); ProcessDirectory( _metadata.GetDirectory(Type.GetType("com.drew.metadata.exif.FujiFilmMakernoteDirectory")), ifdStart); _isMotorollaByteOrder = byteOrderBefore; } else { // TODO how to store makernote data when it's not from a supported camera model? exifDirectory.AddError("Unsupported makernote data ignored."); } } /// /// Indicates if Directory Length is valid or not /// /// where to start /// true if Directory Length is valid private bool isDirectoryLengthValid(int dirStartOffSet) { int dirTagCount = Get16Bits(dirStartOffSet); int dirLength = (2 + (12 * dirTagCount) + 4); return !(dirLength + dirStartOffSet + TIFF_HEADER_START_OFFSET >= _data.Length); } /// /// Processes tag /// /// the directory /// the tag type /// the offset value /// the component count /// the format code private void ProcessTag( Directory directory, int tagType, int tagValueOffSet, int componentCount, int formatCode) { // Directory simply stores raw values // The display side uses a Descriptor class per directory to turn the // raw values into 'pretty' descriptions if (formatCode == FMT_UNDEFINED || formatCode == FMT_STRING) { string s = null; if (tagType == ExifDirectory.TAG_USER_COMMENT) { s = ReadCommentString( tagValueOffSet, componentCount, formatCode); } else { s = ReadString(tagValueOffSet, componentCount); } directory.SetObject(tagType, s); } else if (formatCode == FMT_SRATIONAL || formatCode == FMT_URATIONAL) { if (componentCount == 1) { Rational rational = new Rational( Get32Bits(tagValueOffSet), Get32Bits(tagValueOffSet + 4)); directory.SetObject(tagType, rational); } else { Rational[] rationals = new Rational[componentCount]; for (int i = 0; i < componentCount; i++) { rationals[i] = new Rational( Get32Bits(tagValueOffSet + (8 * i)), Get32Bits(tagValueOffSet + 4 + (8 * i))); } directory.SetObject(tagType, rationals); } } else if (formatCode == FMT_SBYTE || formatCode == FMT_BYTE) { if (componentCount == 1) { // this may need to be a byte, but I think casting to int is fine int b = _data[tagValueOffSet]; directory.SetObject(tagType, b); } else { int[] bytes = new int[componentCount]; for (int i = 0; i < componentCount; i++) { bytes[i] = _data[tagValueOffSet + i]; } directory.SetIntArray(tagType, bytes); } } else if (formatCode == FMT_SINGLE || formatCode == FMT_DOUBLE) { if (componentCount == 1) { int i = _data[tagValueOffSet]; directory.SetObject(tagType, i); } else { int[] ints = new int[componentCount]; for (int i = 0; i < componentCount; i++) { ints[i] = _data[tagValueOffSet + i]; } directory.SetIntArray(tagType, ints); } } else if (formatCode == FMT_USHORT || formatCode == FMT_SSHORT) { if (componentCount == 1) { int i = Get16Bits(tagValueOffSet); directory.SetObject(tagType, i); } else { int[] ints = new int[componentCount]; for (int i = 0; i < componentCount; i++) { ints[i] = Get16Bits(tagValueOffSet + (i * 2)); } directory.SetIntArray(tagType, ints); } } else if (formatCode == FMT_SLONG || formatCode == FMT_ULONG) { if (componentCount == 1) { int i = Get32Bits(tagValueOffSet); directory.SetObject(tagType, i); } else { int[] ints = new int[componentCount]; for (int i = 0; i < componentCount; i++) { ints[i] = Get32Bits(tagValueOffSet + (i * 4)); } directory.SetIntArray(tagType, ints); } } else { directory.AddError("unknown format code " + formatCode); } } /// /// Calculates tag value offset /// /// the byte count /// the dir entry offset /// -1 if error, or the valus offset private int CalculateTagValueOffSet(int byteCount, int dirEntryOffSet) { if (byteCount > 4) { // If its bigger than 4 bytes, the dir entry contains an offSet. // TODO if we're reading FujiFilm makernote tags, the offSet is relative to the start of the makernote itself, not the TIFF segment int offSetVal = Get32Bits(dirEntryOffSet + 8); if (offSetVal + byteCount > _data.Length) { // Bogus pointer offSet and / or bytecount aValue return -1; // signal error } return TIFF_HEADER_START_OFFSET + offSetVal; } else { // 4 bytes or less and aValue is in the dir entry itself return dirEntryOffSet + 8; } } /// /// Creates a string from the _data buffer starting at the specified offSet, /// and ending where byte=='\0' or where Length==maxLength. /// /// the offset /// the max length /// a string representing what was read private string ReadString(int offSet, int maxLength) { int Length = 0; while ((offSet + Length) < _data.Length && _data[offSet + Length] != '\0' && Length < maxLength) { Length++; } return Utils.Decode(_data, offSet, Length, false); } /// /// A special case of ReadString that handle Exif UserComment reading. /// This method is necessary as certain camere models prefix the comment string /// with "ASCII\0", which is all that would be returned by ReadString(...). /// /// the tag value offset /// the component count /// the format code /// a string private string ReadCommentString( int tagValueOffSet, int componentCount, int formatCode) { // Olympus has this padded with trailing spaces. Remove these first. // ArrayIndexOutOfBoundsException bug fixed by Hendrik Wördehoff - 20 Sep 2002 int byteCount = componentCount * BYTES_PER_FORMAT[formatCode]; for (int i = byteCount - 1; i >= 0; i--) { if (_data[tagValueOffSet + i] == ' ') { _data[tagValueOffSet + i] = (byte) '\0'; } else { break; } } // Copy the comment if ("ASCII".Equals(Utils.Decode(_data, tagValueOffSet, 5, false))) { for (int i = 5; i < 10; i++) { byte b = _data[tagValueOffSet + i]; if (b != '\0' && b != ' ') { return ReadString(tagValueOffSet + i, 1999); } } } else if ("UNICODE".Equals(Utils.Decode(_data, tagValueOffSet, 7, false))) { int start = tagValueOffSet+7; for (int i = start; i < 10+start; i++) { byte b = _data[i]; if (b == 0 || (char)b == ' ') { continue; } else { start = i; break; } } int end = _data.Length; // TODO find a way to cut the string properly return Utils.Decode(_data, start, end-start, true); } // TODO implement support for UNICODE and JIS UserComment encodings..? return ReadString(tagValueOffSet, 1999); } /// /// Determine the offSet at which a given InteropArray entry begins within the specified IFD. /// /// the offSet at which the IFD starts /// the zero-based entry number /// the directory entry offset private int CalculateDirectoryEntryOffSet( int ifdStartOffSet, int entryNumber) { return (ifdStartOffSet + 2 + (12 * entryNumber)); } /** * */ /// /// Gets a 16 bit aValue from file's native byte order. Between 0x0000 and 0xFFFF. /// /// the offset /// a 16 bit int private int Get16Bits(int offSet) { if (offSet < 0 || offSet >= _data.Length) { throw new IndexOutOfRangeException( "attempt to read data outside of exif segment (index " + offSet + " where max index is " + (_data.Length - 1) + ")"); } if (_isMotorollaByteOrder) { // Motorola big first return (_data[offSet] << 8 & 0xFF00) | (_data[offSet + 1] & 0xFF); } else { // Intel ordering return (_data[offSet + 1] << 8 & 0xFF00) | (_data[offSet] & 0xFF); } } /// /// Gets a 32 bit aValue from file's native byte order. /// /// the offset /// a 32b int private int Get32Bits(int offSet) { if (offSet < 0 || offSet >= _data.Length) { throw new IndexOutOfRangeException( "attempt to read data outside of exif segment (index " + offSet + " where max index is " + (_data.Length - 1) + ")"); } if (_isMotorollaByteOrder) { // Motorola big first return (int) ( ((uint)(_data[offSet] << 24 & 0xFF000000)) | ((uint)(_data[offSet + 1] << 16 & 0xFF0000)) | ((uint)(_data[offSet + 2] << 8 & 0xFF00)) | ((uint)(_data[offSet + 3] & 0xFF))); } else { // Intel ordering return (int) ( ((uint)(_data[offSet + 3] << 24 & 0xFF000000)) | ((uint)(_data[offSet + 2] << 16 & 0xFF0000)) | ((uint)(_data[offSet + 1] << 8 & 0xFF00)) | ((uint)(_data[offSet] & 0xFF))); } } } }