--- /dev/null
+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);
+ }
+ }
+}