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); } } }