X-Git-Url: http://gitweb.fperrin.net/?p=GpsPrune.git;a=blobdiff_plain;f=src%2Ftim%2Fprune%2Fjpeg%2Fdrew%2FExifReader.java;fp=src%2Ftim%2Fprune%2Fjpeg%2Fdrew%2FExifReader.java;h=c8c1b7c41bf4c0f8a2df9f240183226df1755a23;hp=0000000000000000000000000000000000000000;hb=ce6f2161b8596f7018d6a76bff79bc9e571f35fd;hpb=2d8cb72e84d5cc1089ce77baf1e34ea3ea2f8465 diff --git a/src/tim/prune/jpeg/drew/ExifReader.java b/src/tim/prune/jpeg/drew/ExifReader.java new file mode 100644 index 0000000..c8c1b7c --- /dev/null +++ b/src/tim/prune/jpeg/drew/ExifReader.java @@ -0,0 +1,183 @@ +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 : [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); + } + } +}