]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/jpeg/drew/ExifReader.java
Version 19, May 2018
[GpsPrune.git] / tim / prune / jpeg / drew / ExifReader.java
1 package tim.prune.jpeg.drew;
2
3 import java.io.BufferedInputStream;
4 import java.io.File;
5 import java.io.FileInputStream;
6 import java.io.IOException;
7
8 import tim.prune.jpeg.JpegData;
9
10
11 /**
12  * Extracts Exif data from a JPEG header segment
13  * Based on Drew Noakes' Metadata extractor at https://drewnoakes.com
14  * which in turn is based on code from Jhead http://www.sentex.net/~mwandel/jhead/
15  */
16 public class ExifReader
17 {
18         /** Magic numbers to mark the beginning of all Jpegs */
19         private static final int MAGIC_JPEG_BYTE_1 = 0xFF;
20         private static final int MAGIC_JPEG_BYTE_2 = 0xD8;
21
22         /** 6-byte preamble before starting the TIFF data. */
23         private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0";
24
25         /** Start of segment marker */
26         private static final byte SEGMENT_SOS = (byte) 0xDA;
27
28         /** End of segment marker */
29         private static final byte MARKER_EOI = (byte) 0xD9;
30
31         /**
32          * Processes the provided JPEG data, and extracts the specified JPEG segments into a JpegData object.
33          * @param inFile a {@link File} from which the JPEG data will be read.
34          */
35         public static JpegData readMetadata(File inFile) throws ExifException
36         {
37                 JpegData jpegData = new JpegData();
38                 BufferedInputStream bStream = null;
39
40                 try
41                 {
42                         bStream = new BufferedInputStream(new FileInputStream(inFile));
43                         byte[] segmentBytes = readSegments(bStream);
44                         if (segmentBytes != null)
45                         {
46                                 // Got the bytes for the required segment, now extract the data
47                                 extract(segmentBytes, jpegData);
48                         }
49                 }
50                 catch (IOException ioe) {
51                         throw new ExifException("IO Exception: " + ioe.getMessage());
52                 }
53                 finally
54                 {
55                         if (bStream != null) {
56                                 try {
57                                         bStream.close();
58                                 } catch (IOException ioe) {}
59                         }
60                 }
61                 return jpegData;
62         }
63
64         /**
65          * Reads the relevant segment and returns the bytes.
66          */
67         private static byte[] readSegments(final BufferedInputStream bStream)
68                 throws ExifException, IOException
69         {
70                 // first two bytes should be JPEG magic number
71                 final int magic1 = bStream.read() & 0xFF;
72                 final int magic2 = bStream.read() & 0xFF;
73                 if (magic1 != MAGIC_JPEG_BYTE_1 || magic2 != MAGIC_JPEG_BYTE_2) {
74                         throw new ExifException("Jpeg file failed Magic check");
75                 }
76
77                 final Byte segmentTypeByte = (byte)0xE1; // JpegSegmentType.APP1.byteValue;
78
79                 do {
80                         // Find the segment marker. Markers are zero or more 0xFF bytes, followed
81                         // by a 0xFF and then a byte not equal to 0x00 or 0xFF.
82
83                         final short segmentIdentifier = (short) bStream.read();
84
85                         // We must have at least one 0xFF byte
86                         if (segmentIdentifier != 0xFF)
87                                 throw new ExifException("Expected JPEG segment start identifier 0xFF, not 0x" + Integer.toHexString(segmentIdentifier).toUpperCase());
88
89                         // Read until we have a non-0xFF byte. This identifies the segment type.
90                         byte currSegmentType = (byte) bStream.read();
91                         while (currSegmentType == (byte)0xFF) {
92                                 currSegmentType = (byte) bStream.read();
93                         }
94
95                         if (currSegmentType == 0)
96                                 throw new ExifException("Expected non-zero byte as part of JPEG marker identifier");
97
98                         if (currSegmentType == SEGMENT_SOS) {
99                                 // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
100                                 // have to search for the two bytes: 0xFF 0xD9 (EOI).
101                                 // It comes last so simply return at this point
102                                 return null;
103                         }
104
105                         if (currSegmentType == MARKER_EOI) {
106                                 // the 'End-Of-Image' segment -- this should never be found in this fashion
107                                 return null;
108                         }
109
110                         // next 2-bytes are <segment-size>: [high-byte] [low-byte]
111                         int segmentLength = (bStream.read() << 8) + bStream.read();
112                         // segment length includes size bytes, so subtract two
113                         segmentLength -= 2;
114
115                         if (segmentLength < 0)
116                                 throw new ExifException("JPEG segment size would be less than zero");
117
118                         byte[] segmentBytes = new byte[segmentLength];
119                         int bytesRead = bStream.read(segmentBytes, 0, segmentLength);
120                         // Bail if not all bytes read in one go - otherwise following sections will be out of step
121                         if (bytesRead != segmentLength) {
122                                 throw new ExifException("Tried to read " + segmentLength + " bytes but only got " + bytesRead);
123                         }
124                         // Check whether we are interested in this segment
125                         if (segmentTypeByte == currSegmentType)
126                         {
127                                 // Pass the appropriate byte arrays to reader.
128                                 if (canProcess(segmentBytes)) {
129                                         return segmentBytes;
130                                 }
131                         }
132
133                 } while (true);
134         }
135
136         private static boolean canProcess(final byte[] segmentBytes)
137         {
138                 return segmentBytes.length >= JPEG_EXIF_SEGMENT_PREAMBLE.length() && new String(segmentBytes, 0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equalsIgnoreCase(JPEG_EXIF_SEGMENT_PREAMBLE);
139         }
140
141         /**
142          * Given the bytes, parse them recursively to fill the JpegData
143          * @param segmentBytes bytes out of the file
144          * @param jdata jpeg data to be populated
145          */
146         private static void extract(final byte[] segmentBytes, final JpegData jdata)
147         {
148                 if (segmentBytes == null)
149                         throw new NullPointerException("segmentBytes cannot be null");
150
151                 try
152                 {
153                         ByteArrayReader reader = new ByteArrayReader(segmentBytes);
154
155                         // Check for the header preamble
156                         try {
157                                 if (!reader.getString(0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equals(JPEG_EXIF_SEGMENT_PREAMBLE)) {
158                                         // TODO what to do with this error state?
159                                         System.err.println("Invalid JPEG Exif segment preamble");
160                                         return;
161                                 }
162                         } catch (ExifException e) {
163                                 // TODO what to do with this error state?
164                                 e.printStackTrace(System.err);
165                                 return;
166                         }
167
168                         // Read the TIFF-formatted Exif data
169                         TiffProcessor.processTiff(
170                                 reader,
171                                 jdata,
172                                 JPEG_EXIF_SEGMENT_PREAMBLE.length()
173                         );
174
175                 } catch (ExifException e) {
176                         // TODO what to do with this error state?
177                         e.printStackTrace(System.err);
178                 } catch (IOException e) {
179                         // TODO what to do with this error state?
180                         e.printStackTrace(System.err);
181                 }
182         }
183 }