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}