All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.algart.matrices.tiff.TiffReader Maven / Gradle / Ivy

There is a newer version: 1.3.7
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2023-2024 Daniel Alievsky, AlgART Laboratory (http://algart.net)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package net.algart.matrices.tiff;

import net.algart.arrays.Matrix;
import net.algart.arrays.UpdatablePArray;
import net.algart.matrices.tiff.codecs.TiffCodec;
import net.algart.matrices.tiff.tags.TagCompression;
import net.algart.matrices.tiff.tags.TagRational;
import net.algart.matrices.tiff.tags.TagTypes;
import net.algart.matrices.tiff.tags.Tags;
import net.algart.matrices.tiff.tiles.TiffMap;
import net.algart.matrices.tiff.tiles.TiffTile;
import net.algart.matrices.tiff.tiles.TiffTileIO;
import net.algart.matrices.tiff.tiles.TiffTileIndex;
import org.scijava.Context;
import org.scijava.io.handle.DataHandle;
import org.scijava.io.handle.ReadBufferDataHandle;
import org.scijava.io.location.Location;

import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * Reads TIFF format.
 *
 * 

This object is internally synchronized and thread-safe when used in multi-threaded environment. * However, you should not modify objects, passed to the methods of this class from a parallel thread; * first of all, it concerns the {@link TiffIFD} arguments of many methods. * The same is true for the result of {@link #stream()} method.

*/ public class TiffReader implements Closeable { // Creating this class started from reworking SCIFIO TiffParser class. // Below is a copy of list of its authors and of the SCIFIO license for that class. // (It is placed here to avoid autocorrection by IntelliJ IDEA) /* * @author Curtis Rueden * @author Eric Kjellman * @author Melissa Linkert * @author Chris Allan * * #%L * SCIFIO library for reading and converting scientific file formats. * %% * Copyright (C) 2011 - 2023 SCIFIO developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * #L% */ /** * IFD with number of entries, greater than this limit, is not allowed: * it is mostly probable that it is corrupted file. */ public static int MAX_NUMBER_OF_IFD_ENTRIES = 1_000_000; private static final boolean OPTIMIZE_READING_IFD_ARRAYS = true; // - Note: this optimization allows to speed up reading large array of offsets. // If we use simple FileHandle for reading file (based on RandomAccessFile), // acceleration is up to 100 and more times: // on my computer, 23220 int32 values were loaded in 0.2 ms instead of 570 ms. // Since scijava-common 2.95.1, we use optimized ReadBufferDataHandle for reading file; // now acceleration for 23220 int32 values is 0.2 ms instead of 0.4 ms. static final boolean USE_LEGACY_UNPACK_BYTES = false; // - Should be false for better performance; necessary for debugging needs only // (together with uncommenting unpackBytesLegacy call) static final boolean THOROUGHLY_TEST_Y_CB_CR_LOOP = false; private static final int MINIMAL_ALLOWED_TIFF_FILE_LENGTH = 8 + 2 + 12 + 4; // - 8 bytes header + at least 1 IFD entry (usually at least 2 entries required: ImageWidth + ImageLength); // this constant should be > 16 to detect "dummy" BigTIFF file, containing header only private static final System.Logger LOG = System.getLogger(TiffReader.class.getName()); private static final boolean LOGGABLE_DEBUG = LOG.isLoggable(System.Logger.Level.DEBUG); private boolean requireValidTiff; private boolean interleaveResults = false; private boolean autoUnpackUnusualPrecisions = true; private boolean autoScaleWhenIncreasingBitDepth = true; private boolean autoCorrectInvertedBrightness = false; private boolean enforceUseExternalCodec = false; private boolean cropTilesToImageBoundaries = true; private boolean cachingIFDs = true; private boolean missingTilesAllowed = false; private byte byteFiller = 0; private volatile Context context = null; private final Exception openingException; private final DataHandle in; private final boolean valid; private final boolean bigTiff; /** * Cached list of IFDs in the current file. */ private volatile List ifds; /** * Cached first IFD in the current file. */ private volatile TiffIFD firstIFD; private volatile Object scifio = null; private final Object fileLock = new Object(); private TiffCodec.Options codecOptions = new TiffCodec.Options(); private volatile long positionOfLastIFDOffset = -1; private long timeReading = 0; private long timeCustomizingDecoding = 0; private long timeDecoding = 0; private long timeDecodingMain = 0; private long timeDecodingBridge = 0; private long timeDecodingAdditional = 0; private long timeCompleteDecoding = 0; public TiffReader(Path file) throws IOException { this(file, true); } public TiffReader(Path file, boolean requireValidTiff) throws IOException { this(TiffTools.getExistingFileHandle(file), requireValidTiff, true); } /** * Equivalent to {@link #TiffReader(DataHandle, boolean, boolean)} with false last argument. * Note that you should not call this constructor from another constructor, creating this * DataHandle: in this case, the handle will never be closed! * * @param inputStream input stream. * @param requireValidTiff whether the input file must exist and be a readable TIFF-file * with a correct header. * @throws TiffException if the file is not a correct TIFF file * @throws IOException in a case of any problems with the input file */ public TiffReader(DataHandle inputStream, boolean requireValidTiff) throws IOException { this(inputStream, requireValidTiff, false); } /** * Constructs new reader. * *

If requireValidTiff is true (standard variant), it will throw an exception * in a case of incorrect TIFF header or some other I/O errors. * In this case, closeStreamOnException flag specifies, whether this function * must close the input stream or no. It should be true when you call this constructor * from another constructor, which creates DataHandle: it is the only way to close * an invalid file. In other situation this flag may be false, then you must close * the input stream yourself. * *

If requireValidTiff is false, all exceptions are caught and not thrown, * and {@link #isValid()} method will return false. * In this case, you can know the occurred exception by {@link #openingException()} method. * * @param inputStream input stream. * @param requireValidTiff whether the input file must exist and be a readable TIFF-file * with a correct header. * @param closeStreamOnException if true, the input stream is closed in a case of any exception; * ignored if requireValidTiff is false. * @throws TiffException if the file is not a correct TIFF file. * @throws IOException in a case of any problems with the input file. */ public TiffReader(DataHandle inputStream, boolean requireValidTiff, boolean closeStreamOnException) throws IOException { this(inputStream, null); this.requireValidTiff = requireValidTiff; if (requireValidTiff && openingException != null) { if (closeStreamOnException) { try { inputStream.close(); } catch (Exception ignored) { } } if (openingException instanceof IOException e) { throw e; } if (openingException instanceof RuntimeException e) { throw e; } throw new TiffException(openingException); } } /** * Universal constructor. * *

It never throws an exception. If the file is not a correct TIFF or in a case of any other I/O problem, * the information about the problem is stored in an exception, which can be retrieved later * by {@link #openingException()} method and which is passed to exceptionHandler * (if it is not null). * *

This constructor is useful, because it allows to make constructors in subclasses, which do not through * any exceptions. * * @param inputStream input stream. * @param exceptionHandler if not null, it will be called in a case of some checked exception; * for example, it may log it. But usually it is better idea to use the main * constructor {@link #TiffReader(DataHandle, boolean, boolean)} with catching exception. */ public TiffReader(DataHandle inputStream, Consumer exceptionHandler) { Objects.requireNonNull(inputStream, "Null in stream"); this.requireValidTiff = false; this.in = inputStream instanceof ReadBufferDataHandle ? inputStream : new ReadBufferDataHandle<>(inputStream); AtomicBoolean bigTiff = new AtomicBoolean(false); this.openingException = startReading(bigTiff); this.valid = openingException == null; this.bigTiff = bigTiff.get(); if (exceptionHandler != null) { exceptionHandler.accept(openingException); } } public boolean isRequireValidTiff() { return requireValidTiff; } /** * Sets whether the parser should always require valid TIFF format. * Default value is specified in the constructor or is false * when using a constructor without such argument. * * @param requireValidTiff whether TIFF file should be correct. * @return a reference to this object. */ public TiffReader setRequireValidTiff(boolean requireValidTiff) { this.requireValidTiff = requireValidTiff; return this; } public boolean isInterleaveResults() { return interleaveResults; } /** * Sets the interleave mode: the loaded samples will be returned in chunked form, for example, RGBRGBRGB... * in a case of RGB image. If not set (default behaviour), the samples are returned in unpacked form: * RRR...GGG...BBB... * * @param interleaveResults new interleaving mode. * @return a reference to this object. */ public TiffReader setInterleaveResults(boolean interleaveResults) { this.interleaveResults = interleaveResults; return this; } public boolean isAutoUnpackUnusualPrecisions() { return autoUnpackUnusualPrecisions; } public TiffReader setAutoUnpackUnusualPrecisions(boolean autoUnpackUnusualPrecisions) { this.autoUnpackUnusualPrecisions = autoUnpackUnusualPrecisions; return this; } public boolean isAutoScaleWhenIncreasingBitDepth() { return autoScaleWhenIncreasingBitDepth; } /** * Sets the flag, whether do we need to scale pixel sample values when automatic increasing bit depths, * for example, when we decode 12-bit grayscale image into 16-bit result. * *

This class can successfully read TIFF with bit depths not divided by 8, such as 4-bit, 12-bit images or * 5+5+5 "HiRes" RGB images. But the data returned by this class is always represented by 8-bit, 16-bit, * 32-bit integer values (signed or unsigned) or by 32- or 64-bit floating-point values * (these bit depths correspond to Java primitive types). If the source pixel values have another bit depth, * they are automatically converted to the nearest "larger" type, for example, 4-bit integer is converted * to 8-bit, 12-bit integer is converted to 16-bit, 24-bit to 32-bit.

* *

If this flag is false, this conversion is performed "as-is", so, values 0..15 in 4-bit source data * will be converted to the same values 0..15 with 8-bit precision. * This is good if you need to process these values using some kind of algorithm. * However, if you need to show the real picture to the end user, then values 0..15 with 8-bit * precisions (or 0..4095 with 16-bit precision) will look almost black. To avoid this, you may use true * value of this flag, which causes automatic scaling returned values: multiplying by * (2n−1)/(2k−1), where n is the result bit depth * and k is the source one (for example, for 12-bit image k=12 and n=16). * As the result, the returned picture will look alike the source one.

* *

Default value is true. However, the scaling is still not performed if * PhotometricInterpretation TIFF tag is "Palette" (3) or "Transparency Mask" (4): in these cases * scaling has no sense. * * @param autoScaleWhenIncreasingBitDepth whether do we need to scale pixel samples, represented with k * bits/sample, k%8 ≠ 0, when increasing bit depth * to nearest n bits/sample, where * n > k and n is divided by 8. * @return a reference to this object. */ public TiffReader setAutoScaleWhenIncreasingBitDepth(boolean autoScaleWhenIncreasingBitDepth) { this.autoScaleWhenIncreasingBitDepth = autoScaleWhenIncreasingBitDepth; return this; } public boolean isAutoCorrectInvertedBrightness() { return autoCorrectInvertedBrightness; } /** * Sets the flag, whether do we need to automatically correct (invert) pixel sample values in color space * with inverted sense of pixel brightness, i.e. when PhotometricInterpretation TIFF tag is "WhiteIsZero" (0) * or "Separated" (CMYK, 5). (In these color spaces, white color is encoded as zero, and black color is encoded * as maximal allowed value like 255 for 8-bit samples.) * Note that this flag do not provide correct processing CMYK color model * and absolutely useless for more complex color spaces like CIELAB, but for PhotometricInterpretation=0 or 5 * it helps to provide RGB results more similar to the correct picture. * *

Default value is false. You may set it to true if the only goal of reading TIFF * is to show the image to a user. * * @param autoCorrectInvertedBrightness whether do we need to invert samples for "WhiteIsZero" and "CMYK" * photometric interpretations. * @return a reference to this object. */ public TiffReader setAutoCorrectInvertedBrightness(boolean autoCorrectInvertedBrightness) { this.autoCorrectInvertedBrightness = autoCorrectInvertedBrightness; return this; } public boolean isEnforceUseExternalCodec() { return enforceUseExternalCodec; } public TiffReader setEnforceUseExternalCodec(boolean enforceUseExternalCodec) { this.enforceUseExternalCodec = enforceUseExternalCodec; return this; } public boolean isCropTilesToImageBoundaries() { return cropTilesToImageBoundaries; } public TiffReader setCropTilesToImageBoundaries(boolean cropTilesToImageBoundaries) { this.cropTilesToImageBoundaries = cropTilesToImageBoundaries; return this; } /** * Retrieves the current set of codec options being used to decompress pixel * data. * * @return See above. */ public TiffCodec.Options getCodecOptions() { return codecOptions.clone(); } /** * Sets the codec options to be used when decompressing pixel data. * * @param codecOptions Codec options to use. * @return a reference to this object. */ public TiffReader setCodecOptions(TiffCodec.Options codecOptions) { this.codecOptions = Objects.requireNonNull(codecOptions, "Null codecOptions").clone(); return this; } public boolean isCachingIFDs() { return cachingIFDs; } /** * Sets whether IFD entries, returned by {@link #allIFDs()} method, should be cached. * *

Default value is true. Possible reason to set is to false * is reading file which is dynamically modified. * In other cases, usually it should be true, though false value * also works well if you are not going to call {@link #allIFDs()} more than once. * * @param cachingIFDs whether caching IFD is enabled. * @return a reference to this object. */ public TiffReader setCachingIFDs(final boolean cachingIFDs) { this.cachingIFDs = cachingIFDs; return this; } public boolean isMissingTilesAllowed() { return missingTilesAllowed; } /** * Sets the special mode, when TIFF file is allowed to contain "missing" tiles or strips, * for which the offset (TileOffsets or StripOffsets tag) and/or * byte count (TileByteCounts or StripByteCounts tag) contains zero value. * In this mode, such tiles/strips will be successfully read as empty rectangles, filled by * the {@link #setByteFiller(byte) default filler}. * *

Default value is false. In this case, such tiles/strips are not allowed, * as the standard TIFF format requires. * * @param missingTilesAllowed whether "missing" tiles/strips are allowed. * @return a reference to this object. */ public TiffReader setMissingTilesAllowed(boolean missingTilesAllowed) { this.missingTilesAllowed = missingTilesAllowed; return this; } public byte getByteFiller() { return byteFiller; } /** * Sets the filler byte for tiles, lying completely outside the image. * Value 0 means black color, 0xFF usually means white color. * *

Warning! If you want to work with non-8-bit TIFF, especially float precision, you should * preserve default 0 value, in other case results could be very strange. * * @param byteFiller new filler. * @return a reference to this object. */ public TiffReader setByteFiller(byte byteFiller) { this.byteFiller = byteFiller; return this; } public Context getContext() { return context; } public TiffReader setContext(Context context) { this.scifio = null; this.context = context; return this; } /** * Gets the stream from which TIFF data is being parsed. */ public DataHandle stream() { synchronized (fileLock) { // - we prefer not to return this stream in the middle of I/O operations return in; } } public boolean isValid() { return valid; } public Exception openingException() { return openingException; } /** * Returns whether or not the current TIFF file contains BigTIFF data. */ public boolean isBigTiff() { return bigTiff; } /** * Returns whether or not we are reading little-endian data. * Determined in the constructors. */ public boolean isLittleEndian() { return in.isLittleEndian(); } /** * Returns position in the file of the last IFD offset, loaded by {@link #readIFDOffsets()}, * {@link #readSingleIFDOffset(int)} or {@link #readFirstIFDOffset()} methods. * Usually it is just a position of the offset of the last IFD, because * popular {@link #allIFDs()} method calls {@link #readIFDOffsets()} inside. * *

Immediately after creating new object this position is -1. * * @return file position of the last IFD offset. */ public long positionOfLastIFDOffset() { return positionOfLastIFDOffset; } /** * Returns {@link #allIFDs()}.size(). * * @return number of existing IFDs. * @throws TiffException if the file is not a correct TIFF file. * @throws IOException in a case of any problems with the input file. */ public int numberOfIFDs() throws IOException { return allIFDs().size(); } public TiffMap map(int ifdIndex) throws IOException { return newMap(ifd(ifdIndex)); } /** * Calls {@link #allIFDs()} and returns IFD with the specified index. * If ifdIndex is too big (≥{@link #numberOfIFDs()}), this method throws {@link TiffException}.

* * @param ifdIndex index of IFD. * @return the IFD with the specified index. * @throws TiffException if ifdIndex is too large, or if the file is not a correct TIFF file * and this was not detected while opening it. * @throws IOException in a case of any problems with the input file. * @throws IllegalArgumentException if ifdIndex<0. */ public TiffIFD ifd(int ifdIndex) throws IOException { List ifdList = allIFDs(); if (ifdIndex < 0) { throw new IllegalArgumentException("Negative IFD index " + ifdIndex); } if (ifdIndex >= ifdList.size()) { throw new TiffException( "IFD index " + ifdIndex + " is out of bounds 0 <= index < " + ifdList.size()); } return ifdList.get(ifdIndex); } /** * Reads 1st IFD (#0). * *

Note: this method does not use {@link #allIFDs()} method. * If you really needs access only to 1st IFD, * this method may work faster than {@link #ifd(int)}. */ public TiffIFD firstIFD() throws IOException { TiffIFD firstIFD = this.firstIFD; if (cachingIFDs && firstIFD != null) { return this.firstIFD; } final long offset = readFirstIFDOffset(); firstIFD = readIFDAt(offset); if (cachingIFDs) { this.firstIFD = firstIFD; } return firstIFD; } public List allMaps() throws IOException { return allIFDs().stream().map(this::newMap).toList(); } /** * Returns all IFDs in the file in unmodifiable list. * When first called, reads all IFD from the file * (but this can be disabled using {@link #setCachingIFDs(boolean)} method). * *

Note: if this TIFF file is not valid ({@link #isValid()} returns false), this method * returns an empty list and does not throw an exception. For valid TIFF, result cannot be empty. * * @throws TiffException if the file is not a correct TIFF file, but this was not detected while opening it. * @throws IOException in a case of any problems with the input file. */ public List allIFDs() throws IOException { long t1 = debugTime(); List ifds; synchronized (fileLock) { // - this synchronization is not necessary, but helps // to be sure that the client will not try to read TIFF images // when all IFD are not fully loaded and checked ifds = this.ifds; if (cachingIFDs && ifds != null) { return ifds; } final long[] offsets = readIFDOffsets(); ifds = new ArrayList<>(); for (final long offset : offsets) { final TiffIFD ifd = readIFDAt(offset); assert ifd != null; ifds.add(ifd); long[] subOffsets = null; try { // Deprecated solution: "fillInIFD" technique is no longer used // if (!cachingIFDs && ifd.containsKey(IFD.SUB_IFD)) { // fillInIFD(ifd); // } subOffsets = ifd.getLongArray(Tags.SUB_IFD); } catch (final TiffException ignored) { } if (subOffsets != null) { for (final long subOffset : subOffsets) { final TiffIFD sub = readIFDAt(subOffset, Tags.SUB_IFD, false); if (sub != null) { ifds.add(sub); } } } } if (cachingIFDs) { this.ifds = Collections.unmodifiableList(ifds); } } if (TiffTools.BUILT_IN_TIMING && LOGGABLE_DEBUG) { long t2 = debugTime(); LOG.log(System.Logger.Level.DEBUG, String.format(Locale.US, "%s read %d IFDs: %.3f ms", getClass().getSimpleName(), ifds.size(), (t2 - t1) * 1e-6)); } return ifds; } /** * Returns EXIF IFDs. */ public List exifIFDs() throws IOException { final List ifds = allIFDs(); final List result = new ArrayList<>(); for (final TiffIFD ifd : ifds) { final long offset = ifd.getLong(Tags.EXIF, 0); if (offset != 0) { final TiffIFD exifIFD = readIFDAt(offset, Tags.EXIF, false); if (exifIFD != null) { result.add(exifIFD); } } } return result; } /** * Gets offset to the first IFD. * Updates {@link #positionOfLastIFDOffset()} to the position of first offset (4, for Bit-TIFF 8). */ public long readFirstIFDOffset() throws IOException { synchronized (fileLock) { in.seek(bigTiff ? 8 : 4); return readFirstOffsetFromCurrentPosition(true, this.bigTiff); } } /** * Returns the file offset of IFD with given index or -1 if the index is too high. * Updates {@link #positionOfLastIFDOffset()} to position of this offset. * * @param ifdIndex index of IFD (0, 1, ...). * @return offset of this IFD in the file or -1 if the index is too high. */ public long readSingleIFDOffset(int ifdIndex) throws IOException { if (ifdIndex < 0) { throw new IllegalArgumentException("Negative ifdIndex = " + ifdIndex); } synchronized (fileLock) { final long fileLength = in.length(); long offset = readFirstIFDOffset(); while (offset > 0 && offset < fileLength) { if (ifdIndex-- <= 0) { return offset; } in.seek(offset); skipIFDEntries(fileLength); final long newOffset = readNextOffset(true); if (newOffset == offset) { throw new TiffException("TIFF file is broken - infinite loop of IFD offsets is detected " + "for offset " + offset); } offset = newOffset; } return -1; } } /** * Gets the offsets to every IFD in the file. * *

Note: if this TIFF file is not valid ({@link #isValid()} returns false), this method * returns an empty array and does not throw an exception. For valid TIFF, result cannot be empty. */ public long[] readIFDOffsets() throws IOException { synchronized (fileLock) { if (!requireValidTiff && !valid) { return new long[0]; } final long fileLength = in.length(); final LinkedHashSet ifdOffsets = new LinkedHashSet<>(); long offset = readFirstIFDOffset(); while (offset > 0 && offset < fileLength) { in.seek(offset); final boolean wasNotPresent = ifdOffsets.add(offset); if (!wasNotPresent) { throw new TiffException("TIFF file is broken - infinite loop of IFD offsets is detected " + "for offset " + offset + " (the stored ifdOffsets sequence is " + ifdOffsets.stream().map(Object::toString).collect(Collectors.joining(", ")) + ", " + offset + ", ...)"); } skipIFDEntries(fileLength); offset = readNextOffset(true); } if (requireValidTiff && ifdOffsets.isEmpty()) { throw new AssertionError("No IFDs, but it was not checked in readFirstIFDOffset"); } return ifdOffsets.stream().mapToLong(v -> v).toArray(); } } public TiffIFD readSingleIFD(int ifdIndex) throws IOException, NoSuchElementException { long startOffset = readSingleIFDOffset(ifdIndex); if (startOffset < 0) { throw new NoSuchElementException("No IFD #" + ifdIndex + " in TIFF" + prettyInName() + ": too large index"); } return readIFDAt(startOffset); } /** * Reads the IFD stored at the given offset. * Never returns null. */ public TiffIFD readIFDAt(long startOffset) throws IOException { return readIFDAt(startOffset, null, true); } public TiffIFD readIFDAt(final long startOffset, Integer subIFDType, boolean readNextOffset) throws IOException { if (startOffset < 0) { throw new IllegalArgumentException("Negative file offset = " + startOffset); } if (startOffset < (bigTiff ? 16 : 8)) { throw new IllegalArgumentException("Attempt to read IFD from too small start offset " + startOffset); } long t1 = debugTime(); long timeEntries = 0; long timeArrays = 0; final TiffIFD ifd; synchronized (fileLock) { if (startOffset >= in.length()) { throw new TiffException("TIFF IFD offset " + startOffset + " is outside the file"); } final Map map = new LinkedHashMap<>(); final Map detailedEntries = new LinkedHashMap<>(); // read in directory entries for this IFD in.seek(startOffset); final long numberOfEntries = bigTiff ? in.readLong() : in.readUnsignedShort(); if (numberOfEntries < 0 || numberOfEntries > MAX_NUMBER_OF_IFD_ENTRIES) { throw new TiffException("Too large number of IFD entries: " + (numberOfEntries < 0 ? ">= 2^63" : numberOfEntries + " > " + MAX_NUMBER_OF_IFD_ENTRIES)); // - theoretically BigTIFF allows to have more entries, but we prefer to make some restriction; // in any case, billions if detailedEntries will probably lead to OutOfMemoryError or integer overflow } final int bytesPerEntry = bigTiff ? TiffTools.BIG_TIFF_BYTES_PER_ENTRY : TiffTools.BYTES_PER_ENTRY; final int baseOffset = bigTiff ? 8 : 2; for (long i = 0; i < numberOfEntries; i++) { long tEntry1 = debugTime(); in.seek(startOffset + baseOffset + bytesPerEntry * i); final TiffIFD.TiffEntry entry = readIFDEntry(); final int tag = entry.tag(); long tEntry2 = debugTime(); timeEntries += tEntry2 - tEntry1; final Object value = readIFDValueAtEntryOffset(in, entry); long tEntry3 = debugTime(); timeArrays += tEntry3 - tEntry2; // System.err.printf("%d values from %d: %.6f ms%n", valueCount, valueOffset, (tEntry3 - tEntry2) * 1e-6); if (value != null && !map.containsKey(tag)) { // - null value should not occur in current version; // if this tag is present twice (strange mistake if TIFF file), // we do not throw exception and just use the 1st entry map.put(tag, value); detailedEntries.put(tag, entry); } } final long positionOfNextOffset = startOffset + baseOffset + bytesPerEntry * numberOfEntries; in.seek(positionOfNextOffset); ifd = new TiffIFD(map, detailedEntries); ifd.setLittleEndian(in.isLittleEndian()); ifd.setBigTiff(bigTiff); ifd.setFileOffsetForReading(startOffset); ifd.setSubIFDType(subIFDType); if (readNextOffset) { final long nextOffset = readNextOffset(false); ifd.setNextIFDOffset(nextOffset); in.seek(positionOfNextOffset); // - this "in.seek" provides maximal compatibility with old code (which did not read next IFD offset) // and also with behaviour of this method, when readNextOffset is not requested } } if (TiffTools.BUILT_IN_TIMING && LOGGABLE_DEBUG) { long t2 = debugTime(); LOG.log(System.Logger.Level.TRACE, String.format(Locale.US, "%s read IFD at offset %d: %.3f ms, including %.6f entries + %.6f arrays", getClass().getSimpleName(), startOffset, (t2 - t1) * 1e-6, timeEntries * 1e-6, timeArrays * 1e-6)); } return ifd; } /** * Reads and decodes the tile at the specified position. * Note: the loaded tile is always {@link TiffTile#isSeparated() separated}. * * @param tileIndex position of the file * @return loaded tile. */ public TiffTile readTile(TiffTileIndex tileIndex) throws IOException { TiffTile tile = readEncodedTile(tileIndex); if (tile.isEmpty()) { return tile; } decode(tile); return tile; } public TiffTile readEncodedTile(TiffTileIndex tileIndex) throws IOException { Objects.requireNonNull(tileIndex, "Null tileIndex"); long t1 = debugTime(); final TiffIFD ifd = tileIndex.ifd(); final int index = tileIndex.linearIndex(); // - also checks that tile index is not out of image bounds final long offset = ifd.cachedTileOrStripOffset(index); assert offset >= 0 : "offset " + offset + " was not checked in TiffIFD"; final int byteCount = cachedByteCountWithCompatibilityTrick(ifd, index); /* // Some strange old code, seems to be useless final int rowsPerStrip = ifd.cachedStripSizeY(); final int bytesPerSample = ifd.getBytesPerSampleBasedOnBits(); if (byteCount == ((long) rowsPerStrip * tileSizeX) && bytesPerSample > 1) { byteCount *= bytesPerSample; } if (byteCount >= Integer.MAX_VALUE) { throw new FormatException("Too large tile/strip #" + index + ": " + byteCount + " bytes > 2^31-1"); } */ final TiffTile result = new TiffTile(tileIndex); // - No reasons to put it into the map: this class do not provide access to temporary created map. if (cropTilesToImageBoundaries) { result.cropToMap(true); } // If cropping is disabled, we should not avoid reading extra content of the last strip. // Note the last encoded strip can have actually full strip sizes, // i.e. larger than necessary; this situation is quite possible. if (byteCount == 0 || offset == 0) { if (missingTilesAllowed) { return result; } else { throw new TiffException("Zero tile/strip " + (byteCount == 0 ? "byte-count" : "offset") + " is not allowed in a valid TIFF file (tile " + tileIndex + ")"); } } synchronized (fileLock) { if (offset >= in.length()) { throw new TiffException("Offset of TIFF tile/strip " + offset + " is out of file length (tile " + tileIndex + ")"); // - note: old SCIFIO code allowed such offsets and returned zero-filled tile } TiffTileIO.read(result, in, offset, byteCount); } long t2 = debugTime(); timeReading += t2 - t1; return result; } // Note: result is usually interleaved (RGBRGB...) or monochrome; it is always so in UNCOMPRESSED, LZW, DEFLATE public void decode(TiffTile tile) throws TiffException { Objects.requireNonNull(tile, "Null tile"); long t1 = debugTime(); prepareEncodedTileForDecoding(tile); final TagCompression compression = TagCompression.valueOfCodeOrNull(tile.ifd().getCompressionCode()); TiffCodec codec = null; if (!enforceUseExternalCodec && compression != null) { codec = compression.codec(); // - we are sure that this codec does not require SCIFIO context } TiffCodec.Options options = buildOptions(tile); long t2 = debugTime(); if (codec != null) { options = compression.customizeReading(tile, options); if (USE_LEGACY_UNPACK_BYTES) { options.setInterleaved(true); // - old-style unpackBytes does not "understand" already-separated tiles } if (codec instanceof TiffCodec.Timing timing) { timing.setTiming(TiffTools.BUILT_IN_TIMING && LOGGABLE_DEBUG); timing.clearTiming(); } final byte[] decodedData = codec.decompress(tile.getEncodedData(), options); tile.setPartiallyDecodedData(decodedData); } else { Object externalOptions = buildExternalOptions(tile, options); final byte[] decodedData = decompressExternalFormat(tile, externalOptions); tile.setPartiallyDecodedData(decodedData); } tile.setInterleaved(options.isInterleaved()); long t3 = debugTime(); completeDecoding(tile); long t4 = debugTime(); timeCustomizingDecoding += t2 - t1; timeDecoding += t3 - t2; if (codec instanceof TiffCodec.Timing timing) { timeDecodingMain += timing.timeMain(); timeDecodingBridge += timing.timeBridge(); timeDecodingAdditional += timing.timeAdditional(); } else { timeDecodingMain += t3 - t2; } timeCompleteDecoding += t4 - t3; } public void prepareEncodedTileForDecoding(TiffTile tile) throws TiffException { Objects.requireNonNull(tile, "Null tile"); if (tile.isEmpty()) { // - unlike full decoding, here it is better not to throw exception for empty tile return; } TiffTools.reverseFillOrderIfRequested(tile); TiffIFD ifd = tile.ifd(); final TagCompression compression = ifd.optCompression().orElse(null); if (compression != null && compression.isJpeg()) { final byte[] data = tile.getEncodedData(); final byte[] jpegTable = ifd.getValue(Tags.JPEG_TABLES, byte[].class).orElse(null); // Structure of data: // FF D8 (SOI, start of image) // FF C0 (SOF0, start of frame, or some other marker) // ... // FF D9 (EOI, end of image) // Structure of jpegTable: // FF D8 (SOI, start of image) // FF DB (DQT, define quantization table(s) // ... // FF D9 (EOI, end of image) // From libtiff specification: // When the JPEGTables field is present, it shall contain a valid JPEG // "abbreviated table specification" datastream. This datastream shall begin // with SOI and end with EOI. if (data.length < 2 || data[0] != (byte) 0xFF || data[1] != (byte) 0xD8) { // - the same check is performed inside Java API ImageIO (JPEGImageReaderSpi), // and we prefer to repeat it here for better diagnostics if (compression == TagCompression.JPEG) { throw new TiffException( "Invalid TIFF image: it is declared as JPEG, but the data are not actually JPEG"); } else { throw new UnsupportedTiffFormatException( "Unsupported format of TIFF image: it is declared as \"" + compression.prettyName() + "\", but the data are not actually JPEG"); } } if (jpegTable != null) { if (jpegTable.length <= 4) { throw new TiffException("Too short JPEGTables tag: only " + jpegTable.length + " bytes"); } if ((long) jpegTable.length + (long) data.length - 4 >= Integer.MAX_VALUE) { // - very improbable throw new TiffException( "Too large tile/strip at " + tile.index() + ": JPEG table length " + (jpegTable.length - 2) + " + number of bytes " + (data.length - 2) + " > 2^31-1"); } final byte[] appended = new byte[jpegTable.length + data.length - 4]; appended[0] = (byte) 0xFF; appended[1] = (byte) 0xD8; // - writing SOI System.arraycopy(jpegTable, 2, appended, 2, jpegTable.length - 4); // - skipping both SOI and EOI (2 first and 2 last bytes) from jpegTable System.arraycopy(data, 2, appended, jpegTable.length - 2, data.length - 2); // - skipping SOI (2 first bytes) from main data tile.setEncodedData(appended); } } } /** * Completes decoding tile after decoding by some {@link TiffCodec}. This method is automatically called * at the end of {@link #decode(TiffTile)} method. * *

First of all, this method always rearranges data in the file: if the codec returned * {@link TiffTile#isInterleaved() interleaved} data, this method * {@link TiffTile#separateSamplesIfNecessary() separates} them. * Interleaved data are the standard for internal pixel storage for simple formats like * {@link TagCompression#LZW} and may be returned by complex codecs like {@link TagCompression#JPEG_2000_LOSSLESS}. * *

This method does 3 other corrections for some standard compression algorithms: *

    *
  • {@link TagCompression#UNCOMPRESSED},
  • *
  • {@link TagCompression#LZW},
  • *
  • {@link TagCompression#DEFLATE},
  • *
  • {@link TagCompression#PACK_BITS}.
  • *
* *

1st correction: unpacking. TIFF supports storing pixel samples in any number of bits, * not always divisible by 8, in other words, one pixel sample can occupy non-integer number of bytes. * Most useful from these cases is 1-bit monochrome picture, where 8 pixels are packed into 1 byte. * Sometimes the pictures with 4 bits/pixels (monochrome or with palette) or 3*4=12 bits/pixels (RGB) appear. * You can also meet old RGB 32-bit images with 5+5+5 or 5+6+5 bits/channel.

* *

However, the API of this class represents any image as a sequence of bytes. This method performs * all necessary unpacking so that the result image use an integer number of bytes per each pixel sample (channel). * For example, 1-bit binary image is converted to 8-bit: 0 transformed to 0, 1 to 255; * 4-bit grayscale image is converted by multiplying by 17 (so that maximal possible 4-bit value, 15, * will become maximal possible 8-bit value 255=17*15); 12-bit RGB image is converted to 16-bit * by multiplying each channel by (216−1)/(212−1), etc.

* *

2nd correction: inversion. Some TIFF files use CMYK color space or WhiteIsZero interpretation of grayscale * images, where dark colors are represented by smaller values and bright colors by higher, in particlular, * 0 is white, maximal value (255 for 8-bit) is black.

* *

However, the API of this class suppose that all returned images are RGB, RGBA or usual monochrome. * Complex codecs like JPEG perform necessary conversion themselves, but the simple codecs like * {@link TagCompression#UNCOMPRESSED} do not this correction. This is performed by this method. * For CMYK or WhiteIsZero it means inversion of the pixel samples: v is transformed to MAX−v, * where MAX is the maximal possible value (255 for 8-bit).

* *

3rd correction: conversion of YCbCr color space to usual RGB. It is a rare situation, when * the image is stored as YCbCr, however not in JPEG format, but uncompressed or, for example, as LZW. * This method performs necessary conversion to RGB (but only if the image is exactly 8-bit).

* *

Note: this method never increase number of bytes, necessary for representing a single pixel sample. * It can increase the number of bits per sample, but onty to the nearest greater integer number of * bytes: 1..7 bits are transformed to 8 bits/sample, 9..15 to 16 bits/sample, 17..23 to 24 bits/sample etc. * Thus, this method does not unpack 3-byte samples (to 4-byte) and * does not unpack 16- or 24-bit floating-point formats. These cases * are processed after reading all tiles inside {@link #readSamples(TiffMap, int, int, int, int)} * method, if {@link #isAutoUnpackUnusualPrecisions()} flag is set, or may be performed by external * code with help of {@link TiffTools#unpackUnusualPrecisions(byte[], TiffIFD, int, int, boolean)} method. * *

This method does not allow 5, 6, 7 or greater than 8 bytes/sample * (but 8 bytes/sample is allowed: it is probably double precision).

* * @param tile the tile that should be corrected. */ public void completeDecoding(TiffTile tile) throws TiffException { // scifio.tiff().undifference(tile.getDecodedData(), tile.ifd()); // - this solution requires using SCIFIO context class; it is better to avoid this TiffTools.unsubtractPredictionIfRequested(tile); if (USE_LEGACY_UNPACK_BYTES) { byte[] samples = new byte[tile.map().tileSizeInBytes()]; // TiffTools.unpackBytesLegacy(samples, 0, tile.getDecodedData(), tile.ifd()); // - uncomment this to perform debugging tile.setDecodedData(samples); tile.setInterleaved(false); } else { if (!TiffTools.separateUnpackedSamples(tile)) { if (!TiffTools.separateYCbCrToRGB(tile)) { TiffTools.separateBitsAndInvertValues(tile, autoScaleWhenIncreasingBitDepth, autoCorrectInvertedBrightness); } } } tile.checkDataLengthAlignment(); // - Fully unpacked and separated data must be correctly aligned /* // The following code is equivalent to the analogous code from SCIFIO 0.46.0 and earlier version, // which performed correction of channels with different precision in a rare case PLANAR_CONFIG_SEPARATE. // But it is deprecated: we do not support different number of BYTES in different samples, // and it is checked inside getBytesPerSampleBasedOnBits() method. byte[] samples = tile.getDecodedData(); final TiffTileIndex tileIndex = tile.tileIndex(); final int tileSizeX = tileIndex.tileSizeX(); final int tileSizeY = tileIndex.tileSizeY(); TiffIFD ifd = tile.ifd(); final int samplesPerPixel = ifd.getSamplesPerPixel(); final int planarConfig = ifd.getPlanarConfiguration(); final int bytesPerSample = ifd.getBytesPerSampleBasedOnBits(); final int effectiveChannels = planarConfig == TiffIFD.PLANAR_CONFIG_SEPARATE ? 1 : samplesPerPixel; assert samples.length == ((long) tileSizeX * (long) tileSizeY * bytesPerSample * effectiveChannels); if (planarConfig == TiffIFD.PLANAR_CONFIG_SEPARATE && !ifd.isTiled() && ifd.getSamplesPerPixel() > 1) { final OnDemandLongArray onDemandOffsets = ifd.getOnDemandStripOffsets(); final long[] offsets = onDemandOffsets != null ? null : ifd.getStripOffsets(); final long numberOfStrips = onDemandOffsets != null ? onDemandOffsets.size() : offsets.length; final int channel = (int) (tileIndex.y() % numberOfStrips); int[] differentBytesPerSample = ifd.getBytesPerSample(); if (channel < differentBytesPerSample.length) { final int realBytes = differentBytesPerSample[channel]; if (realBytes != bytesPerSample) { // re-pack pixels to account for differing bits per sample final boolean littleEndian = ifd.isLittleEndian(); final int[] newSamples = new int[samples.length / bytesPerSample]; for (int i = 0; i < newSamples.length; i++) { newSamples[i] = Bytes.toInt(samples, i * realBytes, realBytes, littleEndian); } for (int i = 0; i < newSamples.length; i++) { Bytes.unpack(newSamples[i], samples, i * bytesPerSample, bytesPerSample, littleEndian); } } } } tile.setDecodedData(samples); */ } public TiffMap newMap(TiffIFD ifd) { return new TiffMap(ifd).buildGrid(); // - building grid is useful to perform loops on all tiles } public byte[] readSamples(TiffMap map) throws IOException { return readSamples(map, false); } public byte[] readSamples(TiffMap map, boolean storeTilesInMap) throws IOException { return readSamples(map, 0, 0, map.dimX(), map.dimY(), storeTilesInMap); } /** * Reads samples in byte[] array. * *

Note: you should not change IFD in a parallel thread while calling this method. * * @return loaded samples in a normalized form of byte sequence. */ public byte[] readSamples(TiffMap map, int fromX, int fromY, int sizeX, int sizeY) throws IOException { return readSamples(map, fromX, fromY, sizeX, sizeY, false); } public byte[] readSamples(TiffMap map, int fromX, int fromY, int sizeX, int sizeY, boolean storeTilesInMap) throws IOException { Objects.requireNonNull(map, "Null TIFF map"); long t1 = debugTime(); clearTiming(); TiffTools.checkRequestedArea(fromX, fromY, sizeX, sizeY); // - note: we allow this area to be outside the image final int numberOfChannels = map.numberOfChannels(); final TiffIFD ifd = map.ifd(); final int sizeInBytes = ifd.sizeOfRegion(sizeX, sizeY); assert sizeX >= 0 && sizeY >= 0 : "sizeOfRegion didn't check sizes accurately: " + sizeX + "fromX" + sizeY; byte[] samples = new byte[sizeInBytes]; if (byteFiller != 0) { // - samples array is already zero-filled by Java Arrays.fill(samples, 0, sizeInBytes, byteFiller); } // - important for a case when the requested area is outside the image; // old SCIFIO code did not check this and could return undefined results long t2 = debugTime(); readTiles(map, samples, fromX, fromY, sizeX, sizeY, storeTilesInMap); long t3 = debugTime(); boolean interleave = false; if (interleaveResults) { byte[] newSamples = TiffTools.toInterleavedSamples( samples, numberOfChannels, map.bytesPerSample(), sizeX * sizeY); interleave = newSamples != samples; samples = newSamples; } long t4 = debugTime(); boolean unusualPrecision = false; if (autoUnpackUnusualPrecisions) { byte[] newSamples = TiffTools.unpackUnusualPrecisions( samples, ifd, numberOfChannels, sizeX * sizeY, autoScaleWhenIncreasingBitDepth); unusualPrecision = newSamples != samples; samples = newSamples; // - note: the size of sample array can be increased here! } if (TiffTools.BUILT_IN_TIMING && LOGGABLE_DEBUG) { long t5 = debugTime(); LOG.log(System.Logger.Level.DEBUG, String.format(Locale.US, "%s read %dx%dx%d samples (%.3f MB) in %.3f ms = " + "%.3f initializing + %.3f read/decode " + "(%.3f read, %.3f customize/bit-order, %.3f decode: " + "%.3f decode-main%s, " + "%.3f complete)" + "%s%s, %.3f MB/s", getClass().getSimpleName(), numberOfChannels, sizeX, sizeY, sizeInBytes / 1048576.0, (t5 - t1) * 1e-6, (t2 - t1) * 1e-6, (t3 - t2) * 1e-6, timeReading * 1e-6, timeCustomizingDecoding * 1e-6, timeDecoding * 1e-6, timeDecodingMain * 1e-6, timeDecodingBridge + timeDecodingAdditional > 0 ? String.format(Locale.US, " + %.3f decode-bridge + %.3f decode-additional", timeDecodingBridge * 1e-6, timeDecodingAdditional * 1e-6) : "", timeCompleteDecoding * 1e-6, interleave ? String.format(Locale.US, " + %.3f interleave", (t4 - t3) * 1e-6) : "", unusualPrecision ? String.format(Locale.US, " + %.3f unusual precisions", (t5 - t4) * 1e-6) : "", sizeInBytes / 1048576.0 / ((t5 - t1) * 1e-9))); } return samples; } public Object readJavaArray(TiffMap map) throws IOException { Objects.requireNonNull(map, "Null TIFF map"); return readJavaArray(map, 0, 0, map.dimX(), map.dimY()); } public Object readJavaArray(TiffMap map, int fromX, int fromY, int sizeX, int sizeY) throws IOException { return readJavaArray(map, fromX, fromY, sizeX, sizeY, false); } public Object readJavaArray(TiffMap map, int fromX, int fromY, int sizeX, int sizeY, boolean storeTilesInMap) throws IOException { Objects.requireNonNull(map, "Null TIFF map"); final byte[] samples = readSamples(map, fromX, fromY, sizeX, sizeY, storeTilesInMap); long t1 = debugTime(); final TiffSampleType sampleType = map.sampleType(); if (!autoUnpackUnusualPrecisions && map.bytesPerSample() != sampleType.bytesPerSample()) { throw new IllegalStateException("Cannot convert TIFF pixels, " + map.bytesPerSample() + " bytes/sample, to \"" + sampleType.elementType() + "\" " + sampleType.bytesPerSample() + "-byte Java type: unpacking unusual prevision mode is disabled"); } final Object samplesArray = TiffTools.bytesToJavaArray(samples, sampleType, isLittleEndian()); if (TiffTools.BUILT_IN_TIMING && LOGGABLE_DEBUG) { long t2 = debugTime(); LOG.log(System.Logger.Level.DEBUG, String.format(Locale.US, "%s converted %d bytes (%.3f MB) to %s[] in %.3f ms%s", getClass().getSimpleName(), samples.length, samples.length / 1048576.0, samplesArray.getClass().getComponentType().getSimpleName(), (t2 - t1) * 1e-6, samples == samplesArray ? "" : String.format(Locale.US, " %.3f MB/s", samples.length / 1048576.0 / ((t2 - t1) * 1e-9)))); } return samplesArray; } public Matrix readMatrix(TiffMap map) throws IOException { Objects.requireNonNull(map, "Null TIFF map"); return readMatrix(map, 0, 0, map.dimX(), map.dimY()); } public Matrix readMatrix(TiffMap map, int fromX, int fromY, int sizeX, int sizeY) throws IOException { return readMatrix(map, fromX, fromY, sizeX, sizeY, false); } public Matrix readMatrix( TiffMap map, int fromX, int fromY, int sizeX, int sizeY, boolean storeTilesInMap) throws IOException { final Object samplesArray = readJavaArray(map, fromX, fromY, sizeX, sizeY, storeTilesInMap); return TiffTools.asMatrix(samplesArray, sizeX, sizeY, map.numberOfChannels(), interleaveResults); } @Override public void close() throws IOException { synchronized (fileLock) { in.close(); } } protected Object buildExternalOptions(TiffTile tile, TiffCodec.Options options) throws TiffException { Objects.requireNonNull(tile, "Null tile"); Objects.requireNonNull(options, "Null options"); if (!SCIFIOBridge.isScifioInstalled()) { throw new UnsupportedTiffFormatException("TIFF compression with code " + tile.ifd().getCompressionCode() + " cannot be decompressed"); } return options.toOldStyleOptions(SCIFIOBridge.codecOptionsClass()); } protected byte[] decompressExternalFormat(TiffTile tile, Object externalOptions) throws TiffException { Objects.requireNonNull(tile, "Null tile"); Objects.requireNonNull(externalOptions, "Null externalOptions"); final byte[] encodedData = tile.getEncodedData(); final int compressionCode = tile.ifd().getCompressionCode(); final Object scifio = scifio(); if (scifio == null) { throw new IllegalStateException( "Compression type " + compressionCode + " requires specifying non-null SCIFIO context"); } final Object compression; try { compression = SCIFIOBridge.createTiffCompression(compressionCode); } catch (InvocationTargetException e) { throw new UnsupportedTiffFormatException("TIFF compression code " + compressionCode + " is unknown and is not correctly recognized by the external SCIFIO subsystem", e); } try { return SCIFIOBridge.callDecompress(scifio, compression, encodedData, externalOptions); } catch (InvocationTargetException e) { throw new TiffException(e.getMessage(), e.getCause()); } } Object scifio() { Object scifio = this.scifio; if (scifio == null) { this.scifio = scifio = SCIFIOBridge.createScifioFromContext(context); } return scifio; } private void clearTiming() { timeReading = 0; timeCustomizingDecoding = 0; timeDecoding = 0; timeDecodingMain = 0; timeDecodingBridge = 0; timeDecodingAdditional = 0; timeCompleteDecoding = 0; } // We prefer make this.bigTiff a final field, so we cannot set it outside the constructor private Exception startReading(AtomicBoolean bigTiffReference) { try { synchronized (fileLock) { // - this synchronization is extra, but may become useful // if we will decide to make this method public and called not only from the constructor if (!in.exists()) { return new FileNotFoundException("Input TIFF file" + prettyInName() + " does not exist"); } testHeader(bigTiffReference); return null; } } catch (IOException e) { return e; } } private void testHeader(AtomicBoolean bigTiffReference) throws IOException { final long savedOffset = in.offset(); try { in.seek(0); final long length = in.length(); if (length < MINIMAL_ALLOWED_TIFF_FILE_LENGTH) { // - sometimes we can meet 8-byte "TIFF-files" (or 16-byte "Big-TIFF"), containing only header // and no actual data (for example, results of debugging writing algorithm) throw new TiffException("Too short TIFF file" + prettyInName() + ": only " + length + " bytes (minimum " + MINIMAL_ALLOWED_TIFF_FILE_LENGTH + " bytes are required for valid TIFF)"); } final int endianOne = in.read(); final int endianTwo = in.read(); // byte order must be II or MM final boolean littleEndian = endianOne == TiffTools.FILE_PREFIX_LITTLE_ENDIAN && endianTwo == TiffTools.FILE_PREFIX_LITTLE_ENDIAN; // II final boolean bigEndian = endianOne == TiffTools.FILE_PREFIX_BIG_ENDIAN && endianTwo == TiffTools.FILE_PREFIX_BIG_ENDIAN; // MM if (!littleEndian && !bigEndian) { throw new TiffException("The file" + prettyInName() + " is not TIFF"); } // check magic number (42) in.setLittleEndian(littleEndian); final short magic = in.readShort(); final boolean bigTiff = magic == TiffTools.FILE_BIG_TIFF_MAGIC_NUMBER; bigTiffReference.set(bigTiff); if (magic != TiffTools.FILE_USUAL_MAGIC_NUMBER && magic != TiffTools.FILE_BIG_TIFF_MAGIC_NUMBER) { throw new TiffException("The file" + prettyInName() + " is not TIFF"); } if (bigTiff) { in.seek(8); } readFirstOffsetFromCurrentPosition(false, bigTiff); // - additional check, filling positionOfLastOffset } finally { in.seek(savedOffset); // - for maximal compatibility: in old versions, constructor of this class // guaranteed that file position in the input stream will not change // (that is illogical, because "little-endian" mode was still changed) } } private TiffCodec.Options buildOptions(TiffTile tile) throws TiffException { TiffCodec.Options options = this.codecOptions.clone(); options.setLittleEndian(tile.isLittleEndian()); final int samplesLength = tile.getSizeInBytes(); // - Note: it may be LESS than a usual number of samples in the tile/strip. // Current readEncodedTile() always returns full-size tile without cropping // (see comments inside that method), but the user CAN crop last tile/strip in an external code. // Old SCIFIO code did not detect this situation, in particular, did not distinguish between // last and usual strips in stripped image, and its behaviour could be described by the following assignment: // final int samplesLength = tile.map().tileSizeInBytes(); // For many codecs (like DEFLATE or JPEG) this is not important, but at least // LZWCodec creates result array on the base of options.maxSizeInBytes. // If it will be invalid (too large) value, returned decoded data will be too large, // and this class will throw an exception "data may be lost" in further // tile.completeNumberOfPixels() call. options.setMaxSizeInBytes(Math.max(samplesLength, tile.getStoredDataLength())); options.setInterleaved(true); // - Value "true" is necessary for most codecs, that work with high-level classes (like JPEG or JPEG-2000) and // need to be instructed to interleave results (unlike LZW or DECOMPRESSED, which work with data "as-is" // and suppose that data are interleaved according TIFF format specification). // For JPEG, TagCompression overrides this value to false, because it works faster in this mode. return options; } // Note: this method does not store tile in the tile map. private void readTiles( TiffMap map, byte[] resultSamples, int fromX, int fromY, int sizeX, int sizeY, boolean storeTilesInMap) throws IOException { Objects.requireNonNull(map, "Null TIFF map"); Objects.requireNonNull(resultSamples, "Null result samples"); assert sizeX >= 0 && sizeY >= 0; // Note: we cannot process image larger than 2^31 x 2^31 pixels, // though TIFF supports maximal sizes 2^32 x 2^32 // (IFD.getImageWidth/getImageLength do not allow so large results) if (sizeX == 0 || sizeY == 0) { // - if no pixels are updated, no need to expand the map and to check correct expansion return; } final int mapTileSizeX = map.tileSizeX(); final int mapTileSizeY = map.tileSizeY(); final int bytesPerSample = map.bytesPerSample(); final int numberOfSeparatedPlanes = map.numberOfSeparatedPlanes(); final int samplesPerPixel = map.tileSamplesPerPixel(); final int toX = Math.min(fromX + sizeX, cropTilesToImageBoundaries ? map.dimX() : Integer.MAX_VALUE); final int toY = Math.min(fromY + sizeY, cropTilesToImageBoundaries ? map.dimY() : Integer.MAX_VALUE); // - crop by image sizes to avoid reading unpredictable content of the boundary tiles outside the image final int minXIndex = Math.max(0, divFloor(fromX, mapTileSizeX)); final int minYIndex = Math.max(0, divFloor(fromY, mapTileSizeY)); if (minXIndex >= map.gridTileCountX() || minYIndex >= map.gridTileCountY() || toX < fromX || toY < fromY) { return; } final int maxXIndex = Math.min(map.gridTileCountX() - 1, divFloor(toX - 1, mapTileSizeX)); final int maxYIndex = Math.min(map.gridTileCountY() - 1, divFloor(toY - 1, mapTileSizeY)); if (minYIndex > maxYIndex || minXIndex > maxXIndex) { // - possible when fromX < 0 or fromY < 0 return; } final int tileOneChannelRowSizeInBytes = mapTileSizeX * bytesPerSample; final int samplesOneChannelRowSizeInBytes = sizeX * bytesPerSample; for (int p = 0; p < numberOfSeparatedPlanes; p++) { // - for a rare case PlanarConfiguration=2 (RRR...GGG...BBB...) for (int yIndex = minYIndex; yIndex <= maxYIndex; yIndex++) { final int tileStartY = Math.max(yIndex * mapTileSizeY, fromY); final int fromYInTile = tileStartY % mapTileSizeY; final int yDiff = tileStartY - fromY; for (int xIndex = minXIndex; xIndex <= maxXIndex; xIndex++) { final int tileStartX = Math.max(xIndex * mapTileSizeX, fromX); final int fromXInTile = tileStartX % mapTileSizeX; final int xDiff = tileStartX - fromX; final TiffTile tile = readTile(map.multiplaneIndex(p, xIndex, yIndex)); if (storeTilesInMap) { map.put(tile); } if (tile.isEmpty()) { continue; } if (!tile.isSeparated()) { throw new AssertionError("Illegal behavior of readTile: it returned interleaved tile!"); // - theoretically possible in subclasses } byte[] data = tile.getDecodedData(); final int tileSizeX = tile.getSizeX(); final int tileSizeY = tile.getSizeY(); final int sizeXInTile = Math.min(toX - tileStartX, tileSizeX - fromXInTile); assert sizeXInTile > 0 : "sizeXInTile=" + sizeXInTile; final int sizeYInTile = Math.min(toY - tileStartY, tileSizeY - fromYInTile); assert sizeYInTile > 0 : "sizeYInTile=" + sizeYInTile; final int partSizeXInBytes = sizeXInTile * bytesPerSample; for (int s = 0; s < samplesPerPixel; s++) { int tOffset = (((s * tileSizeY) + fromYInTile) * tileSizeX + fromXInTile) * bytesPerSample; int samplesOffset = (((p + s) * sizeY + yDiff) * sizeX + xDiff) * bytesPerSample; for (int i = 0; i < sizeYInTile; i++) { System.arraycopy(data, tOffset, resultSamples, samplesOffset, partSizeXInBytes); tOffset += tileOneChannelRowSizeInBytes; samplesOffset += samplesOneChannelRowSizeInBytes; } } } } } } private static int cachedByteCountWithCompatibilityTrick(TiffIFD ifd, int index) throws TiffException { final boolean tiled = ifd.hasTileInformation(); final int tag = tiled ? Tags.TILE_BYTE_COUNTS : Tags.STRIP_BYTE_COUNTS; Object value = ifd.get(tag); if (value instanceof long[] byteCounts && byteCounts.length == 1 && byteCounts[0] == (int) byteCounts[0]) { // - possible in a rare case: // we use TiffParser.getIFD to read this IFD, // and this file is Big-TIFF, // and if we set "equal-strip" mode by TiffParser.setAssumeEqualStrips return (int) byteCounts[0]; } return ifd.cachedTileOrStripByteCount(index); } // Unlike AbstractCodec.decompress, this method does not require using "handles" field, annotated as @Parameter // This function is not universal, it cannot be applied to any codec! private String prettyInName() { return prettyFileName(" %s", in); } private long readFirstOffsetFromCurrentPosition(boolean updatePositionOfLastOffset, boolean bigTiff) throws IOException { final long offset = readNextOffset(updatePositionOfLastOffset, true, bigTiff); if (offset == 0) { throw new TiffException("Invalid TIFF" + prettyInName() + ": zero first offset (TIFF must contain at least one IFD!)"); } return offset; } private void skipIFDEntries(long fileLength) throws IOException { final long offset = in.offset(); final int bytesPerEntry = bigTiff ? TiffTools.BIG_TIFF_BYTES_PER_ENTRY : TiffTools.BYTES_PER_ENTRY; final long numberOfEntries = bigTiff ? in.readLong() : in.readUnsignedShort(); if (numberOfEntries < 0 || numberOfEntries > Integer.MAX_VALUE / bytesPerEntry) { throw new TiffException( "Too large number of IFD entries in Big TIFF: " + (numberOfEntries < 0 ? ">= 2^63" : numberOfEntries + "") + " (it is not supported, probably file is broken)"); } final long skippedIFDBytes = numberOfEntries * bytesPerEntry; if (offset + skippedIFDBytes >= fileLength) { throw new TiffException( "Invalid TIFF" + prettyInName() + ": position of next IFD offset " + (offset + skippedIFDBytes) + " after " + numberOfEntries + " entries is outside the file (probably file is broken)"); } in.skipBytes((int) skippedIFDBytes); } private long readNextOffset(boolean updatePositionOfLastOffset) throws IOException { return readNextOffset(updatePositionOfLastOffset, this.requireValidTiff, this.bigTiff); } /** * Read a file offset. For bigTiff, a 64-bit number is read. For other Tiffs, * a 32-bit number is read and possibly adjusted for a possible carry-over * from the previous offset. */ private long readNextOffset(boolean updatePositionOfLastOffset, boolean requireValidTiff, boolean bigTiff) throws IOException { final long fileLength = in.length(); final long filePosition = in.offset(); long offset; if (bigTiff) { offset = in.readLong(); } else { // Below is a deprecated solution // (this "trick" cannot help if a SINGLE image is very large (>2^32): for example, // previous = 8 (1st IFD) and the next is 0x120000000; but it is the mostly typical // problematic situation: for example, very large 1st IFD in SVS file). // // offset = (previous & ~0xffffffffL) | (in.readInt() & 0xffffffffL); // Only adjust the offset if we know that the file is too large for // 32-bit // offsets to be accurate; otherwise, we're making the incorrect // assumption // that IFDs are stored sequentially. // if (offset < previous && offset != 0 && in.length() > Integer.MAX_VALUE) { // offset += 0x100000000L; // } // return offset; offset = (long) in.readInt() & 0xffffffffL; // - in usual TIFF format, offset if 32-bit UNSIGNED value } if (requireValidTiff) { if (offset < 0) { // - possibly in Big-TIFF only throw new TiffException("Invalid TIFF" + prettyInName() + ": negative 64-bit offset " + offset + " at file position " + filePosition + ", probably the file is corrupted"); } if (offset >= fileLength) { throw new TiffException("Invalid TIFF" + prettyInName() + ": offset " + offset + " at file position " + filePosition + " is outside the file, probably the is corrupted"); } } if (updatePositionOfLastOffset) { this.positionOfLastIFDOffset = filePosition; } return offset; } private static Object readIFDValueAtEntryOffset(DataHandle in, TiffIFD.TiffEntry entry) throws IOException { final int type = entry.type(); final int count = entry.valueCount(); final long offset = entry.valueOffset(); LOG.log(System.Logger.Level.TRACE, () -> "Reading entry " + entry.tag() + " from " + offset + "; type=" + type + ", count=" + count); in.seek(offset); switch (type) { case TagTypes.BYTE -> { // 8-bit unsigned integer if (count == 1) { return (short) in.readByte(); } final byte[] bytes = new byte[count]; in.readFully(bytes); // bytes are unsigned, so use shorts final short[] shorts = new short[count]; for (int j = 0; j < count; j++) { shorts[j] = (short) (bytes[j] & 0xff); } return shorts; } case TagTypes.ASCII -> { // 8-bit byte that contain a 7-bit ASCII code; // the last byte must be NUL (binary zero) final byte[] ascii = new byte[count]; in.read(ascii); // count number of null terminators int zeroCount = 0; for (int j = 0; j < count; j++) { if (ascii[j] == 0 || j == count - 1) { zeroCount++; } } // convert character array to array of strings final String[] strings = zeroCount == 1 ? null : new String[zeroCount]; String s = null; int c = 0, index = -1; for (int j = 0; j < count; j++) { if (ascii[j] == 0) { s = new String(ascii, index + 1, j - index - 1, StandardCharsets.UTF_8); index = j; } else if (j == count - 1) { // handle non-null-terminated strings s = new String(ascii, index + 1, j - index, StandardCharsets.UTF_8); } else { s = null; } if (strings != null && s != null) { strings[c++] = s; } } return strings != null ? strings : s != null ? s : ""; } case TagTypes.SHORT -> { // 16-bit (2-byte) unsigned integer if (count == 1) { return in.readUnsignedShort(); } if (OPTIMIZE_READING_IFD_ARRAYS) { final byte[] bytes = readIFDBytes(in, 2 * (long) count); final short[] shorts = TiffTools.bytesToShortArray(bytes, in.isLittleEndian()); final int[] result = new int[count]; for (int j = 0; j < count; j++) { result[j] = shorts[j] & 0xFFFF; } return result; } else { final int[] shorts = new int[count]; for (int j = 0; j < count; j++) { shorts[j] = in.readUnsignedShort(); } return shorts; } } case TagTypes.LONG, TagTypes.IFD -> { // 32-bit (4-byte) unsigned integer if (count == 1) { return in.readInt() & 0xFFFFFFFFL; } if (OPTIMIZE_READING_IFD_ARRAYS) { final byte[] bytes = readIFDBytes(in, 4 * (long) count); final int[] ints = TiffTools.bytesToIntArray(bytes, in.isLittleEndian()); return Arrays.stream(ints).mapToLong(anInt -> anInt & 0xFFFFFFFFL).toArray(); // note: TIFF_LONG is UNSIGNED long } else { final long[] longs = new long[count]; for (int j = 0; j < count; j++) { longs[j] = in.readInt() & 0xFFFFFFFFL; } return longs; } } case TagTypes.LONG8, TagTypes.SLONG8, TagTypes.IFD8 -> { if (count == 1) { return in.readLong(); } if (OPTIMIZE_READING_IFD_ARRAYS) { final byte[] bytes = readIFDBytes(in, 8 * (long) count); return TiffTools.bytesToLongArray(bytes, in.isLittleEndian()); } else { long[] longs = new long[count]; for (int j = 0; j < count; j++) { longs[j] = in.readLong(); } return longs; } } case TagTypes.RATIONAL, TagTypes.SRATIONAL -> { // Two LONGs or SLONGs: the first represents the numerator of a fraction; the second, the denominator if (count == 1) { return new TagRational(in.readInt(), in.readInt()); } final TagRational[] rationals = new TagRational[count]; for (int j = 0; j < count; j++) { rationals[j] = new TagRational(in.readInt(), in.readInt()); } return rationals; } case TagTypes.SBYTE, TagTypes.UNDEFINED -> { // SBYTE: An 8-bit signed (twos-complement) integer // UNDEFINED: An 8-bit byte that may contain anything, // depending on the definition of the field if (count == 1) { return in.readByte(); } final byte[] sbytes = new byte[count]; in.read(sbytes); return sbytes; } case TagTypes.SSHORT -> { // A 16-bit (2-byte) signed (twos-complement) integer if (count == 1) { return in.readShort(); } final short[] sshorts = new short[count]; for (int j = 0; j < count; j++) { sshorts[j] = in.readShort(); } return sshorts; } case TagTypes.SLONG -> { // A 32-bit (4-byte) signed (twos-complement) integer if (count == 1) { return in.readInt(); } final int[] slongs = new int[count]; for (int j = 0; j < count; j++) { slongs[j] = in.readInt(); } return slongs; } case TagTypes.FLOAT -> { // Single precision (4-byte) IEEE format if (count == 1) { return in.readFloat(); } final float[] floats = new float[count]; for (int j = 0; j < count; j++) { floats[j] = in.readFloat(); } return floats; } case TagTypes.DOUBLE -> { // Double precision (8-byte) IEEE format if (count == 1) { return in.readDouble(); } final double[] doubles = new double[count]; for (int j = 0; j < count; j++) { doubles[j] = in.readDouble(); } return doubles; } default -> { final long valueOrOffset = in.readLong(); return new TiffIFD.UnsupportedTypeValue(type, count, valueOrOffset); } } } private static byte[] readIFDBytes(DataHandle in, long length) throws IOException { if (length > Integer.MAX_VALUE) { throw new TiffException("Too large IFD value: " + length + " >= 2^31 bytes"); } byte[] bytes = new byte[(int) length]; in.readFully(bytes); return bytes; } private TiffIFD.TiffEntry readIFDEntry() throws IOException { final int entryTag = in.readUnsignedShort(); final int entryType = in.readUnsignedShort(); final long valueCount = bigTiff ? in.readLong() : ((long) in.readInt()) & 0xFFFFFFFFL; if (valueCount < 0 || valueCount > Integer.MAX_VALUE) { throw new TiffException("Invalid TIFF: very large number of IFD values in array " + (valueCount < 0 ? " >= 2^63" : valueCount + " >= 2^31") + " is not supported"); } final int bytesPerElement = TagTypes.sizeOfType(entryType); // - will be zero for unknown type; in this case we will set valueOffset=in.offset() below final long valueLength = valueCount * (long) bytesPerElement; final int threshold = bigTiff ? 8 : 4; final long valueOffset = valueLength > threshold ? readNextOffset(false) : in.offset(); if (valueOffset < 0) { throw new TiffException("Invalid TIFF: negative offset of IFD values " + valueOffset); } if (valueOffset > in.length() - valueLength) { throw new TiffException("Invalid TIFF: offset of IFD values " + valueOffset + " + total lengths of values " + valueLength + " = " + valueCount + "*" + bytesPerElement + " is outside the file length " + in.length()); } final TiffIFD.TiffEntry result = new TiffIFD.TiffEntry(entryTag, entryType, (int) valueCount, valueOffset); LOG.log(System.Logger.Level.TRACE, () -> String.format( "Reading IFD entry: %s - %s", result, Tags.tiffTagName(result.tag(), true))); return result; } static int divFloor(int a, int b) { assert b > 0; return a >= 0 ? a / b : (a - b + 1) / b; } static String prettyFileName(String format, DataHandle handle) { if (handle == null) { return ""; } Location location = handle.get(); if (location == null) { return ""; } URI uri = location.getURI(); if (uri == null) { return ""; } return format.formatted(uri); } private static long debugTime() { return TiffTools.BUILT_IN_TIMING && LOGGABLE_DEBUG ? System.nanoTime() : 0; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy