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}