001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.imaging.formats.tiff;
018
019import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes;
020import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
021import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
022import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
023import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes;
024import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_ENTRY_MAX_VALUE_LENGTH;
025
026import java.io.IOException;
027import java.io.InputStream;
028import java.nio.ByteOrder;
029import java.util.ArrayList;
030import java.util.List;
031
032import org.apache.commons.imaging.FormatCompliance;
033import org.apache.commons.imaging.ImageReadException;
034import org.apache.commons.imaging.common.BinaryFileParser;
035import org.apache.commons.imaging.common.ByteConversions;
036import org.apache.commons.imaging.common.bytesource.ByteSource;
037import org.apache.commons.imaging.common.bytesource.ByteSourceFile;
038import org.apache.commons.imaging.formats.jpeg.JpegConstants;
039import org.apache.commons.imaging.formats.tiff.TiffDirectory.ImageDataElement;
040import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
041import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants;
042import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
043import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType;
044import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDirectory;
045
046public class TiffReader extends BinaryFileParser {
047
048    private final boolean strict;
049
050    public TiffReader(final boolean strict) {
051        this.strict = strict;
052    }
053
054    private TiffHeader readTiffHeader(final ByteSource byteSource) throws ImageReadException, IOException {
055        try (InputStream is = byteSource.getInputStream()) {
056            return readTiffHeader(is);
057        }
058    }
059
060    private ByteOrder getTiffByteOrder(final int byteOrderByte) throws ImageReadException {
061        if (byteOrderByte == 'I') {
062            return ByteOrder.LITTLE_ENDIAN; // Intel
063        }
064        if (byteOrderByte == 'M') {
065            return ByteOrder.BIG_ENDIAN; // Motorola
066        }
067        throw new ImageReadException("Invalid TIFF byte order " + (0xff & byteOrderByte));
068    }
069
070    private TiffHeader readTiffHeader(final InputStream is) throws ImageReadException, IOException {
071        final int byteOrder1 = readByte("BYTE_ORDER_1", is, "Not a Valid TIFF File");
072        final int byteOrder2 = readByte("BYTE_ORDER_2", is, "Not a Valid TIFF File");
073        if (byteOrder1 != byteOrder2) {
074            throw new ImageReadException("Byte Order bytes don't match (" + byteOrder1 + ", " + byteOrder2 + ").");
075        }
076
077        final ByteOrder byteOrder = getTiffByteOrder(byteOrder1);
078        setByteOrder(byteOrder);
079
080        final int tiffVersion = read2Bytes("tiffVersion", is, "Not a Valid TIFF File", getByteOrder());
081        if (tiffVersion != 42) {
082            throw new ImageReadException("Unknown Tiff Version: " + tiffVersion);
083        }
084
085        final long offsetToFirstIFD =
086                0xFFFFffffL & read4Bytes("offsetToFirstIFD", is, "Not a Valid TIFF File", getByteOrder());
087
088        skipBytes(is, offsetToFirstIFD - 8, "Not a Valid TIFF File: couldn't find IFDs");
089
090        return new TiffHeader(byteOrder, tiffVersion, offsetToFirstIFD);
091    }
092
093    private void readDirectories(final ByteSource byteSource,
094            final FormatCompliance formatCompliance, final Listener listener)
095            throws ImageReadException, IOException {
096        final TiffHeader tiffHeader = readTiffHeader(byteSource);
097        if (!listener.setTiffHeader(tiffHeader)) {
098            return;
099        }
100
101        final long offset = tiffHeader.offsetToFirstIFD;
102        final int dirType = TiffDirectoryConstants.DIRECTORY_TYPE_ROOT;
103
104        final List<Number> visited = new ArrayList<>();
105        readDirectory(byteSource, offset, dirType, formatCompliance, listener, visited);
106    }
107
108    private boolean readDirectory(final ByteSource byteSource, final long offset,
109            final int dirType, final FormatCompliance formatCompliance, final Listener listener,
110            final List<Number> visited) throws ImageReadException, IOException {
111        final boolean ignoreNextDirectory = false;
112        return readDirectory(byteSource, offset, dirType, formatCompliance,
113                listener, ignoreNextDirectory, visited);
114    }
115
116    private boolean readDirectory(final ByteSource byteSource, final long directoryOffset,
117            final int dirType, final FormatCompliance formatCompliance, final Listener listener,
118            final boolean ignoreNextDirectory, final List<Number> visited)
119            throws ImageReadException, IOException {
120
121        if (visited.contains(directoryOffset)) {
122            return false;
123        }
124        visited.add(directoryOffset);
125
126        try (InputStream is = byteSource.getInputStream()) {
127            if (directoryOffset >= byteSource.getLength()) {
128                return true;
129            }
130
131            skipBytes(is, directoryOffset);
132
133            final List<TiffField> fields = new ArrayList<>();
134
135            int entryCount;
136            try {
137                entryCount = read2Bytes("DirectoryEntryCount", is, "Not a Valid TIFF File", getByteOrder());
138            } catch (final IOException e) {
139                if (strict) {
140                    throw e;
141                }
142                return true;
143            }
144
145            for (int i = 0; i < entryCount; i++) {
146                final int tag = read2Bytes("Tag", is, "Not a Valid TIFF File", getByteOrder());
147                final int type = read2Bytes("Type", is, "Not a Valid TIFF File", getByteOrder());
148                final long count = 0xFFFFffffL & read4Bytes("Count", is, "Not a Valid TIFF File", getByteOrder());
149                final byte[] offsetBytes = readBytes("Offset", is, 4, "Not a Valid TIFF File");
150                final long offset = 0xFFFFffffL & ByteConversions.toInt(offsetBytes, getByteOrder());
151
152                if (tag == 0) {
153                    // skip invalid fields.
154                    // These are seen very rarely, but can have invalid value
155                    // lengths,
156                    // which can cause OOM problems.
157                    continue;
158                }
159
160                final FieldType fieldType;
161                try {
162                    fieldType = FieldType.getFieldType(type);
163                } catch (final ImageReadException imageReadEx) {
164                    // skip over unknown fields types, since we
165                    // can't calculate their size without
166                    // knowing their type
167                    continue;
168                }
169                final long valueLength = count * fieldType.getSize();
170                final byte[] value;
171                if (valueLength > TIFF_ENTRY_MAX_VALUE_LENGTH) {
172                    if ((offset < 0) || (offset + valueLength) > byteSource.getLength()) {
173                        if (strict) {
174                            throw new IOException(
175                                    "Attempt to read byte range starting from " + offset + " "
176                                            + "of length " + valueLength + " "
177                                            + "which is outside the file's size of "
178                                            + byteSource.getLength());
179                        }
180                        // corrupt field, ignore it
181                        continue;
182                    }
183                    value = byteSource.getBlock(offset, (int) valueLength);
184                } else {
185                    value = offsetBytes;
186                }
187
188                final TiffField field = new TiffField(tag, dirType, fieldType, count,
189                        offset, value, getByteOrder(), i);
190
191                fields.add(field);
192
193                if (!listener.addField(field)) {
194                    return true;
195                }
196            }
197
198            final long nextDirectoryOffset = 0xFFFFffffL & read4Bytes("nextDirectoryOffset", is,
199                    "Not a Valid TIFF File", getByteOrder());
200
201            final TiffDirectory directory = new TiffDirectory(
202                dirType,
203                fields,
204                directoryOffset,
205                nextDirectoryOffset,
206                getByteOrder());
207
208            if (listener.readImageData()) {
209                if (directory.hasTiffImageData()) {
210                    final TiffImageData rawImageData = getTiffRawImageData(
211                            byteSource, directory);
212                    directory.setTiffImageData(rawImageData);
213                }
214                if (directory.hasJpegImageData()) {
215                    final JpegImageData rawJpegImageData = getJpegRawImageData(
216                            byteSource, directory);
217                    directory.setJpegImageData(rawJpegImageData);
218                }
219            }
220
221            if (!listener.addDirectory(directory)) {
222                return true;
223            }
224
225            if (listener.readOffsetDirectories()) {
226                final TagInfoDirectory[] offsetFields = {
227                        ExifTagConstants.EXIF_TAG_EXIF_OFFSET,
228                        ExifTagConstants.EXIF_TAG_GPSINFO,
229                        ExifTagConstants.EXIF_TAG_INTEROP_OFFSET
230                };
231                final int[] directoryTypes = {
232                        TiffDirectoryConstants.DIRECTORY_TYPE_EXIF,
233                        TiffDirectoryConstants.DIRECTORY_TYPE_GPS,
234                        TiffDirectoryConstants.DIRECTORY_TYPE_INTEROPERABILITY
235                };
236                for (int i = 0; i < offsetFields.length; i++) {
237                    final TagInfoDirectory offsetField = offsetFields[i];
238                    final TiffField field = directory.findField(offsetField);
239                    if (field != null) {
240                        long subDirectoryOffset;
241                        int subDirectoryType;
242                        boolean subDirectoryRead = false;
243                        try {
244                            subDirectoryOffset = directory.getFieldValue(offsetField);
245                            subDirectoryType = directoryTypes[i];
246                            subDirectoryRead = readDirectory(byteSource,
247                                    subDirectoryOffset, subDirectoryType,
248                                    formatCompliance, listener, true, visited);
249
250                        } catch (final ImageReadException imageReadException) {
251                            if (strict) {
252                                throw imageReadException;
253                            }
254                        }
255                        if (!subDirectoryRead) {
256                            fields.remove(field);
257                        }
258                    }
259                }
260            }
261
262            if (!ignoreNextDirectory && directory.nextDirectoryOffset > 0) {
263                // Debug.debug("next dir", directory.nextDirectoryOffset );
264                readDirectory(byteSource, directory.nextDirectoryOffset,
265                        dirType + 1, formatCompliance, listener, visited);
266            }
267
268            return true;
269        }
270    }
271
272    public interface Listener {
273        boolean setTiffHeader(TiffHeader tiffHeader);
274
275        boolean addDirectory(TiffDirectory directory);
276
277        boolean addField(TiffField field);
278
279        boolean readImageData();
280
281        boolean readOffsetDirectories();
282    }
283
284    private static class Collector implements Listener {
285        private TiffHeader tiffHeader;
286        private final List<TiffDirectory> directories = new ArrayList<>();
287        private final List<TiffField> fields = new ArrayList<>();
288        private final boolean readThumbnails;
289
290        Collector() {
291            this(new TiffImagingParameters());
292        }
293
294        Collector(final TiffImagingParameters params) {
295            this.readThumbnails = params.isReadThumbnails();
296        }
297
298        @Override
299        public boolean setTiffHeader(final TiffHeader tiffHeader) {
300            this.tiffHeader = tiffHeader;
301            return true;
302        }
303
304        @Override
305        public boolean addDirectory(final TiffDirectory directory) {
306            directories.add(directory);
307            return true;
308        }
309
310        @Override
311        public boolean addField(final TiffField field) {
312            fields.add(field);
313            return true;
314        }
315
316        @Override
317        public boolean readImageData() {
318            return readThumbnails;
319        }
320
321        @Override
322        public boolean readOffsetDirectories() {
323            return true;
324        }
325
326        public TiffContents getContents() {
327            return new TiffContents(tiffHeader, directories, fields);
328        }
329    }
330
331    private static class FirstDirectoryCollector extends Collector {
332        private final boolean readImageData;
333
334        FirstDirectoryCollector(final boolean readImageData) {
335            this.readImageData = readImageData;
336        }
337
338        @Override
339        public boolean addDirectory(final TiffDirectory directory) {
340            super.addDirectory(directory);
341            return false;
342        }
343
344        @Override
345        public boolean readImageData() {
346            return readImageData;
347        }
348    }
349
350//    NOT USED
351//    private static class DirectoryCollector extends Collector {
352//        private final boolean readImageData;
353//
354//        public DirectoryCollector(final boolean readImageData) {
355//            this.readImageData = readImageData;
356//        }
357//
358//        @Override
359//        public boolean addDirectory(final TiffDirectory directory) {
360//            super.addDirectory(directory);
361//            return false;
362//        }
363//
364//        @Override
365//        public boolean readImageData() {
366//            return readImageData;
367//        }
368//    }
369
370    public TiffContents readFirstDirectory(final ByteSource byteSource, final boolean readImageData, final FormatCompliance formatCompliance)
371            throws ImageReadException, IOException {
372        final Collector collector = new FirstDirectoryCollector(readImageData);
373        read(byteSource, formatCompliance, collector);
374        final TiffContents contents = collector.getContents();
375        if (contents.directories.isEmpty()) {
376            throw new ImageReadException(
377                    "Image did not contain any directories.");
378        }
379        return contents;
380    }
381
382    public TiffContents readDirectories(final ByteSource byteSource,
383            final boolean readImageData, final FormatCompliance formatCompliance)
384            throws ImageReadException, IOException {
385        final TiffImagingParameters params = new TiffImagingParameters();
386        params.setReadThumbnails(readImageData);
387        final Collector collector = new Collector(params);
388        readDirectories(byteSource, formatCompliance, collector);
389        final TiffContents contents = collector.getContents();
390        if (contents.directories.isEmpty()) {
391            throw new ImageReadException(
392                    "Image did not contain any directories.");
393        }
394        return contents;
395    }
396
397    public TiffContents readContents(final ByteSource byteSource, final TiffImagingParameters params,
398            final FormatCompliance formatCompliance) throws ImageReadException,
399            IOException {
400
401        final Collector collector = new Collector(params);
402        read(byteSource, formatCompliance, collector);
403        return collector.getContents();
404    }
405
406    public void read(final ByteSource byteSource, final FormatCompliance formatCompliance, final Listener listener)
407            throws ImageReadException, IOException {
408        readDirectories(byteSource, formatCompliance, listener);
409    }
410
411    private TiffImageData getTiffRawImageData(final ByteSource byteSource,
412            final TiffDirectory directory) throws ImageReadException, IOException {
413
414        final List<ImageDataElement> elements = directory.getTiffRawImageDataElements();
415        final TiffImageData.Data[] data = new TiffImageData.Data[elements.size()];
416
417        if (byteSource instanceof ByteSourceFile) {
418            final ByteSourceFile bsf = (ByteSourceFile) byteSource;
419            for (int i = 0; i < elements.size(); i++) {
420                final TiffDirectory.ImageDataElement element = elements.get(i);
421                data[i] = new TiffImageData.ByteSourceData(element.offset,
422                        element.length, bsf);
423            }
424        } else {
425            for (int i = 0; i < elements.size(); i++) {
426                final TiffDirectory.ImageDataElement element = elements.get(i);
427                final byte[] bytes = byteSource.getBlock(element.offset, element.length);
428                data[i] = new TiffImageData.Data(element.offset, element.length, bytes);
429            }
430        }
431
432        if (directory.imageDataInStrips()) {
433            final TiffField rowsPerStripField = directory.findField(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP);
434            /*
435             * Default value of rowsperstrip is assumed to be infinity
436             * http://www.awaresystems.be/imaging/tiff/tifftags/rowsperstrip.html
437             */
438            int rowsPerStrip = Integer.MAX_VALUE;
439
440            if (null != rowsPerStripField) {
441                rowsPerStrip = rowsPerStripField.getIntValue();
442            } else {
443                final TiffField imageHeight = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH);
444                /**
445                 * if rows per strip not present then rowsPerStrip is equal to
446                 * imageLength or an infinity value;
447                 */
448                if (imageHeight != null) {
449                    rowsPerStrip = imageHeight.getIntValue();
450                }
451
452            }
453
454            return new TiffImageData.Strips(data, rowsPerStrip);
455        }
456        final TiffField tileWidthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_WIDTH);
457        if (null == tileWidthField) {
458            throw new ImageReadException("Can't find tile width field.");
459        }
460        final int tileWidth = tileWidthField.getIntValue();
461
462        final TiffField tileLengthField = directory.findField(TiffTagConstants.TIFF_TAG_TILE_LENGTH);
463        if (null == tileLengthField) {
464            throw new ImageReadException("Can't find tile length field.");
465        }
466        final int tileLength = tileLengthField.getIntValue();
467
468        return new TiffImageData.Tiles(data, tileWidth, tileLength);
469    }
470
471    private JpegImageData getJpegRawImageData(final ByteSource byteSource,
472            final TiffDirectory directory) throws ImageReadException, IOException {
473        final ImageDataElement element = directory.getJpegRawImageDataElement();
474        final long offset = element.offset;
475        int length = element.length;
476        // In case the length is not correct, adjust it and check if the last read byte actually is the end of the image
477        if (offset + length > byteSource.getLength()) {
478            length = (int) (byteSource.getLength() - offset);
479        }
480        final byte[] data = byteSource.getBlock(offset, length);
481        // check if the last read byte is actually the end of the image data
482        if (strict &&
483                (length < 2 ||
484                (((data[data.length - 2] & 0xff) << 8) | (data[data.length - 1] & 0xff)) != JpegConstants.EOI_MARKER)) {
485            throw new ImageReadException("JPEG EOI marker could not be found at expected location");
486        }
487        return new JpegImageData(offset, length, data);
488    }
489
490}