    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}