2 * Copyright 2002-2015 Drew Noakes
4 * More information about this project is available at:
6 * https://drewnoakes.com/code/exif/
7 * https://github.com/drewnoakes/metadata-extractor
9 package tim.prune.jpeg.drew;
12 import java.io.IOException;
13 import java.util.HashSet;
16 import tim.prune.jpeg.JpegData;
19 * Processes TIFF-formatted data, using an ExifTiffHandler
21 * @author Drew Noakes https://drewnoakes.com
23 public class TiffProcessor
26 * Processes a TIFF data sequence.
28 * @param reader the {@link RandomAccessReader} from which the data should be read
29 * @param jpegData the data to populate
30 * @param tiffHeaderOffset the offset within <code>reader</code> at which the TIFF header starts
31 * @throws ExifException if an error occurred during the processing of TIFF data that could
32 * not be ignored or recovered from
33 * @throws IOException an error occurred while accessing the required data
35 public static void processTiff(final ByteArrayReader reader, JpegData jpegData,
36 final int tiffHeaderOffset) throws ExifException, IOException
38 // This must be either "MM" or "II".
39 short byteOrderIdentifier = reader.getInt16(tiffHeaderOffset);
41 if (byteOrderIdentifier == 0x4d4d) { // "MM"
42 reader.setMotorolaByteOrder(true);
43 } else if (byteOrderIdentifier == 0x4949) { // "II"
44 reader.setMotorolaByteOrder(false);
46 throw new ExifException("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier);
50 int firstIfdOffset = reader.getInt32(4 + tiffHeaderOffset) + tiffHeaderOffset;
52 // David Ekholm sent a digital camera image that has this problem
53 if (firstIfdOffset >= reader.getLength() - 1) {
54 //handler.warn("First IFD offset is beyond the end of the TIFF data segment -- trying default offset");
55 // First directory normally starts immediately after the offset bytes, so try that
56 firstIfdOffset = tiffHeaderOffset + 2 + 2 + 4;
59 // Make a handler object to use for the processing
60 ExifTiffHandler handler = new ExifTiffHandler(jpegData);
62 Set<Integer> processedIfdOffsets = new HashSet<Integer>();
63 processDirectory(handler, reader, processedIfdOffsets, firstIfdOffset, tiffHeaderOffset, 0);
65 handler.completed(reader, tiffHeaderOffset);
69 * Processes a TIFF IFD.
73 * <li><b>2 bytes</b> number of tags</li>
77 * <li><b>2 bytes</b> tag type</li>
78 * <li><b>2 bytes</b> format code (values 1 to 12, inclusive)</li>
79 * <li><b>4 bytes</b> component count</li>
80 * <li><b>4 bytes</b> inline value, or offset pointer if too large to fit in four bytes</li>
83 * @param handler the {@link ExifTiffHandler} that will coordinate processing and accept read values
84 * @param reader the byte reader from which the data should be read
85 * @param processedIfdOffsets the set of visited IFD offsets, to avoid revisiting the same IFD in an endless loop
86 * @param ifdOffset the offset within <code>reader</code> at which the IFD data starts
87 * @param tiffHeaderOffset the offset within <code>reader</code> at which the TIFF header starts
88 * @param inDirectoryId directory id
89 * @throws IOException an error occurred while accessing the required data
91 private static void processDirectory(final ExifTiffHandler handler,
92 final ByteArrayReader reader, final Set<Integer> processedIfdOffsets,
93 final int ifdOffset, final int tiffHeaderOffset, int inDirectoryId) throws ExifException
95 // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist
96 if (processedIfdOffsets.contains(Integer.valueOf(ifdOffset))) {
100 // remember that we've visited this directory so that we don't visit it again later
101 processedIfdOffsets.add(ifdOffset);
103 if (ifdOffset >= reader.getLength() || ifdOffset < 0) {
104 //handler.error("Ignored IFD marked to start outside data segment");
108 // First two bytes in the IFD are the number of tags in this directory
109 int dirTagCount = reader.getUInt16(ifdOffset);
111 int dirLength = (2 + (12 * dirTagCount) + 4);
112 if (dirLength + ifdOffset > reader.getLength()) {
113 //handler.error("Illegally sized IFD");
118 // Handle each tag in this directory
120 int invalidTiffFormatCodeCount = 0;
121 for (int tagNumber = 0; tagNumber < dirTagCount; tagNumber++)
123 final int tagOffset = calculateTagOffset(ifdOffset, tagNumber);
125 // 2 bytes for the tag id
126 final int childTagId = reader.getUInt16(tagOffset);
128 // 2 bytes for the format code
129 final int formatCode = reader.getUInt16(tagOffset + 2);
130 final int componentSizeInBytes = TiffDataFormat.getComponentSize(formatCode);
132 if (componentSizeInBytes == 0)
134 // This error suggests that we are processing at an incorrect index and will generate
135 // rubbish until we go out of bounds (which may be a while).
136 if (++invalidTiffFormatCodeCount > 5) {
137 //handler.error("Stopping processing as too many errors seen in TIFF IFD");
143 // 4 bytes dictate the number of components in this tag's data
144 final int componentCount = reader.getInt32(tagOffset + 4);
145 if (componentCount < 0) {
146 //handler.error("Negative TIFF tag component count");
150 final int byteCount = componentCount * componentSizeInBytes;
152 final int tagValueOffset;
155 // If it's bigger than 4 bytes, the dir entry contains an offset.
156 final int offsetVal = reader.getInt32(tagOffset + 8);
157 if (offsetVal + byteCount > reader.getLength()) {
158 // Bogus pointer offset and / or byteCount value
159 //handler.error("Illegal TIFF tag pointer offset");
162 tagValueOffset = tiffHeaderOffset + offsetVal;
165 // 4 bytes or less and value is in the dir entry itself.
166 tagValueOffset = tagOffset + 8;
169 if (tagValueOffset < 0 || tagValueOffset > reader.getLength()) {
170 //handler.error("Illegal TIFF tag pointer offset");
174 // Check that this tag isn't going to allocate outside the bounds of the data array.
175 // This addresses an uncommon OutOfMemoryError.
176 if (byteCount < 0 || tagValueOffset + byteCount > reader.getLength()) {
177 //handler.error("Illegal number of bytes for TIFF tag data: " + byteCount);
181 // Special handling for tags that point to other IFDs
182 if (byteCount == 4 && handler.isTagIfdPointer(childTagId)) {
183 final int subDirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
184 processDirectory(handler, reader, processedIfdOffsets, subDirOffset, tiffHeaderOffset, childTagId);
186 else if (handler.isInterestingTag(inDirectoryId, childTagId))
188 processTag(handler, childTagId, tagValueOffset, componentCount, formatCode, reader);
192 // at the end of each IFD is an optional link to the next IFD
193 final int finalTagOffset = calculateTagOffset(ifdOffset, dirTagCount);
194 int nextIfdOffset = reader.getInt32(finalTagOffset);
195 if (nextIfdOffset != 0)
197 nextIfdOffset += tiffHeaderOffset;
198 if (nextIfdOffset >= reader.getLength()) {
199 // Last 4 bytes of IFD reference another IFD with an address that is out of bounds
200 // Note this could have been caused by jhead 1.3 cropping too much
203 else if (nextIfdOffset < ifdOffset) {
204 // Last 4 bytes of IFD reference another IFD with an address that is before the start of this directory
208 processDirectory(handler, reader, processedIfdOffsets, nextIfdOffset, tiffHeaderOffset, inDirectoryId);
214 * Process a single tag value
216 private static void processTag(final ExifTiffHandler handler,
217 final int tagId, final int tagValueOffset,
218 final int componentCount, final int formatCode,
219 final ByteArrayReader reader) throws ExifException
223 case TiffDataFormat.CODE_STRING:
224 handler.setString(tagId, reader.getNullTerminatedString(tagValueOffset, componentCount));
226 case TiffDataFormat.CODE_RATIONAL_S:
227 if (componentCount == 1) {
228 handler.setRational(tagId, new Rational(reader.getInt32(tagValueOffset), reader.getInt32(tagValueOffset + 4)));
229 } else if (componentCount > 1) {
230 Rational[] array = new Rational[componentCount];
231 for (int i = 0; i < componentCount; i++)
232 array[i] = new Rational(reader.getInt32(tagValueOffset + (8 * i)), reader.getInt32(tagValueOffset + 4 + (8 * i)));
233 handler.setRationalArray(tagId, array);
236 case TiffDataFormat.CODE_RATIONAL_U:
237 if (componentCount == 1) {
238 handler.setRational(tagId, new Rational(reader.getUInt32(tagValueOffset), reader.getUInt32(tagValueOffset + 4)));
239 } else if (componentCount > 1) {
240 Rational[] array = new Rational[componentCount];
241 for (int i = 0; i < componentCount; i++)
242 array[i] = new Rational(reader.getUInt32(tagValueOffset + (8 * i)), reader.getUInt32(tagValueOffset + 4 + (8 * i)));
243 handler.setRationalArray(tagId, array);
246 case TiffDataFormat.CODE_INT8_S:
247 if (componentCount == 1) {
248 handler.setIntegerValue(tagId, reader.getInt8(tagValueOffset));
251 case TiffDataFormat.CODE_INT8_U:
252 if (componentCount == 1) {
253 handler.setIntegerValue(tagId, reader.getUInt8(tagValueOffset));
256 case TiffDataFormat.CODE_INT16_S:
257 if (componentCount == 1) {
258 handler.setIntegerValue(tagId, reader.getInt16(tagValueOffset));
261 case TiffDataFormat.CODE_INT16_U:
262 if (componentCount == 1) {
263 handler.setIntegerValue(tagId, reader.getUInt16(tagValueOffset));
266 case TiffDataFormat.CODE_INT32_S:
267 // NOTE 'long' in this case means 32 bit, not 64
268 if (componentCount == 1) {
269 handler.setIntegerValue(tagId, reader.getInt32(tagValueOffset));
272 case TiffDataFormat.CODE_INT32_U:
273 // NOTE 'long' in this case means 32 bit, not 64
274 if (componentCount == 1) {
275 handler.setRational(tagId, new Rational(reader.getUInt32(tagValueOffset), 1L));
278 case TiffDataFormat.CODE_SINGLE:
279 case TiffDataFormat.CODE_DOUBLE:
280 case TiffDataFormat.CODE_UNDEFINED:
287 * Determine the offset of a given tag within the specified IFD.
289 * @param ifdStartOffset the offset at which the IFD starts
290 * @param entryNumber the zero-based entry number
292 private static int calculateTagOffset(int ifdStartOffset, int entryNumber)
294 // Add 2 bytes for the tag count.
295 // Each entry is 12 bytes.
296 return ifdStartOffset + 2 + (12 * entryNumber);