]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/drew/jpeg/ExifReader.java
Version 2, March 2007
[GpsPrune.git] / tim / prune / drew / jpeg / ExifReader.java
1 package tim.prune.drew.jpeg;\r
2 \r
3 import java.io.File;\r
4 import java.util.HashMap;\r
5 \r
6 /**\r
7  * Extracts Exif data from a JPEG header segment\r
8  * Based on Drew Noakes' Metadata extractor at http://drewnoakes.com\r
9  * which in turn is based on code from Jhead http://www.sentex.net/~mwandel/jhead/\r
10  */\r
11 public class ExifReader\r
12 {\r
13         /** The JPEG segment as an array of bytes */\r
14         private final byte[] _data;\r
15 \r
16         /**\r
17          * Represents the native byte ordering used in the JPEG segment.  If true,\r
18          * then we're using Motorola ordering (Big endian), else we're using Intel\r
19          * ordering (Little endian).\r
20          */\r
21         private boolean _isMotorolaByteOrder;\r
22 \r
23         /**\r
24          * The number of bytes used per format descriptor.\r
25          */\r
26         private static final int[] BYTES_PER_FORMAT = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8};\r
27 \r
28         /**\r
29          * The number of formats known.\r
30          */\r
31         private static final int MAX_FORMAT_CODE = 12;\r
32 \r
33         // Format types\r
34         // Note: Cannot use the DataFormat enumeration in the case statement that uses these tags.\r
35         //         Is there a better way?\r
36         private static final int FMT_BYTE = 1;\r
37         private static final int FMT_STRING = 2;\r
38         private static final int FMT_USHORT = 3;\r
39         private static final int FMT_ULONG = 4;\r
40         private static final int FMT_URATIONAL = 5;\r
41         private static final int FMT_SBYTE = 6;\r
42         private static final int FMT_UNDEFINED = 7;\r
43         private static final int FMT_SSHORT = 8;\r
44         private static final int FMT_SLONG = 9;\r
45         private static final int FMT_SRATIONAL = 10;\r
46         private static final int FMT_SINGLE = 11;\r
47         private static final int FMT_DOUBLE = 12;\r
48 \r
49         public static final int TAG_EXIF_OFFSET = 0x8769;\r
50         public static final int TAG_INTEROP_OFFSET = 0xA005;\r
51         public static final int TAG_GPS_INFO_OFFSET = 0x8825;\r
52         public static final int TAG_MAKER_NOTE = 0x927C;\r
53 \r
54         public static final int TIFF_HEADER_START_OFFSET = 6;\r
55 \r
56         /** GPS tag version GPSVersionID 0 0 BYTE 4 */\r
57         public static final int TAG_GPS_VERSION_ID = 0x0000;\r
58         /** North or South Latitude GPSLatitudeRef 1 1 ASCII 2 */\r
59         public static final int TAG_GPS_LATITUDE_REF = 0x0001;\r
60         /** Latitude GPSLatitude 2 2 RATIONAL 3 */\r
61         public static final int TAG_GPS_LATITUDE = 0x0002;\r
62         /** East or West Longitude GPSLongitudeRef 3 3 ASCII 2 */\r
63         public static final int TAG_GPS_LONGITUDE_REF = 0x0003;\r
64         /** Longitude GPSLongitude 4 4 RATIONAL 3 */\r
65         public static final int TAG_GPS_LONGITUDE = 0x0004;\r
66         /** Altitude reference GPSAltitudeRef 5 5 BYTE 1 */\r
67         public static final int TAG_GPS_ALTITUDE_REF = 0x0005;\r
68         /** Altitude GPSAltitude 6 6 RATIONAL 1 */\r
69         public static final int TAG_GPS_ALTITUDE = 0x0006;\r
70         /** GPS time (atomic clock) GPSTimeStamp 7 7 RATIONAL 3 */\r
71         public static final int TAG_GPS_TIMESTAMP = 0x0007;\r
72         /** GPS date (atomic clock) GPSDateStamp 23 1d RATIONAL 3 */\r
73         public static final int TAG_GPS_DATESTAMP = 0x001d;\r
74 \r
75         /**\r
76          * Creates an ExifReader for a Jpeg file.\r
77          * @param inFile File object to attempt to read from\r
78          * @throws JpegProcessingException on failure\r
79          */\r
80         public ExifReader(File inFile) throws JpegException\r
81         {\r
82                 JpegSegmentData segments = JpegSegmentReader.readSegments(inFile);\r
83                 _data = segments.getSegment(JpegSegmentReader.SEGMENT_APP1);\r
84         }\r
85 \r
86         /**\r
87          * Performs the Exif data extraction\r
88          * @return the GPS data found in the file\r
89          */\r
90         public JpegData extract()\r
91         {\r
92                 JpegData metadata = new JpegData();\r
93                 if (_data==null)\r
94                         return metadata;\r
95 \r
96                 // check for the header length\r
97                 if (_data.length<=14)\r
98                 {\r
99                         metadata.addError("Exif data segment must contain at least 14 bytes");\r
100                         return metadata;\r
101                 }\r
102 \r
103                 // check for the header preamble\r
104                 if (!"Exif\0\0".equals(new String(_data, 0, 6)))\r
105                 {\r
106                         metadata.addError("Exif data segment doesn't begin with 'Exif'");\r
107                         return metadata;\r
108                 }\r
109 \r
110                 // this should be either "MM" or "II"\r
111                 String byteOrderIdentifier = new String(_data, 6, 2);\r
112                 if (!setByteOrder(byteOrderIdentifier))\r
113                 {\r
114                         metadata.addError("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier);\r
115                         return metadata;\r
116                 }\r
117 \r
118                 // Check the next two values are 0x2A as expected\r
119                 if (get16Bits(8)!=0x2a)\r
120                 {\r
121                         metadata.addError("Invalid Exif start - should have 0x2A at offset 8 in Exif header");\r
122                         return metadata;\r
123                 }\r
124 \r
125                 int firstDirectoryOffset = get32Bits(10) + TIFF_HEADER_START_OFFSET;\r
126 \r
127                 // Check that offset is within range\r
128                 if (firstDirectoryOffset>=_data.length - 1)\r
129                 {\r
130                         metadata.addError("First exif directory offset is beyond end of Exif data segment");\r
131                         // First directory normally starts 14 bytes in -- try it here and catch another error in the worst case\r
132                         firstDirectoryOffset = 14;\r
133                 }\r
134 \r
135                 HashMap processedDirectoryOffsets = new HashMap();\r
136 \r
137                 // 0th IFD (we merge with Exif IFD)\r
138                 processDirectory(metadata, false, processedDirectoryOffsets, firstDirectoryOffset, TIFF_HEADER_START_OFFSET);\r
139 \r
140                 return metadata;\r
141         }\r
142 \r
143 \r
144         /**\r
145          * Set the byte order identifier\r
146          * @param byteOrderIdentifier String from exif\r
147          * @return true if recognised, false otherwise\r
148          */\r
149         private boolean setByteOrder(String byteOrderIdentifier)\r
150         {\r
151                 if ("MM".equals(byteOrderIdentifier)) {\r
152                         _isMotorolaByteOrder = true;\r
153                 } else if ("II".equals(byteOrderIdentifier)) {\r
154                         _isMotorolaByteOrder = false;\r
155                 } else {\r
156                         return false;\r
157                 }\r
158                 return true;\r
159         }\r
160 \r
161 \r
162         /**\r
163          * Recursive call to process one of the nested Tiff IFD directories.\r
164          * 2 bytes: number of tags\r
165          * for each tag\r
166          *   2 bytes: tag type\r
167          *   2 bytes: format code\r
168          *   4 bytes: component count\r
169          */\r
170         private void processDirectory(JpegData inMetadata, boolean inIsGPS, HashMap inDirectoryOffsets,\r
171                 int inDirOffset, int inTiffHeaderOffset)\r
172         {\r
173                 // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist\r
174                 if (inDirectoryOffsets.containsKey(new Integer(inDirOffset)))\r
175                         return;\r
176 \r
177                 // remember that we've visited this directory so that we don't visit it again later\r
178                 inDirectoryOffsets.put(new Integer(inDirOffset), "processed");\r
179 \r
180                 if (inDirOffset >= _data.length || inDirOffset < 0)\r
181                 {\r
182                         inMetadata.addError("Ignored directory marked to start outside data segment");\r
183                         return;\r
184                 }\r
185 \r
186                 // First two bytes in the IFD are the number of tags in this directory\r
187                 int dirTagCount = get16Bits(inDirOffset);\r
188                 // If no tags, exit without complaint\r
189                 if (dirTagCount == 0) return;\r
190 \r
191                 if (!isDirectoryLengthValid(inDirOffset, inTiffHeaderOffset))\r
192                 {\r
193                         inMetadata.addError("Directory length is not valid");\r
194                         return;\r
195                 }\r
196 \r
197                 inMetadata.setExifDataPresent();\r
198                 // Handle each tag in this directory\r
199                 for (int tagNumber = 0; tagNumber<dirTagCount; tagNumber++)\r
200                 {\r
201                         final int tagOffset = calculateTagOffset(inDirOffset, tagNumber);\r
202 \r
203                         // 2 bytes for the tag type\r
204                         final int tagType = get16Bits(tagOffset);\r
205 \r
206                         // 2 bytes for the format code\r
207                         final int formatCode = get16Bits(tagOffset + 2);\r
208                         if (formatCode < 1 || formatCode > MAX_FORMAT_CODE)\r
209                         {\r
210                                 inMetadata.addError("Invalid format code: " + formatCode);\r
211                                 continue;\r
212                         }\r
213 \r
214                         // 4 bytes dictate the number of components in this tag's data\r
215                         final int componentCount = get32Bits(tagOffset + 4);\r
216                         if (componentCount < 0)\r
217                         {\r
218                                 inMetadata.addError("Negative component count in EXIF");\r
219                                 continue;\r
220                         }\r
221                         // each component may have more than one byte... calculate the total number of bytes\r
222                         final int byteCount = componentCount * BYTES_PER_FORMAT[formatCode];\r
223                         final int tagValueOffset = calculateTagValueOffset(byteCount, tagOffset, inTiffHeaderOffset);\r
224                         if (tagValueOffset < 0 || tagValueOffset > _data.length)\r
225                         {\r
226                                 inMetadata.addError("Illegal pointer offset value in EXIF");\r
227                                 continue;\r
228                         }\r
229 \r
230                         // Check that this tag isn't going to allocate outside the bounds of the data array.\r
231                         // This addresses an uncommon OutOfMemoryError.\r
232                         if (byteCount < 0 || tagValueOffset + byteCount > _data.length)\r
233                         {\r
234                                 inMetadata.addError("Illegal number of bytes: " + byteCount);\r
235                                 continue;\r
236                         }\r
237 \r
238                         // Calculate the value as an offset for cases where the tag represents a directory\r
239                         final int subdirOffset = inTiffHeaderOffset + get32Bits(tagValueOffset);\r
240 \r
241                         // TODO: Also look for timestamp(s) in Exif for correlation - which directory?\r
242                         switch (tagType)\r
243                         {\r
244                                 case TAG_EXIF_OFFSET:\r
245                                         // ignore\r
246                                         continue;\r
247                                 case TAG_INTEROP_OFFSET:\r
248                                         // ignore\r
249                                         continue;\r
250                                 case TAG_GPS_INFO_OFFSET:\r
251                                         processDirectory(inMetadata, true, inDirectoryOffsets, subdirOffset, inTiffHeaderOffset);\r
252                                         continue;\r
253                                 case TAG_MAKER_NOTE:\r
254                                         // ignore\r
255                                         continue;\r
256                                 default:\r
257                                         // not a known directory, so must just be a normal tag\r
258                                         // ignore if we're not in gps directory\r
259                                         if (inIsGPS)\r
260                                                 processGpsTag(inMetadata, tagType, tagValueOffset, componentCount, formatCode);\r
261                                         break;\r
262                         }\r
263                 }\r
264 \r
265                 // at the end of each IFD is an optional link to the next IFD\r
266                 final int finalTagOffset = calculateTagOffset(inDirOffset, dirTagCount);\r
267                 int nextDirectoryOffset = get32Bits(finalTagOffset);\r
268                 if (nextDirectoryOffset != 0)\r
269                 {\r
270                         nextDirectoryOffset += inTiffHeaderOffset;\r
271                         if (nextDirectoryOffset>=_data.length)\r
272                         {\r
273                                 // Last 4 bytes of IFD reference another IFD with an address that is out of bounds\r
274                                 return;\r
275                         }\r
276                         else if (nextDirectoryOffset < inDirOffset)\r
277                         {\r
278                                 // Last 4 bytes of IFD reference another IFD with an address before the start of this directory\r
279                                 return;\r
280                         }\r
281                         // the next directory is of same type as this one\r
282                         processDirectory(inMetadata, false, inDirectoryOffsets, nextDirectoryOffset, inTiffHeaderOffset);\r
283                 }\r
284         }\r
285 \r
286 \r
287         /**\r
288          * Check if the directory length is valid\r
289          * @param dirStartOffset start offset for directory\r
290          * @param tiffHeaderOffset Tiff header offeset\r
291          * @return true if length is valid\r
292          */\r
293         private boolean isDirectoryLengthValid(int inDirStartOffset, int inTiffHeaderOffset)\r
294         {\r
295                 int dirTagCount = get16Bits(inDirStartOffset);\r
296                 int dirLength = (2 + (12 * dirTagCount) + 4);\r
297                 if (dirLength + inDirStartOffset + inTiffHeaderOffset >= _data.length)\r
298                 {\r
299                         // Note: Files that had thumbnails trimmed with jhead 1.3 or earlier might trigger this\r
300                         return false;\r
301                 }\r
302                 return true;\r
303         }\r
304 \r
305 \r
306         /**\r
307          * Process a GPS tag and put the contents in the given metadata\r
308          * @param inMetadata metadata holding extracted values\r
309          * @param inTagType tag type (eg latitude)\r
310          * @param inTagValueOffset start offset in data array\r
311          * @param inComponentCount component count for tag\r
312          * @param inFormatCode format code, eg byte\r
313          */\r
314         private void processGpsTag(JpegData inMetadata, int inTagType, int inTagValueOffset,\r
315                 int inComponentCount, int inFormatCode)\r
316         {\r
317                 // Only interested in tags latref, lat, longref, lon, altref, alt and gps timestamp\r
318                 switch (inTagType)\r
319                 {\r
320                         case TAG_GPS_LATITUDE_REF:\r
321                                 inMetadata.setLatitudeRef(readString(inTagValueOffset, inFormatCode, inComponentCount));\r
322                                 break;\r
323                         case TAG_GPS_LATITUDE:\r
324                                 inMetadata.setLatitude(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
325                                 break;\r
326                         case TAG_GPS_LONGITUDE_REF:\r
327                                 inMetadata.setLongitudeRef(readString(inTagValueOffset, inFormatCode, inComponentCount));\r
328                                 break;\r
329                         case TAG_GPS_LONGITUDE:\r
330                                 inMetadata.setLongitude(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
331                                 break;\r
332                         case TAG_GPS_ALTITUDE_REF:\r
333                                 inMetadata.setAltitudeRef(_data[inTagValueOffset]);\r
334                                 break;\r
335                         case TAG_GPS_ALTITUDE:\r
336                                 inMetadata.setAltitude(readRational(inTagValueOffset, inFormatCode, inComponentCount));\r
337                                 break;\r
338                         case TAG_GPS_TIMESTAMP:\r
339                                 inMetadata.setTimestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
340                                 break;\r
341                         case TAG_GPS_DATESTAMP:\r
342                                 inMetadata.setDatestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
343                                 break;\r
344                         default: // ignore all other tags\r
345                 }\r
346         }\r
347 \r
348 \r
349         /**\r
350          * Calculate the tag value offset\r
351          * @param inByteCount\r
352          * @param inDirEntryOffset\r
353          * @param inTiffHeaderOffset\r
354          * @return new offset\r
355          */\r
356         private int calculateTagValueOffset(int inByteCount, int inDirEntryOffset, int inTiffHeaderOffset)\r
357         {\r
358                 if (inByteCount > 4)\r
359                 {\r
360                         // If it's bigger than 4 bytes, the dir entry contains an offset.\r
361                         // dirEntryOffset must be passed, as some makernote implementations (e.g. FujiFilm) incorrectly use an\r
362                         // offset relative to the start of the makernote itself, not the TIFF segment.\r
363                         final int offsetVal = get32Bits(inDirEntryOffset + 8);\r
364                         if (offsetVal + inByteCount > _data.length)\r
365                         {\r
366                                 // Bogus pointer offset and / or bytecount value\r
367                                 return -1; // signal error\r
368                         }\r
369                         return inTiffHeaderOffset + offsetVal;\r
370                 }\r
371                 else\r
372                 {\r
373                         // 4 bytes or less and value is in the dir entry itself\r
374                         return inDirEntryOffset + 8;\r
375                 }\r
376         }\r
377 \r
378 \r
379         /**\r
380          * Creates a String from the _data buffer starting at the specified offset,\r
381          * and ending where byte=='\0' or where length==maxLength.\r
382          * @param inOffset start offset\r
383          * @param inFormatCode format code - should be string\r
384          * @param inMaxLength max length of string\r
385          * @return contents of tag, or null if format incorrect\r
386          */\r
387         private String readString(int inOffset, int inFormatCode, int inMaxLength)\r
388         {\r
389                 if (inFormatCode != FMT_STRING) return null;\r
390                 // Calculate length\r
391                 int length = 0;\r
392                 while ((inOffset + length)<_data.length\r
393                         && _data[inOffset + length]!='\0'\r
394                         && length < inMaxLength)\r
395                 {\r
396                         length++;\r
397                 }\r
398                 return new String(_data, inOffset, length);\r
399         }\r
400 \r
401         /**\r
402          * Creates a Rational from the _data buffer starting at the specified offset\r
403          * @param inOffset start offset\r
404          * @param inFormatCode format code - should be srational or urational\r
405          * @param inCount component count - should be 1\r
406          * @return contents of tag as a Rational object\r
407          */\r
408         private Rational readRational(int inOffset, int inFormatCode, int inCount)\r
409         {\r
410                 // Check the format is a single rational as expected\r
411                 if (inFormatCode != FMT_SRATIONAL && inFormatCode != FMT_URATIONAL\r
412                         || inCount != 1) return null;\r
413                 return new Rational(get32Bits(inOffset), get32Bits(inOffset + 4));\r
414         }\r
415 \r
416 \r
417         /**\r
418          * Creates a Rational array from the _data buffer starting at the specified offset\r
419          * @param inOffset start offset\r
420          * @param inFormatCode format code - should be srational or urational\r
421          * @param inCount component count - number of components\r
422          * @return contents of tag as an array of Rational objects\r
423          */\r
424         private Rational[] readRationalArray(int inOffset, int inFormatCode, int inCount)\r
425         {\r
426                 // Check the format is rational as expected\r
427                 if (inFormatCode != FMT_SRATIONAL && inFormatCode != FMT_URATIONAL)\r
428                         return null;\r
429                 // Build array of Rationals\r
430                 Rational[] answer = new Rational[inCount];\r
431                 for (int i=0; i<inCount; i++)\r
432                         answer[i] = new Rational(get32Bits(inOffset + (8 * i)), get32Bits(inOffset + 4 + (8 * i)));\r
433                 return answer;\r
434         }\r
435 \r
436 \r
437         /**\r
438          * Determine the offset at which a given InteropArray entry begins within the specified IFD.\r
439          * @param dirStartOffset the offset at which the IFD starts\r
440          * @param entryNumber the zero-based entry number\r
441          */\r
442         private int calculateTagOffset(int dirStartOffset, int entryNumber)\r
443         {\r
444                 // add 2 bytes for the tag count\r
445                 // each entry is 12 bytes, so we skip 12 * the number seen so far\r
446                 return dirStartOffset + 2 + (12 * entryNumber);\r
447         }\r
448 \r
449 \r
450         /**\r
451          * Get a 16 bit value from file's native byte order.  Between 0x0000 and 0xFFFF.\r
452          */\r
453         private int get16Bits(int offset)\r
454         {\r
455                 if (offset<0 || offset+2>_data.length)\r
456                         throw new ArrayIndexOutOfBoundsException("attempt to read data outside of exif segment (index " + offset + " where max index is " + (_data.length - 1) + ")");\r
457 \r
458                 if (_isMotorolaByteOrder) {\r
459                         // Motorola - MSB first\r
460                         return (_data[offset] << 8 & 0xFF00) | (_data[offset + 1] & 0xFF);\r
461                 } else {\r
462                         // Intel ordering - LSB first\r
463                         return (_data[offset + 1] << 8 & 0xFF00) | (_data[offset] & 0xFF);\r
464                 }\r
465         }\r
466 \r
467 \r
468         /**\r
469          * Get a 32 bit value from file's native byte order.\r
470          */\r
471         private int get32Bits(int offset)\r
472         {\r
473                 if (offset < 0 || offset+4 > _data.length)\r
474                         throw new ArrayIndexOutOfBoundsException("attempt to read data outside of exif segment (index "\r
475                                 + offset + " where max index is " + (_data.length - 1) + ")");\r
476 \r
477                 if (_isMotorolaByteOrder)\r
478                 {\r
479                         // Motorola - MSB first\r
480                         return (_data[offset] << 24 & 0xFF000000) |\r
481                                         (_data[offset + 1] << 16 & 0xFF0000) |\r
482                                         (_data[offset + 2] << 8 & 0xFF00) |\r
483                                         (_data[offset + 3] & 0xFF);\r
484                 }\r
485                 else\r
486                 {\r
487                         // Intel ordering - LSB first\r
488                         return (_data[offset + 3] << 24 & 0xFF000000) |\r
489                                         (_data[offset + 2] << 16 & 0xFF0000) |\r
490                                         (_data[offset + 1] << 8 & 0xFF00) |\r
491                                         (_data[offset] & 0xFF);\r
492                 }\r
493         }\r
494 }\r