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