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