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