/* * Copyright 2002-2012 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * More information about this project is available at: * * http://drewnoakes.com/code/exif/ * http://code.google.com/p/metadata-extractor/ */ package com.almalence.util.exifreader.metadata.exif; import com.almalence.util.exifreader.lang.BufferBoundsException; import com.almalence.util.exifreader.lang.BufferReader; import com.almalence.util.exifreader.lang.Rational; import com.almalence.util.exifreader.lang.annotations.NotNull; import com.almalence.util.exifreader.metadata.Directory; import com.almalence.util.exifreader.metadata.Metadata; import com.almalence.util.exifreader.metadata.MetadataReader; import java.util.HashSet; import java.util.Set; /** * Decodes Exif binary data, populating a {@link Metadata} object with tag values in {@link ExifSubIFDDirectory}, * {@link ExifThumbnailDirectory}, {@link ExifInteropDirectory}, {@link GpsDirectory} and one of the many camera makernote directories. * * @author Drew Noakes http://drewnoakes.com */ public class ExifReader implements MetadataReader { // extract a reusable TiffReader from this class with hooks for special tag handling and subdir following /** The number of bytes used per format descriptor. */ @NotNull private static final int[] BYTES_PER_FORMAT = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 }; /** The number of formats known. */ private static final int MAX_FORMAT_CODE = 12; // Format types // use an enum for these? /** An 8-bit unsigned integer. */ private static final int FMT_BYTE = 1; /** A fixed-length character string. */ private static final int FMT_STRING = 2; /** An unsigned 16-bit integer. */ private static final int FMT_USHORT = 3; /** An unsigned 32-bit integer. */ private static final int FMT_ULONG = 4; private static final int FMT_URATIONAL = 5; /** An 8-bit signed integer. */ private static final int FMT_SBYTE = 6; private static final int FMT_UNDEFINED = 7; /** A signed 16-bit integer. */ private static final int FMT_SSHORT = 8; /** A signed 32-bit integer. */ private static final int FMT_SLONG = 9; private static final int FMT_SRATIONAL = 10; /** A 32-bit floating point number. */ private static final int FMT_SINGLE = 11; /** A 64-bit floating point number. */ private static final int FMT_DOUBLE = 12; /** This tag is a pointer to the Exif SubIFD. */ public static final int TAG_EXIF_SUB_IFD_OFFSET = 0x8769; /** This tag is a pointer to the Exif Interop IFD. */ public static final int TAG_INTEROP_OFFSET = 0xA005; /** This tag is a pointer to the Exif GPS IFD. */ public static final int TAG_GPS_INFO_OFFSET = 0x8825; /** This tag is a pointer to the Exif Makernote IFD. */ public static final int TAG_MAKER_NOTE_OFFSET = 0x927C; public static final int TIFF_HEADER_START_OFFSET = 6; /** * Performs the Exif data extraction, adding found values to the specified * instance of <code>Metadata</code>. * * @param reader The buffer reader from which Exif data should be read. * @param metadata The Metadata object into which extracted values should be merged. */ public void extract(@NotNull final BufferReader reader, @NotNull Metadata metadata) { final ExifSubIFDDirectory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class); // check for the header length if (reader.getLength() <= 14) { directory.addError("Exif data segment must contain at least 14 bytes"); return; } // check for the header preamble try { if (!reader.getString(0, 6).equals("Exif\0\0")) { directory.addError("Exif data segment doesn't begin with 'Exif'"); return; } extractIFD(metadata, metadata.getOrCreateDirectory(ExifIFD0Directory.class), TIFF_HEADER_START_OFFSET, reader); } catch (BufferBoundsException e) { directory.addError("Exif data segment ended prematurely"); } } /** * Performs the Exif data extraction on a TIFF/RAW, adding found values to the specified * instance of <code>Metadata</code>. * * @param reader The BufferReader from which TIFF data should be read. * @param metadata The Metadata object into which extracted values should be merged. */ public void extractTiff(@NotNull BufferReader reader, @NotNull Metadata metadata) { final ExifIFD0Directory directory = metadata.getOrCreateDirectory(ExifIFD0Directory.class); try { extractIFD(metadata, directory, 0, reader); } catch (BufferBoundsException e) { directory.addError("Exif data segment ended prematurely"); } } private void extractIFD(@NotNull Metadata metadata, @NotNull final ExifIFD0Directory directory, int tiffHeaderOffset, @NotNull BufferReader reader) throws BufferBoundsException { // this should be either "MM" or "II" String byteOrderIdentifier = reader.getString(tiffHeaderOffset, 2); if ("MM".equals(byteOrderIdentifier)) { reader.setMotorolaByteOrder(true); } else if ("II".equals(byteOrderIdentifier)) { reader.setMotorolaByteOrder(false); } else { directory.addError("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier); return; } // Check the next two values for correctness. final int tiffMarker = reader.getUInt16(2 + tiffHeaderOffset); final int standardTiffMarker = 0x002A; final int olympusRawTiffMarker = 0x4F52; // for ORF files final int panasonicRawTiffMarker = 0x0055; // for RW2 files if (tiffMarker != standardTiffMarker && tiffMarker != olympusRawTiffMarker && tiffMarker != panasonicRawTiffMarker) { directory.addError("Unexpected TIFF marker after byte order identifier: 0x" + Integer.toHexString(tiffMarker)); return; } int firstDirectoryOffset = reader.getInt32(4 + tiffHeaderOffset) + tiffHeaderOffset; // David Ekholm sent a digital camera image that has this problem if (firstDirectoryOffset >= reader.getLength() - 1) { directory.addError("First exif directory offset is beyond end of Exif data segment"); // First directory normally starts 14 bytes in -- try it here and catch another error in the worst case firstDirectoryOffset = 14; } Set<Integer> processedDirectoryOffsets = new HashSet<Integer>(); processDirectory(directory, processedDirectoryOffsets, firstDirectoryOffset, tiffHeaderOffset, metadata, reader); } /** * Process one of the nested Tiff IFD directories. * <p/> * Header * 2 bytes: number of tags * <p/> * Then for each tag * 2 bytes: tag type * 2 bytes: format code * 4 bytes: component count */ private void processDirectory(@NotNull Directory directory, @NotNull Set<Integer> processedDirectoryOffsets, int dirStartOffset, int tiffHeaderOffset, @NotNull final Metadata metadata, @NotNull final BufferReader reader) throws BufferBoundsException { // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist if (processedDirectoryOffsets.contains(Integer.valueOf(dirStartOffset))) return; // remember that we've visited this directory so that we don't visit it again later processedDirectoryOffsets.add(dirStartOffset); if (dirStartOffset >= reader.getLength() || dirStartOffset < 0) { directory.addError("Ignored directory marked to start outside data segment"); return; } // First two bytes in the IFD are the number of tags in this directory int dirTagCount = reader.getUInt16(dirStartOffset); int dirLength = (2 + (12 * dirTagCount) + 4); if (dirLength + dirStartOffset > reader.getLength()) { directory.addError("Illegally sized directory"); return; } // Handle each tag in this directory for (int tagNumber = 0; tagNumber < dirTagCount; tagNumber++) { final int tagOffset = calculateTagOffset(dirStartOffset, tagNumber); // 2 bytes for the tag type final int tagType = reader.getUInt16(tagOffset); // 2 bytes for the format code final int formatCode = reader.getUInt16(tagOffset + 2); if (formatCode < 1 || formatCode > MAX_FORMAT_CODE) { // This error suggests that we are processing at an incorrect index and will generate // rubbish until we go out of bounds (which may be a while). Exit now. directory.addError("Invalid TIFF tag format code: " + formatCode); return; } // 4 bytes dictate the number of components in this tag's data final int componentCount = reader.getInt32(tagOffset + 4); if (componentCount < 0) { directory.addError("Negative TIFF tag component count"); continue; } // each component may have more than one byte... calculate the total number of bytes final int byteCount = componentCount * BYTES_PER_FORMAT[formatCode]; final int tagValueOffset; if (byteCount > 4) { // If it's bigger than 4 bytes, the dir entry contains an offset. // dirEntryOffset must be passed, as some makernote implementations (e.g. FujiFilm) incorrectly use an // offset relative to the start of the makernote itself, not the TIFF segment. final int offsetVal = reader.getInt32(tagOffset + 8); if (offsetVal + byteCount > reader.getLength()) { // Bogus pointer offset and / or byteCount value directory.addError("Illegal TIFF tag pointer offset"); continue; } tagValueOffset = tiffHeaderOffset + offsetVal; } else { // 4 bytes or less and value is in the dir entry itself tagValueOffset = tagOffset + 8; } if (tagValueOffset < 0 || tagValueOffset > reader.getLength()) { directory.addError("Illegal TIFF tag pointer offset"); continue; } // Check that this tag isn't going to allocate outside the bounds of the data array. // This addresses an uncommon OutOfMemoryError. if (byteCount < 0 || tagValueOffset + byteCount > reader.getLength()) { directory.addError("Illegal number of bytes: " + byteCount); continue; } switch (tagType) { case TAG_EXIF_SUB_IFD_OFFSET: { final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset); processDirectory(metadata.getOrCreateDirectory(ExifSubIFDDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader); continue; } case TAG_MAKER_NOTE_OFFSET: { processMakerNote(tagValueOffset, processedDirectoryOffsets, tiffHeaderOffset, metadata, reader); continue; } default: { processTag(directory, tagType, tagValueOffset, componentCount, formatCode, reader); break; } } } // at the end of each IFD is an optional link to the next IFD final int finalTagOffset = calculateTagOffset(dirStartOffset, dirTagCount); int nextDirectoryOffset = reader.getInt32(finalTagOffset); if (nextDirectoryOffset != 0) { nextDirectoryOffset += tiffHeaderOffset; if (nextDirectoryOffset >= reader.getLength()) { // Last 4 bytes of IFD reference another IFD with an address that is out of bounds // Note this could have been caused by jhead 1.3 cropping too much return; } else if (nextDirectoryOffset < dirStartOffset) { // Last 4 bytes of IFD reference another IFD with an address that is before the start of this directory return; } } } private void processMakerNote(int subdirOffset, @NotNull Set<Integer> processedDirectoryOffsets, int tiffHeaderOffset, @NotNull final Metadata metadata, @NotNull BufferReader reader) throws BufferBoundsException { // Determine the camera model and makernote format Directory ifd0Directory = metadata.getDirectory(ExifIFD0Directory.class); if (ifd0Directory==null) return; } private void processTag(@NotNull Directory directory, int tagType, int tagValueOffset, int componentCount, int formatCode, @NotNull final BufferReader reader) throws BufferBoundsException { // Directory simply stores raw values // The display side uses a Descriptor class per directory to turn the raw values into 'pretty' descriptions switch (formatCode) { case FMT_UNDEFINED: // this includes exif user comments directory.setByteArray(tagType, reader.getBytes(tagValueOffset, componentCount)); break; case FMT_STRING: String string = reader.getNullTerminatedString(tagValueOffset, componentCount); directory.setString(tagType, string); break; case FMT_SRATIONAL: if (componentCount == 1) { directory.setRational(tagType, new Rational(reader.getInt32(tagValueOffset), reader.getInt32(tagValueOffset + 4))); } else if (componentCount > 1) { Rational[] rationals = new Rational[componentCount]; for (int i = 0; i < componentCount; i++) rationals[i] = new Rational(reader.getInt32(tagValueOffset + (8 * i)), reader.getInt32(tagValueOffset + 4 + (8 * i))); directory.setRationalArray(tagType, rationals); } break; case FMT_URATIONAL: if (componentCount == 1) { directory.setRational(tagType, new Rational(reader.getUInt32(tagValueOffset), reader.getUInt32(tagValueOffset + 4))); } else if (componentCount > 1) { Rational[] rationals = new Rational[componentCount]; for (int i = 0; i < componentCount; i++) rationals[i] = new Rational(reader.getUInt32(tagValueOffset + (8 * i)), reader.getUInt32(tagValueOffset + 4 + (8 * i))); directory.setRationalArray(tagType, rationals); } break; case FMT_SINGLE: if (componentCount == 1) { directory.setFloat(tagType, reader.getFloat32(tagValueOffset)); } else { float[] floats = new float[componentCount]; for (int i = 0; i < componentCount; i++) floats[i] = reader.getFloat32(tagValueOffset + (i * 4)); directory.setFloatArray(tagType, floats); } break; case FMT_DOUBLE: if (componentCount == 1) { directory.setDouble(tagType, reader.getDouble64(tagValueOffset)); } else { double[] doubles = new double[componentCount]; for (int i = 0; i < componentCount; i++) doubles[i] = reader.getDouble64(tagValueOffset + (i * 4)); directory.setDoubleArray(tagType, doubles); } break; // // Note that all integral types are stored as int32 internally (the largest supported by TIFF) // case FMT_SBYTE: if (componentCount == 1) { directory.setInt(tagType, reader.getInt8(tagValueOffset)); } else { int[] bytes = new int[componentCount]; for (int i = 0; i < componentCount; i++) bytes[i] = reader.getInt8(tagValueOffset + i); directory.setIntArray(tagType, bytes); } break; case FMT_BYTE: if (componentCount == 1) { directory.setInt(tagType, reader.getUInt8(tagValueOffset)); } else { int[] bytes = new int[componentCount]; for (int i = 0; i < componentCount; i++) bytes[i] = reader.getUInt8(tagValueOffset + i); directory.setIntArray(tagType, bytes); } break; case FMT_USHORT: if (componentCount == 1) { int i = reader.getUInt16(tagValueOffset); directory.setInt(tagType, i); } else { int[] ints = new int[componentCount]; for (int i = 0; i < componentCount; i++) ints[i] = reader.getUInt16(tagValueOffset + (i * 2)); directory.setIntArray(tagType, ints); } break; case FMT_SSHORT: if (componentCount == 1) { int i = reader.getInt16(tagValueOffset); directory.setInt(tagType, i); } else { int[] ints = new int[componentCount]; for (int i = 0; i < componentCount; i++) ints[i] = reader.getInt16(tagValueOffset + (i * 2)); directory.setIntArray(tagType, ints); } break; case FMT_SLONG: case FMT_ULONG: // NOTE 'long' in this case means 32 bit, not 64 if (componentCount == 1) { int i = reader.getInt32(tagValueOffset); directory.setInt(tagType, i); } else { int[] ints = new int[componentCount]; for (int i = 0; i < componentCount; i++) ints[i] = reader.getInt32(tagValueOffset + (i * 4)); directory.setIntArray(tagType, ints); } break; default: directory.addError("Unknown format code " + formatCode + " for tag " + tagType); } } /** * Determine the offset at which a given InteropArray entry begins within the specified IFD. * * @param dirStartOffset the offset at which the IFD starts * @param entryNumber the zero-based entry number */ private int calculateTagOffset(int dirStartOffset, int entryNumber) { // add 2 bytes for the tag count // each entry is 12 bytes, so we skip 12 * the number seen so far return dirStartOffset + 2 + (12 * entryNumber); } }