View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.tiff;
18  
19  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.ENTRY_MAX_VALUE_LENGTH;
20  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.ENTRY_MAX_VALUE_LENGTH_BIG;
21  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.VERSION_BIG;
22  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.VERSION_STANDARD;
23  
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.nio.ByteOrder;
27  import java.util.ArrayList;
28  import java.util.List;
29  
30  import org.apache.commons.imaging.FormatCompliance;
31  import org.apache.commons.imaging.ImagingException;
32  import org.apache.commons.imaging.bytesource.ByteSource;
33  import org.apache.commons.imaging.common.BinaryFileParser;
34  import org.apache.commons.imaging.common.BinaryFunctions;
35  import org.apache.commons.imaging.common.ByteConversions;
36  import org.apache.commons.imaging.formats.jpeg.JpegConstants;
37  import org.apache.commons.imaging.formats.tiff.TiffDirectory.ImageDataElement;
38  import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
39  import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants;
40  import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
41  import org.apache.commons.imaging.formats.tiff.fieldtypes.AbstractFieldType;
42  import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDirectory;
43  
44  public class TiffReader extends BinaryFileParser {
45  
46      private static class Collector implements Listener {
47  
48          private TiffHeader tiffHeader;
49          private final List<TiffDirectory> directories = new ArrayList<>();
50          private final List<TiffField> fields = new ArrayList<>();
51          private final boolean readThumbnails;
52  
53          Collector() {
54              this(new TiffImagingParameters());
55          }
56  
57          Collector(final TiffImagingParameters params) {
58              this.readThumbnails = params.isReadThumbnails();
59          }
60  
61          @Override
62          public boolean addDirectory(final TiffDirectory directory) {
63              directories.add(directory);
64              return true;
65          }
66  
67          @Override
68          public boolean addField(final TiffField field) {
69              fields.add(field);
70              return true;
71          }
72  
73          public TiffContents getContents() {
74              return new TiffContents(tiffHeader, directories, fields);
75          }
76  
77          @Override
78          public boolean readImageData() {
79              return readThumbnails;
80          }
81  
82          @Override
83          public boolean readOffsetDirectories() {
84              return true;
85          }
86  
87          @Override
88          public boolean setTiffHeader(final TiffHeader tiffHeader) {
89              this.tiffHeader = tiffHeader;
90              return true;
91          }
92      }
93  
94      private static final class FirstDirectoryCollector extends Collector {
95          private final boolean readImageData;
96  
97          FirstDirectoryCollector(final boolean readImageData) {
98              this.readImageData = readImageData;
99          }
100 
101         @Override
102         public boolean addDirectory(final TiffDirectory directory) {
103             super.addDirectory(directory);
104             return false;
105         }
106 
107         @Override
108         public boolean readImageData() {
109             return readImageData;
110         }
111     }
112 
113     public interface Listener {
114         boolean addDirectory(TiffDirectory directory);
115 
116         boolean addField(TiffField field);
117 
118         boolean readImageData();
119 
120         boolean readOffsetDirectories();
121 
122         boolean setTiffHeader(TiffHeader tiffHeader);
123     }
124 
125     private final boolean strict;
126     private boolean bigTiff;
127     private boolean standardTiff;
128     private int entryMaxValueLength;
129 
130     public TiffReader(final boolean strict) {
131         this.strict = strict;
132     }
133 
134     private JpegImageData getJpegRawImageData(final ByteSource byteSource, final TiffDirectory directory) throws ImagingException, IOException {
135         final ImageDataElement element = directory.getJpegRawImageDataElement();
136         final long offset = element.offset;
137         int length = element.length;
138         // In case the length is not correct, adjust it and check if the last read byte actually is the end of the image
139         if (offset + length > byteSource.size()) {
140             length = (int) (byteSource.size() - offset);
141         }
142         final byte[] data = byteSource.getByteArray(offset, length);
143         // check if the last read byte is actually the end of the image data
144         if (strict && (length < 2 || ((data[data.length - 2] & 0xff) << 8 | data[data.length - 1] & 0xff) != JpegConstants.EOI_MARKER)) {
145             throw new ImagingException("JPEG EOI marker could not be found at expected location");
146         }
147         return new JpegImageData(offset, length, data);
148     }
149 
150     private ByteOrder getTiffByteOrder(final int byteOrderByte) throws ImagingException {
151         if (byteOrderByte == 'I') {
152             return ByteOrder.LITTLE_ENDIAN; // Intel
153         }
154         if (byteOrderByte == 'M') {
155             return ByteOrder.BIG_ENDIAN; // Motorola
156         }
157         throw new ImagingException("Invalid TIFF byte order " + (0xff & byteOrderByte));
158     }
159 
160     private AbstractTiffImageData getTiffRawImageData(final ByteSource byteSource, final TiffDirectory directory) throws ImagingException, IOException {
161 
162         final List<ImageDataElement> elements = directory.getTiffRawImageDataElements();
163         final AbstractTiffImageData.Data[] data = new AbstractTiffImageData.Data[elements.size()];
164 
165         for (int i = 0; i < elements.size(); i++) {
166             final TiffDirectory.ImageDataElement element = elements.get(i);
167             final byte[] bytes = byteSource.getByteArray(element.offset, element.length);
168             data[i] = new AbstractTiffImageData.Data(element.offset, element.length, bytes);
169         }
170 
171         if (directory.imageDataInStrips()) {
172             final TiffField rowsPerStripField = directory.findField(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP);
173             //
174             // Default value of rowsPerStripField is assumed to be infinity
175             // https://www.awaresystems.be/imaging/tiff/tifftags/rowsperstrip.html
176             //
177             int rowsPerStrip = Integer.MAX_VALUE;
178 
179             if (null != rowsPerStripField) {
180                 rowsPerStrip = rowsPerStripField.getIntValue();
181             } else {
182                 final TiffField imageHeight = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH);
183                 //
184                 // if rows per strip not present then rowsPerStrip is equal to
185                 // imageLength or an infinity value;
186                 //
187                 if (imageHeight != null) {
188                     rowsPerStrip = imageHeight.getIntValue();
189                 }
190 
191             }
192 
193             return new AbstractTiffImageData.Strips(data, rowsPerStrip);
194         }
195         final TiffField tileWidthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_WIDTH);
196         if (null == tileWidthField) {
197             throw new ImagingException("Can't find tile width field.");
198         }
199         final int tileWidth = tileWidthField.getIntValue();
200 
201         final TiffField tileLengthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_LENGTH);
202         if (null == tileLengthField) {
203             throw new ImagingException("Can't find tile length field.");
204         }
205         final int tileLength = tileLengthField.getIntValue();
206 
207         return new AbstractTiffImageData.Tiles(data, tileWidth, tileLength);
208     }
209 
210     public void read(final ByteSource byteSource, final FormatCompliance formatCompliance, final Listener listener) throws ImagingException, IOException {
211         readDirectories(byteSource, formatCompliance, listener);
212     }
213 
214     public TiffContents readContents(final ByteSource byteSource, final TiffImagingParameters params, final FormatCompliance formatCompliance)
215             throws ImagingException, IOException {
216 
217         final Collector collector = new Collector(params);
218         read(byteSource, formatCompliance, collector);
219         return collector.getContents();
220     }
221 
222     public TiffContents readDirectories(final ByteSource byteSource, final boolean readImageData, final FormatCompliance formatCompliance)
223             throws ImagingException, IOException {
224         final TiffImagingParameters params = new TiffImagingParameters();
225         params.setReadThumbnails(readImageData);
226         final Collector collector = new Collector(params);
227         readDirectories(byteSource, formatCompliance, collector);
228         final TiffContents contents = collector.getContents();
229         if (contents.directories.isEmpty()) {
230             throw new ImagingException("Image did not contain any directories.");
231         }
232         return contents;
233     }
234 
235 //    NOT USED
236 //    private static final class DirectoryCollector extends Collector {
237 //        private final boolean readImageData;
238 //
239 //        public DirectoryCollector(final boolean readImageData) {
240 //            this.readImageData = readImageData;
241 //        }
242 //
243 //        @Override
244 //        public boolean addDirectory(final TiffDirectory directory) {
245 //            super.addDirectory(directory);
246 //            return false;
247 //        }
248 //
249 //        @Override
250 //        public boolean readImageData() {
251 //            return readImageData;
252 //        }
253 //    }
254 
255     private void readDirectories(final ByteSource byteSource, final FormatCompliance formatCompliance, final Listener listener)
256             throws ImagingException, IOException {
257         final TiffHeader tiffHeader = readTiffHeader(byteSource);
258         if (!listener.setTiffHeader(tiffHeader)) {
259             return;
260         }
261 
262         final long offset = tiffHeader.offsetToFirstIFD;
263         final int dirType = TiffDirectoryConstants.DIRECTORY_TYPE_ROOT;
264 
265         final List<Number> visited = new ArrayList<>();
266         readDirectory(byteSource, offset, dirType, formatCompliance, listener, visited);
267     }
268 
269     private boolean readDirectory(final ByteSource byteSource, final long directoryOffset, final int dirType, final FormatCompliance formatCompliance,
270             final Listener listener, final boolean ignoreNextDirectory, final List<Number> visited) throws ImagingException, IOException {
271 
272         if (visited.contains(directoryOffset)) {
273             return false;
274         }
275         visited.add(directoryOffset);
276 
277         try (InputStream is = byteSource.getInputStream()) {
278             if (directoryOffset >= byteSource.size()) {
279                 return true;
280             }
281 
282             BinaryFunctions.skipBytes(is, directoryOffset);
283 
284             final List<TiffField> fields = new ArrayList<>();
285 
286             final long entryCount;
287             try {
288                 if (standardTiff) {
289                     entryCount = BinaryFunctions.read2Bytes("DirectoryEntryCount", is, "Not a Valid TIFF File", getByteOrder());
290                 } else {
291                     entryCount = BinaryFunctions.read8Bytes("DirectoryEntryCount", is, "Not a Valid TIFF File", getByteOrder());
292                 }
293             } catch (final IOException e) {
294                 if (strict) {
295                     throw e;
296                 }
297                 return true;
298             }
299 
300             for (int i = 0; i < entryCount; i++) {
301                 final int tag = BinaryFunctions.read2Bytes("Tag", is, "Not a Valid TIFF File", getByteOrder());
302                 final int type = BinaryFunctions.read2Bytes("Type", is, "Not a Valid TIFF File", getByteOrder());
303                 final long count;
304                 final byte[] offsetBytes;
305                 final long offset;
306                 if (standardTiff) {
307                     count = 0xFFFFffffL & BinaryFunctions.read4Bytes("Count", is, "Not a Valid TIFF File", getByteOrder());
308                     offsetBytes = BinaryFunctions.readBytes("Offset", is, 4, "Not a Valid TIFF File");
309                     offset = 0xFFFFffffL & ByteConversions.toInt(offsetBytes, getByteOrder());
310                 } else {
311                     count = BinaryFunctions.read8Bytes("Count", is, "Not a Valid TIFF File", getByteOrder());
312                     offsetBytes = BinaryFunctions.readBytes("Offset", is, 8, "Not a Valid TIFF File");
313                     offset = ByteConversions.toLong(offsetBytes, getByteOrder());
314                 }
315 
316                 if (tag == 0) {
317                     // skip invalid fields.
318                     // These are seen very rarely, but can have invalid value
319                     // lengths,
320                     // which can cause OOM problems.
321                     continue;
322                 }
323 
324                 final AbstractFieldType abstractFieldType;
325                 try {
326                     abstractFieldType = AbstractFieldType.getFieldType(type);
327                 } catch (final ImagingException imageReadEx) {
328                     // skip over unknown fields types, since we
329                     // can't calculate their size without
330                     // knowing their type
331                     continue;
332                 }
333                 final long valueLength = count * abstractFieldType.getSize();
334                 final byte[] value;
335                 if (valueLength > entryMaxValueLength) {
336                     if (offset < 0 || offset + valueLength > byteSource.size()) {
337                         if (strict) {
338                             throw new IOException("Attempt to read byte range starting from " + offset + " " + "of length " + valueLength + " "
339                                     + "which is outside the file's size of " + byteSource.size());
340                         }
341                         // corrupt field, ignore it
342                         continue;
343                     }
344                     value = byteSource.getByteArray(offset, (int) valueLength);
345                 } else {
346                     value = offsetBytes;
347                 }
348 
349                 final TiffField field = new TiffField(tag, dirType, abstractFieldType, count, offset, value, getByteOrder(), i);
350 
351                 fields.add(field);
352 
353                 if (!listener.addField(field)) {
354                     return true;
355                 }
356             }
357 
358             final long nextDirectoryOffset = 0xFFFFffffL & BinaryFunctions.read4Bytes("nextDirectoryOffset", is, "Not a Valid TIFF File", getByteOrder());
359 
360             final TiffDirectory directory = new TiffDirectory(dirType, fields, directoryOffset, nextDirectoryOffset, getByteOrder());
361 
362             if (listener.readImageData()) {
363                 if (directory.hasTiffImageData()) {
364                     final AbstractTiffImageData rawImageData = getTiffRawImageData(byteSource, directory);
365                     directory.setTiffImageData(rawImageData);
366                 }
367                 if (directory.hasJpegImageData()) {
368                     final JpegImageData rawJpegImageData = getJpegRawImageData(byteSource, directory);
369                     directory.setJpegImageData(rawJpegImageData);
370                 }
371             }
372 
373             if (!listener.addDirectory(directory)) {
374                 return true;
375             }
376 
377             if (listener.readOffsetDirectories()) {
378                 final TagInfoDirectory[] offsetFields = { ExifTagConstants.EXIF_TAG_EXIF_OFFSET, ExifTagConstants.EXIF_TAG_GPSINFO,
379                         ExifTagConstants.EXIF_TAG_INTEROP_OFFSET };
380                 final int[] directoryTypes = { TiffDirectoryConstants.DIRECTORY_TYPE_EXIF, TiffDirectoryConstants.DIRECTORY_TYPE_GPS,
381                         TiffDirectoryConstants.DIRECTORY_TYPE_INTEROPERABILITY };
382                 for (int i = 0; i < offsetFields.length; i++) {
383                     final TagInfoDirectory offsetField = offsetFields[i];
384                     final TiffField field = directory.findField(offsetField);
385                     if (field != null) {
386                         final long subDirectoryOffset;
387                         final int subDirectoryType;
388                         boolean subDirectoryRead = false;
389                         try {
390                             subDirectoryOffset = directory.getFieldValue(offsetField);
391                             subDirectoryType = directoryTypes[i];
392                             subDirectoryRead = readDirectory(byteSource, subDirectoryOffset, subDirectoryType, formatCompliance, listener, true, visited);
393 
394                         } catch (final ImagingException imageReadException) {
395                             if (strict) {
396                                 throw imageReadException;
397                             }
398                         }
399                         if (!subDirectoryRead) {
400                             fields.remove(field);
401                         }
402                     }
403                 }
404             }
405 
406             if (!ignoreNextDirectory && directory.getNextDirectoryOffset() > 0) {
407                 // Debug.debug("next dir", directory.nextDirectoryOffset );
408                 readDirectory(byteSource, directory.getNextDirectoryOffset(), dirType + 1, formatCompliance, listener, visited);
409             }
410 
411             return true;
412         }
413     }
414 
415     private boolean readDirectory(final ByteSource byteSource, final long offset, final int dirType, final FormatCompliance formatCompliance,
416             final Listener listener, final List<Number> visited) throws ImagingException, IOException {
417         final boolean ignoreNextDirectory = false;
418         return readDirectory(byteSource, offset, dirType, formatCompliance, listener, ignoreNextDirectory, visited);
419     }
420 
421     public TiffContents readFirstDirectory(final ByteSource byteSource, final boolean readImageData, final FormatCompliance formatCompliance)
422             throws ImagingException, IOException {
423         final Collector collector = new FirstDirectoryCollector(readImageData);
424         read(byteSource, formatCompliance, collector);
425         final TiffContents contents = collector.getContents();
426         if (contents.directories.isEmpty()) {
427             throw new ImagingException("Image did not contain any directories.");
428         }
429         return contents;
430     }
431 
432     private TiffHeader readTiffHeader(final ByteSource byteSource) throws ImagingException, IOException {
433         try (InputStream is = byteSource.getInputStream()) {
434             return readTiffHeader(is);
435         }
436     }
437 
438     private TiffHeader readTiffHeader(final InputStream is) throws ImagingException, IOException {
439         final int byteOrder1 = BinaryFunctions.readByte("BYTE_ORDER_1", is, "Not a Valid TIFF File");
440         final int byteOrder2 = BinaryFunctions.readByte("BYTE_ORDER_2", is, "Not a Valid TIFF File");
441         if (byteOrder1 != byteOrder2) {
442             throw new ImagingException("Byte Order bytes don't match (" + byteOrder1 + ", " + byteOrder2 + ").");
443         }
444 
445         final ByteOrder byteOrder = getTiffByteOrder(byteOrder1);
446         setByteOrder(byteOrder);
447 
448         // verify that the file is a supported TIFF format using
449         // the numeric indentifier
450         // Classic TIFF (32 bit): 42
451         // Big TIFF (64 bit): 43
452         //
453         final long offsetToFirstIFD;
454         final int tiffVersion = BinaryFunctions.read2Bytes("tiffVersion", is, "Not a Valid TIFF File", getByteOrder());
455         if (tiffVersion == VERSION_STANDARD) {
456             bigTiff = false;
457             standardTiff = true;
458             entryMaxValueLength = ENTRY_MAX_VALUE_LENGTH;
459             offsetToFirstIFD = 0xFFFFffffL & BinaryFunctions.read4Bytes("offsetToFirstIFD", is, "Not a Valid TIFF File", getByteOrder());
460         } else if (tiffVersion == VERSION_BIG) {
461             bigTiff = true;
462             standardTiff = false;
463             entryMaxValueLength = ENTRY_MAX_VALUE_LENGTH_BIG;
464             final int byteSize = BinaryFunctions.read2Bytes("bytesizeOfOffset", is, "Not a Valid TIFF File", getByteOrder());
465             final int expectedZero = BinaryFunctions.read2Bytes("expectedZero", is, "Not a Valid TIFF File", getByteOrder());
466             if (byteSize != 8 || expectedZero != 0) {
467                 throw new ImagingException("Misformed Big-TIFF header: " + tiffVersion);
468             }
469             offsetToFirstIFD = BinaryFunctions.read8Bytes("offsetToFirstIFD", is, "Not a Valid TIFF File", getByteOrder());
470         } else {
471             throw new ImagingException("Unknown TIFF Version: " + tiffVersion);
472         }
473 
474         BinaryFunctions.skipBytes(is, offsetToFirstIFD - 8, "Not a Valid TIFF File: couldn't find IFDs");
475 
476         return new TiffHeader(byteOrder, tiffVersion, offsetToFirstIFD, bigTiff);
477     }
478 }