]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - tim/prune/jpeg/drew/ExifReader.java
Version 19, May 2018
[GpsPrune.git] / tim / prune / jpeg / drew / ExifReader.java
index 4c90cabc687a6d7b0417632c36802cfba68134b6..c8c1b7c41bf4c0f8a2df9f240183226df1755a23 100644 (file)
-package tim.prune.jpeg.drew;\r
-\r
-import java.io.File;\r
-import java.util.HashMap;\r
-\r
-import tim.prune.jpeg.ExifGateway;\r
-import tim.prune.jpeg.JpegData;\r
-\r
-/**\r
- * Extracts Exif data from a JPEG header segment\r
- * Based on Drew Noakes' Metadata extractor at http://drewnoakes.com\r
- * which in turn is based on code from Jhead http://www.sentex.net/~mwandel/jhead/\r
- */\r
-public class ExifReader\r
-{\r
-       /** The JPEG segment as an array of bytes */\r
-       private final byte[] _data;\r
-\r
-       /**\r
-        * Represents the native byte ordering used in the JPEG segment.  If true,\r
-        * then we're using Motorola ordering (Big endian), else we're using Intel\r
-        * ordering (Little endian).\r
-        */\r
-       private boolean _isMotorolaByteOrder;\r
-\r
-       /** Thumbnail offset */\r
-       private int _thumbnailOffset = -1;\r
-       /** Thumbnail length */\r
-       private int _thumbnailLength = -1;\r
-\r
-       /** The number of bytes used per format descriptor */\r
-       private static final int[] BYTES_PER_FORMAT = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8};\r
-\r
-       /** The number of formats known */\r
-       private static final int MAX_FORMAT_CODE = 12;\r
-\r
-       // Format types\r
-       // Note: Cannot use the DataFormat enumeration in the case statement that uses these tags.\r
-       //         Is there a better way?\r
-       //private static final int FMT_BYTE = 1;\r
-       private static final int FMT_STRING = 2;\r
-       //private static final int FMT_USHORT = 3;\r
-       //private static final int FMT_ULONG = 4;\r
-       private static final int FMT_URATIONAL = 5;\r
-       //private static final int FMT_SBYTE = 6;\r
-       //private static final int FMT_UNDEFINED = 7;\r
-       //private static final int FMT_SSHORT = 8;\r
-       //private static final int FMT_SLONG = 9;\r
-       private static final int FMT_SRATIONAL = 10;\r
-       //private static final int FMT_SINGLE = 11;\r
-       //private static final int FMT_DOUBLE = 12;\r
-\r
-       public static final int TAG_EXIF_OFFSET = 0x8769;\r
-       public static final int TAG_INTEROP_OFFSET = 0xA005;\r
-       public static final int TAG_GPS_INFO_OFFSET = 0x8825;\r
-       public static final int TAG_MAKER_NOTE = 0x927C;\r
-\r
-       public static final int TIFF_HEADER_START_OFFSET = 6;\r
-\r
-       /** GPS tag version GPSVersionID 0 0 BYTE 4 */\r
-       public static final int TAG_GPS_VERSION_ID = 0x0000;\r
-       /** North or South Latitude GPSLatitudeRef 1 1 ASCII 2 */\r
-       public static final int TAG_GPS_LATITUDE_REF = 0x0001;\r
-       /** Latitude GPSLatitude 2 2 RATIONAL 3 */\r
-       public static final int TAG_GPS_LATITUDE = 0x0002;\r
-       /** East or West Longitude GPSLongitudeRef 3 3 ASCII 2 */\r
-       public static final int TAG_GPS_LONGITUDE_REF = 0x0003;\r
-       /** Longitude GPSLongitude 4 4 RATIONAL 3 */\r
-       public static final int TAG_GPS_LONGITUDE = 0x0004;\r
-       /** Altitude reference GPSAltitudeRef 5 5 BYTE 1 */\r
-       public static final int TAG_GPS_ALTITUDE_REF = 0x0005;\r
-       /** Altitude GPSAltitude 6 6 RATIONAL 1 */\r
-       public static final int TAG_GPS_ALTITUDE = 0x0006;\r
-       /** GPS time (atomic clock) GPSTimeStamp 7 7 RATIONAL 3 */\r
-       public static final int TAG_GPS_TIMESTAMP = 0x0007;\r
-       /** GPS date (atomic clock) GPSDateStamp 23 1d RATIONAL 3 */\r
-       public static final int TAG_GPS_DATESTAMP = 0x001d;\r
-       /** "Original" Exif timestamp */\r
-       public static final int TAG_DATETIME_ORIGINAL = 0x9003;\r
-       /** "Creation" or "Digitized" timestamp */\r
-       public static final int TAG_DATETIME_DIGITIZED = 0x9004;\r
-       /** Thumbnail offset */\r
-       private static final int TAG_THUMBNAIL_OFFSET = 0x0201;\r
-       /** Thumbnail length */\r
-       private static final int TAG_THUMBNAIL_LENGTH = 0x0202;\r
-       /** Orientation of image */\r
-       private static final int TAG_ORIENTATION = 0x0112;\r
-       /** Bearing direction of image */\r
-       private static final int TAG_BEARING = 0x0011;\r
-\r
-\r
-       /**\r
-        * Creates an ExifReader for a Jpeg file\r
-        * @param inFile File object to attempt to read from\r
-        * @throws JpegException on failure\r
-        */\r
-       public ExifReader(File inFile) throws JpegException\r
-       {\r
-               _data = JpegSegmentReader.readExifSegment(inFile);\r
-       }\r
-\r
-       /**\r
-        * Performs the Exif data extraction\r
-        * @return the GPS data found in the file\r
-        */\r
-       public JpegData extract()\r
-       {\r
-               JpegData metadata = new JpegData();\r
-               if (_data==null)\r
-                       return metadata;\r
-\r
-               // check for the header length\r
-               if (_data.length<=14)\r
-               {\r
-                       metadata.addError("Exif data segment must contain at least 14 bytes");\r
-                       return metadata;\r
-               }\r
-\r
-               // check for the header preamble\r
-               if (!"Exif\0\0".equals(new String(_data, 0, 6)))\r
-               {\r
-                       metadata.addError("Exif data segment doesn't begin with 'Exif'");\r
-                       return metadata;\r
-               }\r
-\r
-               // this should be either "MM" or "II"\r
-               String byteOrderIdentifier = new String(_data, 6, 2);\r
-               if (!setByteOrder(byteOrderIdentifier))\r
-               {\r
-                       metadata.addError("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier);\r
-                       return metadata;\r
-               }\r
-\r
-               // Check the next two values are 0x2A as expected\r
-               if (get16Bits(8)!=0x2a)\r
-               {\r
-                       metadata.addError("Invalid Exif start - should have 0x2A at offset 8 in Exif header");\r
-                       return metadata;\r
-               }\r
-\r
-               int firstDirectoryOffset = get32Bits(10) + TIFF_HEADER_START_OFFSET;\r
-\r
-               // Check that offset is within range\r
-               if (firstDirectoryOffset>=_data.length - 1)\r
-               {\r
-                       metadata.addError("First exif directory offset is beyond end of Exif data segment");\r
-                       // First directory normally starts 14 bytes in -- try it here and catch another error in the worst case\r
-                       firstDirectoryOffset = 14;\r
-               }\r
-\r
-               HashMap<Integer, String> processedDirectoryOffsets = new HashMap<Integer, String>();\r
-\r
-               // 0th IFD (we merge with Exif IFD)\r
-               processDirectory(metadata, false, processedDirectoryOffsets, firstDirectoryOffset, TIFF_HEADER_START_OFFSET);\r
-\r
-               return metadata;\r
-       }\r
-\r
-\r
-       /**\r
-        * Set the byte order identifier\r
-        * @param byteOrderIdentifier String from exif\r
-        * @return true if recognised, false otherwise\r
-        */\r
-       private boolean setByteOrder(String byteOrderIdentifier)\r
-       {\r
-               if ("MM".equals(byteOrderIdentifier)) {\r
-                       _isMotorolaByteOrder = true;\r
-               } else if ("II".equals(byteOrderIdentifier)) {\r
-                       _isMotorolaByteOrder = false;\r
-               } else {\r
-                       return false;\r
-               }\r
-               return true;\r
-       }\r
-\r
-\r
-       /**\r
-        * Recursive call to process one of the nested Tiff IFD directories.\r
-        * 2 bytes: number of tags\r
-        * for each tag\r
-        *   2 bytes: tag type\r
-        *   2 bytes: format code\r
-        *   4 bytes: component count\r
-        */\r
-       private void processDirectory(JpegData inMetadata, boolean inIsGPS, HashMap<Integer, String> inDirectoryOffsets,\r
-               int inDirOffset, int inTiffHeaderOffset)\r
-       {\r
-               // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist\r
-               if (inDirectoryOffsets.containsKey(Integer.valueOf(inDirOffset)))\r
-                       return;\r
-\r
-               // remember that we've visited this directory so that we don't visit it again later\r
-               inDirectoryOffsets.put(Integer.valueOf(inDirOffset), "processed");\r
-\r
-               if (inDirOffset >= _data.length || inDirOffset < 0)\r
-               {\r
-                       inMetadata.addError("Ignored directory marked to start outside data segment");\r
-                       return;\r
-               }\r
-\r
-               // First two bytes in the IFD are the number of tags in this directory\r
-               int dirTagCount = get16Bits(inDirOffset);\r
-               // If no tags, exit without complaint\r
-               if (dirTagCount == 0) return;\r
-\r
-               if (!isDirectoryLengthValid(inDirOffset, inTiffHeaderOffset))\r
-               {\r
-                       inMetadata.addError("Directory length is not valid");\r
-                       return;\r
-               }\r
-\r
-               inMetadata.setExifDataPresent();\r
-               // Handle each tag in this directory\r
-               for (int tagNumber = 0; tagNumber<dirTagCount; tagNumber++)\r
-               {\r
-                       final int tagOffset = calculateTagOffset(inDirOffset, tagNumber);\r
-\r
-                       // 2 bytes for the tag type\r
-                       final int tagType = get16Bits(tagOffset);\r
-\r
-                       // 2 bytes for the format code\r
-                       final int formatCode = get16Bits(tagOffset + 2);\r
-                       if (formatCode < 1 || formatCode > MAX_FORMAT_CODE)\r
-                       {\r
-                               inMetadata.addError("Invalid format code: " + formatCode);\r
-                               continue;\r
-                       }\r
-\r
-                       // 4 bytes dictate the number of components in this tag's data\r
-                       final int componentCount = get32Bits(tagOffset + 4);\r
-                       if (componentCount < 0)\r
-                       {\r
-                               inMetadata.addError("Negative component count in EXIF");\r
-                               continue;\r
-                       }\r
-                       // each component may have more than one byte... calculate the total number of bytes\r
-                       final int byteCount = componentCount * BYTES_PER_FORMAT[formatCode];\r
-                       final int tagValueOffset = calculateTagValueOffset(byteCount, tagOffset, inTiffHeaderOffset);\r
-                       if (tagValueOffset < 0 || tagValueOffset > _data.length)\r
-                       {\r
-                               inMetadata.addError("Illegal pointer offset value in EXIF");\r
-                               continue;\r
-                       }\r
-\r
-                       // Check that this tag isn't going to allocate outside the bounds of the data array.\r
-                       // This addresses an uncommon OutOfMemoryError.\r
-                       if (byteCount < 0 || tagValueOffset + byteCount > _data.length)\r
-                       {\r
-                               inMetadata.addError("Illegal number of bytes: " + byteCount);\r
-                               continue;\r
-                       }\r
-\r
-                       // Calculate the value as an offset for cases where the tag represents a directory\r
-                       final int subdirOffset = inTiffHeaderOffset + get32Bits(tagValueOffset);\r
-\r
-                       // Look in both basic Exif tags (for timestamp, thumbnail) and Gps tags (for lat, long, altitude, timestamp)\r
-                       switch (tagType)\r
-                       {\r
-                               case TAG_EXIF_OFFSET:\r
-                                       processDirectory(inMetadata, false, inDirectoryOffsets, subdirOffset, inTiffHeaderOffset);\r
-                                       continue;\r
-                               case TAG_INTEROP_OFFSET:\r
-                                       // ignore\r
-                                       continue;\r
-                               case TAG_GPS_INFO_OFFSET:\r
-                                       processDirectory(inMetadata, true, inDirectoryOffsets, subdirOffset, inTiffHeaderOffset);\r
-                                       continue;\r
-                               case TAG_MAKER_NOTE:\r
-                                       // ignore\r
-                                       continue;\r
-                               default:\r
-                                       // not a known directory, so must just be a normal tag\r
-                                       if (inIsGPS)\r
-                                       {\r
-                                               processGpsTag(inMetadata, tagType, tagValueOffset, componentCount, formatCode);\r
-                                       }\r
-                                       else\r
-                                       {\r
-                                               processExifTag(inMetadata, tagType, tagValueOffset, componentCount, formatCode);\r
-                                       }\r
-                                       break;\r
-                       }\r
-               }\r
-\r
-               // at the end of each IFD is an optional link to the next IFD\r
-               final int finalTagOffset = calculateTagOffset(inDirOffset, dirTagCount);\r
-               int nextDirectoryOffset = get32Bits(finalTagOffset);\r
-               if (nextDirectoryOffset != 0)\r
-               {\r
-                       nextDirectoryOffset += inTiffHeaderOffset;\r
-                       if (nextDirectoryOffset>=_data.length)\r
-                       {\r
-                               // Last 4 bytes of IFD reference another IFD with an address that is out of bounds\r
-                               return;\r
-                       }\r
-                       else if (nextDirectoryOffset < inDirOffset)\r
-                       {\r
-                               // Last 4 bytes of IFD reference another IFD with an address before the start of this directory\r
-                               return;\r
-                       }\r
-                       // the next directory is of same type as this one\r
-                       processDirectory(inMetadata, false, inDirectoryOffsets, nextDirectoryOffset, inTiffHeaderOffset);\r
-               }\r
-       }\r
-\r
-\r
-       /**\r
-        * Check if the directory length is valid\r
-        * @param dirStartOffset start offset for directory\r
-        * @param tiffHeaderOffset Tiff header offeset\r
-        * @return true if length is valid\r
-        */\r
-       private boolean isDirectoryLengthValid(int inDirStartOffset, int inTiffHeaderOffset)\r
-       {\r
-               int dirTagCount = get16Bits(inDirStartOffset);\r
-               int dirLength = (2 + (12 * dirTagCount) + 4);\r
-               if (dirLength + inDirStartOffset + inTiffHeaderOffset >= _data.length)\r
-               {\r
-                       // Note: Files that had thumbnails trimmed with jhead 1.3 or earlier might trigger this\r
-                       return false;\r
-               }\r
-               return true;\r
-       }\r
-\r
-\r
-       /**\r
-        * Process a GPS tag and put the contents in the given metadata\r
-        * @param inMetadata metadata holding extracted values\r
-        * @param inTagType tag type (eg latitude)\r
-        * @param inTagValueOffset start offset in data array\r
-        * @param inComponentCount component count for tag\r
-        * @param inFormatCode format code, eg byte\r
-        */\r
-       private void processGpsTag(JpegData inMetadata, int inTagType, int inTagValueOffset,\r
-               int inComponentCount, int inFormatCode)\r
-       {\r
-               try\r
-               {\r
-                       // Only interested in tags latref, lat, longref, lon, altref, alt and gps timestamp\r
-                       switch (inTagType)\r
-                       {\r
-                               case TAG_GPS_LATITUDE_REF:\r
-                                       inMetadata.setLatitudeRef(readString(inTagValueOffset, inFormatCode, inComponentCount));\r
-                                       break;\r
-                               case TAG_GPS_LATITUDE:\r
-                                       Rational[] latitudes = readRationalArray(inTagValueOffset, inFormatCode, inComponentCount);\r
-                                       inMetadata.setLatitude(new double[] {latitudes[0].doubleValue(), latitudes[1].doubleValue(),\r
-                                               ExifGateway.convertToPositiveValue(latitudes[2].getNumerator(), latitudes[2].getDenominator())});\r
-                                       break;\r
-                               case TAG_GPS_LONGITUDE_REF:\r
-                                       inMetadata.setLongitudeRef(readString(inTagValueOffset, inFormatCode, inComponentCount));\r
-                                       break;\r
-                               case TAG_GPS_LONGITUDE:\r
-                                       Rational[] longitudes = readRationalArray(inTagValueOffset, inFormatCode, inComponentCount);\r
-                                       inMetadata.setLongitude(new double[] {longitudes[0].doubleValue(), longitudes[1].doubleValue(),\r
-                                               ExifGateway.convertToPositiveValue(longitudes[2].getNumerator(), longitudes[2].getDenominator())});\r
-                                       break;\r
-                               case TAG_GPS_ALTITUDE_REF:\r
-                                       inMetadata.setAltitudeRef(_data[inTagValueOffset]);\r
-                                       break;\r
-                               case TAG_GPS_ALTITUDE:\r
-                                       inMetadata.setAltitude(readRational(inTagValueOffset, inFormatCode, inComponentCount).intValue());\r
-                                       break;\r
-                               case TAG_GPS_TIMESTAMP:\r
-                                       Rational[] times = readRationalArray(inTagValueOffset, inFormatCode, inComponentCount);\r
-                                       inMetadata.setGpsTimestamp(new int[] {times[0].intValue(), times[1].intValue(), times[2].intValue()});\r
-                                       break;\r
-                               case TAG_GPS_DATESTAMP:\r
-                                       Rational[] dates = readRationalArray(inTagValueOffset, inFormatCode, inComponentCount);\r
-                                       if (dates != null) {\r
-                                               inMetadata.setGpsDatestamp(new int[] {dates[0].intValue(), dates[1].intValue(), dates[2].intValue()});\r
-                                       }\r
-                                       else\r
-                                       {\r
-                                               // Not in rational array format, but maybe as String?\r
-                                               String date = readString(inTagValueOffset, inFormatCode, inComponentCount);\r
-                                               if (date != null && date.length() == 10) {\r
-                                                       inMetadata.setGpsDatestamp(new int[] {Integer.parseInt(date.substring(0, 4)),\r
-                                                               Integer.parseInt(date.substring(5, 7)), Integer.parseInt(date.substring(8))});\r
-                                               }\r
-                                       }\r
-                                       break;\r
-                               case TAG_BEARING:\r
-                                       Rational val = readRational(inTagValueOffset, inFormatCode, inComponentCount);\r
-                                       if (val != null) {\r
-                                               inMetadata.setBearing(val.doubleValue());\r
-                                       }\r
-                                       break;\r
-                               default: // ignore all other tags\r
-                       }\r
-               }\r
-               catch (Exception e) {} // ignore and continue\r
-       }\r
-\r
-\r
-       /**\r
-        * Process a general Exif tag\r
-        * @param inMetadata metadata holding extracted values\r
-        * @param inTagType tag type (eg latitude)\r
-        * @param inTagValueOffset start offset in data array\r
-        * @param inComponentCount component count for tag\r
-        * @param inFormatCode format code, eg byte\r
-        */\r
-       private void processExifTag(JpegData inMetadata, int inTagType, int inTagValueOffset,\r
-               int inComponentCount, int inFormatCode)\r
-       {\r
-               // Only interested in original timestamp, thumbnail offset and thumbnail length\r
-               if (inTagType == TAG_DATETIME_ORIGINAL) {\r
-                       inMetadata.setOriginalTimestamp(readString(inTagValueOffset, inFormatCode, inComponentCount));\r
-               }\r
-               else if (inTagType == TAG_DATETIME_DIGITIZED) {\r
-                       inMetadata.setDigitizedTimestamp(readString(inTagValueOffset, inFormatCode, inComponentCount));\r
-               }\r
-               else if (inTagType == TAG_THUMBNAIL_OFFSET) {\r
-                       _thumbnailOffset = TIFF_HEADER_START_OFFSET + get16Bits(inTagValueOffset);\r
-                       extractThumbnail(inMetadata);\r
-               }\r
-               else if (inTagType == TAG_THUMBNAIL_LENGTH) {\r
-                       _thumbnailLength = get16Bits(inTagValueOffset);\r
-                       extractThumbnail(inMetadata);\r
-               }\r
-               else if (inTagType == TAG_ORIENTATION) {\r
-                       if (inMetadata.getOrientationCode() < 1) {\r
-                               inMetadata.setOrientationCode(get16Bits(inTagValueOffset));\r
-                       }\r
-               }\r
-       }\r
-\r
-       /**\r
-        * Attempt to extract the thumbnail image\r
-        */\r
-       private void extractThumbnail(JpegData inMetadata)\r
-       {\r
-               if (_thumbnailOffset > 0 && _thumbnailLength > 0 && inMetadata.getThumbnailImage() == null)\r
-               {\r
-                       byte[] thumbnailBytes = new byte[_thumbnailLength];\r
-                       System.arraycopy(_data, _thumbnailOffset, thumbnailBytes, 0, _thumbnailLength);\r
-                       inMetadata.setThumbnailImage(thumbnailBytes);\r
-               }\r
-       }\r
-\r
-\r
-       /**\r
-        * Calculate the tag value offset\r
-        * @param inByteCount\r
-        * @param inDirEntryOffset\r
-        * @param inTiffHeaderOffset\r
-        * @return new offset\r
-        */\r
-       private int calculateTagValueOffset(int inByteCount, int inDirEntryOffset, int inTiffHeaderOffset)\r
-       {\r
-               if (inByteCount > 4)\r
-               {\r
-                       // If it's bigger than 4 bytes, the dir entry contains an offset.\r
-                       // dirEntryOffset must be passed, as some makers (e.g. FujiFilm) incorrectly use an\r
-                       // offset relative to the start of the makernote itself, not the TIFF segment.\r
-                       final int offsetVal = get32Bits(inDirEntryOffset + 8);\r
-                       if (offsetVal + inByteCount > _data.length)\r
-                       {\r
-                               // Bogus pointer offset and / or bytecount value\r
-                               return -1; // signal error\r
-                       }\r
-                       return inTiffHeaderOffset + offsetVal;\r
-               }\r
-               else\r
-               {\r
-                       // 4 bytes or less and value is in the dir entry itself\r
-                       return inDirEntryOffset + 8;\r
-               }\r
-       }\r
-\r
-\r
-       /**\r
-        * Creates a String from the _data buffer starting at the specified offset,\r
-        * and ending where byte=='\0' or where length==maxLength.\r
-        * @param inOffset start offset\r
-        * @param inFormatCode format code - should be string\r
-        * @param inMaxLength max length of string\r
-        * @return contents of tag, or null if format incorrect\r
-        */\r
-       private String readString(int inOffset, int inFormatCode, int inMaxLength)\r
-       {\r
-               if (inFormatCode != FMT_STRING) return null;\r
-               // Calculate length\r
-               int length = 0;\r
-               while ((inOffset + length)<_data.length\r
-                       && _data[inOffset + length]!='\0'\r
-                       && length < inMaxLength)\r
-               {\r
-                       length++;\r
-               }\r
-               return new String(_data, inOffset, length);\r
-       }\r
-\r
-       /**\r
-        * Creates a Rational from the _data buffer starting at the specified offset\r
-        * @param inOffset start offset\r
-        * @param inFormatCode format code - should be srational or urational\r
-        * @param inCount component count - should be 1\r
-        * @return contents of tag as a Rational object\r
-        */\r
-       private Rational readRational(int inOffset, int inFormatCode, int inCount)\r
-       {\r
-               // Check the format is a single rational as expected\r
-               if (inFormatCode != FMT_SRATIONAL && inFormatCode != FMT_URATIONAL\r
-                       || inCount != 1) return null;\r
-               return new Rational(get32Bits(inOffset), get32Bits(inOffset + 4));\r
-       }\r
-\r
-\r
-       /**\r
-        * Creates a Rational array from the _data buffer starting at the specified offset\r
-        * @param inOffset start offset\r
-        * @param inFormatCode format code - should be srational or urational\r
-        * @param inCount component count - number of components\r
-        * @return contents of tag as an array of Rational objects\r
-        */\r
-       private Rational[] readRationalArray(int inOffset, int inFormatCode, int inCount)\r
-       {\r
-               // Check the format is rational as expected\r
-               if (inFormatCode != FMT_SRATIONAL && inFormatCode != FMT_URATIONAL)\r
-                       return null;\r
-               // Build array of Rationals\r
-               Rational[] answer = new Rational[inCount];\r
-               for (int i=0; i<inCount; i++)\r
-                       answer[i] = new Rational(get32Bits(inOffset + (8 * i)), get32Bits(inOffset + 4 + (8 * i)));\r
-               return answer;\r
-       }\r
-\r
-\r
-       /**\r
-        * Determine the offset at which a given InteropArray entry begins within the specified IFD.\r
-        * @param dirStartOffset the offset at which the IFD starts\r
-        * @param entryNumber the zero-based entry number\r
-        */\r
-       private int calculateTagOffset(int dirStartOffset, int entryNumber)\r
-       {\r
-               // add 2 bytes for the tag count\r
-               // each entry is 12 bytes, so we skip 12 * the number seen so far\r
-               return dirStartOffset + 2 + (12 * entryNumber);\r
-       }\r
-\r
-\r
-       /**\r
-        * Get a 16 bit value from file's native byte order.  Between 0x0000 and 0xFFFF.\r
-        */\r
-       private int get16Bits(int offset)\r
-       {\r
-               if (offset<0 || offset+2>_data.length)\r
-                       throw new ArrayIndexOutOfBoundsException("attempt to read data outside of exif segment (index "\r
-                               + offset + " where max index is " + (_data.length - 1) + ")");\r
-\r
-               if (_isMotorolaByteOrder) {\r
-                       // Motorola - MSB first\r
-                       return (_data[offset] << 8 & 0xFF00) | (_data[offset + 1] & 0xFF);\r
-               } else {\r
-                       // Intel ordering - LSB first\r
-                       return (_data[offset + 1] << 8 & 0xFF00) | (_data[offset] & 0xFF);\r
-               }\r
-       }\r
-\r
-\r
-       /**\r
-        * Get a 32 bit value from file's native byte order.\r
-        */\r
-       private int get32Bits(int offset)\r
-       {\r
-               if (offset < 0 || offset+4 > _data.length)\r
-                       throw new ArrayIndexOutOfBoundsException("attempt to read data outside of exif segment (index "\r
-                               + offset + " where max index is " + (_data.length - 1) + ")");\r
-\r
-               if (_isMotorolaByteOrder)\r
-               {\r
-                       // Motorola - MSB first\r
-                       return (_data[offset] << 24 & 0xFF000000) |\r
-                                       (_data[offset + 1] << 16 & 0xFF0000) |\r
-                                       (_data[offset + 2] << 8 & 0xFF00) |\r
-                                       (_data[offset + 3] & 0xFF);\r
-               }\r
-               else\r
-               {\r
-                       // Intel ordering - LSB first\r
-                       return (_data[offset + 3] << 24 & 0xFF000000) |\r
-                                       (_data[offset + 2] << 16 & 0xFF0000) |\r
-                                       (_data[offset + 1] << 8 & 0xFF00) |\r
-                                       (_data[offset] & 0xFF);\r
-               }\r
-       }\r
-}\r
+package tim.prune.jpeg.drew;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import tim.prune.jpeg.JpegData;
+
+
+/**
+ * Extracts Exif data from a JPEG header segment
+ * Based on Drew Noakes' Metadata extractor at https://drewnoakes.com
+ * which in turn is based on code from Jhead http://www.sentex.net/~mwandel/jhead/
+ */
+public class ExifReader
+{
+       /** Magic numbers to mark the beginning of all Jpegs */
+       private static final int MAGIC_JPEG_BYTE_1 = 0xFF;
+       private static final int MAGIC_JPEG_BYTE_2 = 0xD8;
+
+       /** 6-byte preamble before starting the TIFF data. */
+       private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0";
+
+       /** Start of segment marker */
+       private static final byte SEGMENT_SOS = (byte) 0xDA;
+
+       /** End of segment marker */
+       private static final byte MARKER_EOI = (byte) 0xD9;
+
+       /**
+        * Processes the provided JPEG data, and extracts the specified JPEG segments into a JpegData object.
+        * @param inFile a {@link File} from which the JPEG data will be read.
+        */
+       public static JpegData readMetadata(File inFile) throws ExifException
+       {
+               JpegData jpegData = new JpegData();
+               BufferedInputStream bStream = null;
+
+               try
+               {
+                       bStream = new BufferedInputStream(new FileInputStream(inFile));
+                       byte[] segmentBytes = readSegments(bStream);
+                       if (segmentBytes != null)
+                       {
+                               // Got the bytes for the required segment, now extract the data
+                               extract(segmentBytes, jpegData);
+                       }
+               }
+               catch (IOException ioe) {
+                       throw new ExifException("IO Exception: " + ioe.getMessage());
+               }
+               finally
+               {
+                       if (bStream != null) {
+                               try {
+                                       bStream.close();
+                               } catch (IOException ioe) {}
+                       }
+               }
+               return jpegData;
+       }
+
+       /**
+        * Reads the relevant segment and returns the bytes.
+        */
+       private static byte[] readSegments(final BufferedInputStream bStream)
+               throws ExifException, IOException
+       {
+               // first two bytes should be JPEG magic number
+               final int magic1 = bStream.read() & 0xFF;
+               final int magic2 = bStream.read() & 0xFF;
+               if (magic1 != MAGIC_JPEG_BYTE_1 || magic2 != MAGIC_JPEG_BYTE_2) {
+                       throw new ExifException("Jpeg file failed Magic check");
+               }
+
+               final Byte segmentTypeByte = (byte)0xE1; // JpegSegmentType.APP1.byteValue;
+
+               do {
+                       // Find the segment marker. Markers are zero or more 0xFF bytes, followed
+                       // by a 0xFF and then a byte not equal to 0x00 or 0xFF.
+
+                       final short segmentIdentifier = (short) bStream.read();
+
+                       // We must have at least one 0xFF byte
+                       if (segmentIdentifier != 0xFF)
+                               throw new ExifException("Expected JPEG segment start identifier 0xFF, not 0x" + Integer.toHexString(segmentIdentifier).toUpperCase());
+
+                       // Read until we have a non-0xFF byte. This identifies the segment type.
+                       byte currSegmentType = (byte) bStream.read();
+                       while (currSegmentType == (byte)0xFF) {
+                               currSegmentType = (byte) bStream.read();
+                       }
+
+                       if (currSegmentType == 0)
+                               throw new ExifException("Expected non-zero byte as part of JPEG marker identifier");
+
+                       if (currSegmentType == SEGMENT_SOS) {
+                               // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
+                               // have to search for the two bytes: 0xFF 0xD9 (EOI).
+                               // It comes last so simply return at this point
+                               return null;
+                       }
+
+                       if (currSegmentType == MARKER_EOI) {
+                               // the 'End-Of-Image' segment -- this should never be found in this fashion
+                               return null;
+                       }
+
+                       // next 2-bytes are <segment-size>: [high-byte] [low-byte]
+                       int segmentLength = (bStream.read() << 8) + bStream.read();
+                       // segment length includes size bytes, so subtract two
+                       segmentLength -= 2;
+
+                       if (segmentLength < 0)
+                               throw new ExifException("JPEG segment size would be less than zero");
+
+                       byte[] segmentBytes = new byte[segmentLength];
+                       int bytesRead = bStream.read(segmentBytes, 0, segmentLength);
+                       // Bail if not all bytes read in one go - otherwise following sections will be out of step
+                       if (bytesRead != segmentLength) {
+                               throw new ExifException("Tried to read " + segmentLength + " bytes but only got " + bytesRead);
+                       }
+                       // Check whether we are interested in this segment
+                       if (segmentTypeByte == currSegmentType)
+                       {
+                               // Pass the appropriate byte arrays to reader.
+                               if (canProcess(segmentBytes)) {
+                                       return segmentBytes;
+                               }
+                       }
+
+               } while (true);
+       }
+
+       private static boolean canProcess(final byte[] segmentBytes)
+       {
+               return segmentBytes.length >= JPEG_EXIF_SEGMENT_PREAMBLE.length() && new String(segmentBytes, 0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equalsIgnoreCase(JPEG_EXIF_SEGMENT_PREAMBLE);
+       }
+
+       /**
+        * Given the bytes, parse them recursively to fill the JpegData
+        * @param segmentBytes bytes out of the file
+        * @param jdata jpeg data to be populated
+        */
+       private static void extract(final byte[] segmentBytes, final JpegData jdata)
+       {
+               if (segmentBytes == null)
+                       throw new NullPointerException("segmentBytes cannot be null");
+
+               try
+               {
+                       ByteArrayReader reader = new ByteArrayReader(segmentBytes);
+
+                       // Check for the header preamble
+                       try {
+                               if (!reader.getString(0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equals(JPEG_EXIF_SEGMENT_PREAMBLE)) {
+                                       // TODO what to do with this error state?
+                                       System.err.println("Invalid JPEG Exif segment preamble");
+                                       return;
+                               }
+                       } catch (ExifException e) {
+                               // TODO what to do with this error state?
+                               e.printStackTrace(System.err);
+                               return;
+                       }
+
+                       // Read the TIFF-formatted Exif data
+                       TiffProcessor.processTiff(
+                               reader,
+                               jdata,
+                               JPEG_EXIF_SEGMENT_PREAMBLE.length()
+                       );
+
+               } catch (ExifException e) {
+                       // TODO what to do with this error state?
+                       e.printStackTrace(System.err);
+               } catch (IOException e) {
+                       // TODO what to do with this error state?
+                       e.printStackTrace(System.err);
+               }
+       }
+}