/* * #%L * BSD implementations of Bio-Formats readers and writers * %% * Copyright (C) 2005 - 2015 Open Microscopy Environment: * - Board of Regents of the University of Wisconsin-Madison * - Glencoe Software, Inc. * - University of Dundee * %% * 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% */ package loci.formats.tiff; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import loci.common.ByteArrayHandle; import loci.common.Constants; import loci.common.DataTools; import loci.common.RandomAccessInputStream; import loci.common.Region; import loci.common.enumeration.EnumException; import loci.formats.FormatException; import loci.formats.codec.CodecOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Parses TIFF data from an input source. * * @author Curtis Rueden ctrueden at wisc.edu * @author Eric Kjellman egkjellman at wisc.edu * @author Melissa Linkert melissa at glencoesoftware.com * @author Chris Allan callan at blackcat.ca */ public class TiffParser { // -- Constants -- private static final Logger LOGGER = LoggerFactory.getLogger(TiffParser.class); // -- Fields -- /** Input source from which to parse TIFF data. */ protected transient RandomAccessInputStream in; /** Cached tile buffer to avoid re-allocations when reading tiles. */ private byte[] cachedTileBuffer; /** Whether or not the TIFF file contains BigTIFF data. */ private boolean bigTiff; /** Whether or not 64-bit offsets are used for non-BigTIFF files. */ private boolean fakeBigTiff = false; private boolean ycbcrCorrection = true; private boolean equalStrips = false; private boolean doCaching; /** Cached list of IFDs in the current file. */ private IFDList ifdList; /** Cached first IFD in the current file. */ private IFD firstIFD; private int ifdCount = 0; /** Codec options to be used when decoding compressed pixel data. */ private CodecOptions codecOptions = CodecOptions.getDefaultOptions(); // -- Constructors -- /** Constructs a new TIFF parser from the given file name. */ public TiffParser(String filename) throws IOException { this(new RandomAccessInputStream(filename)); } /** Constructs a new TIFF parser from the given input source. */ public TiffParser(RandomAccessInputStream in) { this.in = in; doCaching = true; try { long fp = in.getFilePointer(); checkHeader(); in.seek(fp); } catch (IOException e) { } } // -- TiffParser methods -- /** * Sets whether or not to assume that strips are of equal size. * @param equalStrips Whether or not the strips are of equal size. */ public void setAssumeEqualStrips(boolean equalStrips) { this.equalStrips = equalStrips; } /** * Sets the codec options to be used when decompressing pixel data. * @param codecOptions Codec options to use. */ public void setCodecOptions(CodecOptions codecOptions) { this.codecOptions = codecOptions; } /** * Retrieves the current set of codec options being used to decompress pixel * data. * @return See above. */ public CodecOptions getCodecOptions() { return codecOptions; } /** Sets whether or not IFD entries should be cached. */ public void setDoCaching(boolean doCaching) { this.doCaching = doCaching; } /** Sets whether or not 64-bit offsets are used for non-BigTIFF files. */ public void setUse64BitOffsets(boolean use64Bit) { fakeBigTiff = use64Bit; } /** Sets whether or not YCbCr color correction is allowed. */ public void setYCbCrCorrection(boolean correctionAllowed) { ycbcrCorrection = correctionAllowed; } /** Gets the stream from which TIFF data is being parsed. */ public RandomAccessInputStream getStream() { return in; } /** Tests this stream to see if it represents a TIFF file. */ public boolean isValidHeader() { try { return checkHeader() != null; } catch (IOException e) { return false; } } /** * Checks the TIFF header. * * @return true if little-endian, * false if big-endian, * or null if not a TIFF. */ public Boolean checkHeader() throws IOException { if (in.length() < 4) return null; // byte order must be II or MM in.seek(0); int endianOne = in.read(); int endianTwo = in.read(); boolean littleEndian = endianOne == TiffConstants.LITTLE && endianTwo == TiffConstants.LITTLE; // II boolean bigEndian = endianOne == TiffConstants.BIG && endianTwo == TiffConstants.BIG; // MM if (!littleEndian && !bigEndian) return null; // check magic number (42) in.order(littleEndian); short magic = in.readShort(); bigTiff = magic == TiffConstants.BIG_TIFF_MAGIC_NUMBER; if (magic != TiffConstants.MAGIC_NUMBER && magic != TiffConstants.BIG_TIFF_MAGIC_NUMBER) { return null; } return new Boolean(littleEndian); } /** Returns whether or not the current TIFF file contains BigTIFF data. */ public boolean isBigTiff() { return bigTiff; } // -- TiffParser methods - IFD parsing -- /** Returns all IFDs in the file. */ public IFDList getIFDs() throws IOException { if (ifdList != null) return ifdList; long[] offsets = getIFDOffsets(); IFDList ifds = new IFDList(); for (long offset : offsets) { IFD ifd = getIFD(offset); if (ifd == null) continue; if (ifd.containsKey(IFD.IMAGE_WIDTH)) ifds.add(ifd); long[] subOffsets = null; try { if (!doCaching && ifd.containsKey(IFD.SUB_IFD)) { fillInIFD(ifd); } subOffsets = ifd.getIFDLongArray(IFD.SUB_IFD); } catch (FormatException e) { } if (subOffsets != null) { for (long subOffset : subOffsets) { IFD sub = getIFD(subOffset); if (sub != null) { ifds.add(sub); } } } } if (doCaching) ifdList = ifds; return ifds; } /** Returns thumbnail IFDs. */ public IFDList getThumbnailIFDs() throws IOException { IFDList ifds = getIFDs(); IFDList thumbnails = new IFDList(); for (IFD ifd : ifds) { Number subfile = (Number) ifd.getIFDValue(IFD.NEW_SUBFILE_TYPE); int subfileType = subfile == null ? 0 : subfile.intValue(); if (subfileType == 1) { thumbnails.add(ifd); } } return thumbnails; } /** Returns non-thumbnail IFDs. */ public IFDList getNonThumbnailIFDs() throws IOException { IFDList ifds = getIFDs(); IFDList nonThumbs = new IFDList(); for (IFD ifd : ifds) { Number subfile = (Number) ifd.getIFDValue(IFD.NEW_SUBFILE_TYPE); int subfileType = subfile == null ? 0 : subfile.intValue(); if (subfileType != 1 || ifds.size() <= 1) { nonThumbs.add(ifd); } } return nonThumbs; } /** Returns EXIF IFDs. */ public IFDList getExifIFDs() throws FormatException, IOException { IFDList ifds = getIFDs(); IFDList exif = new IFDList(); for (IFD ifd : ifds) { long offset = ifd.getIFDLongValue(IFD.EXIF, 0); if (offset != 0) { IFD exifIFD = getIFD(offset); if (exifIFD != null) { exif.add(exifIFD); } } } return exif; } /** Gets the offsets to every IFD in the file. */ public long[] getIFDOffsets() throws IOException { // check TIFF header int bytesPerEntry = bigTiff ? TiffConstants.BIG_TIFF_BYTES_PER_ENTRY : TiffConstants.BYTES_PER_ENTRY; final List<Long> offsets = new ArrayList<Long>(); long offset = getFirstOffset(); while (offset > 0 && offset < in.length()) { in.seek(offset); offsets.add(offset); int nEntries = bigTiff ? (int) in.readLong() : in.readUnsignedShort(); in.skipBytes(nEntries * bytesPerEntry); offset = getNextOffset(offset); } long[] f = new long[offsets.size()]; for (int i=0; i<f.length; i++) { f[i] = offsets.get(i).longValue(); } ifdCount = f.length; return f; } /** * Gets the first IFD within the TIFF file, or null * if the input source is not a valid TIFF file. */ public IFD getFirstIFD() throws IOException { if (firstIFD != null) return firstIFD; long offset = getFirstOffset(); IFD ifd = getIFD(offset); if (doCaching) firstIFD = ifd; return ifd; } /** * Retrieve a given entry from the first IFD in the stream. * * @param tag the tag of the entry to be retrieved. * @return an object representing the entry's fields. * @throws IOException when there is an error accessing the stream. * @throws IllegalArgumentException when the tag number is unknown. */ // TODO : Try to remove this method. It is only being used by // loci.formats.in.MetamorphReader. public TiffIFDEntry getFirstIFDEntry(int tag) throws IOException { // Get the offset of the first IFD long offset = getFirstOffset(); if (offset < 0) return null; // The following loosely resembles the logic of getIFD()... in.seek(offset); long numEntries = bigTiff ? in.readLong() : in.readUnsignedShort(); for (int i = 0; i < numEntries; i++) { in.seek(offset + // The beginning of the IFD (bigTiff ? 8 : 2) + // The width of the initial numEntries field (bigTiff ? TiffConstants.BIG_TIFF_BYTES_PER_ENTRY : TiffConstants.BYTES_PER_ENTRY) * i); TiffIFDEntry entry = readTiffIFDEntry(); if (entry.getTag() == tag) { return entry; } } throw new IllegalArgumentException("Unknown tag: " + tag); } /** * Gets offset to the first IFD, or -1 if stream is not TIFF. */ public long getFirstOffset() throws IOException { Boolean header = checkHeader(); if (header == null) return -1; if (bigTiff) in.skipBytes(4); return getNextOffset(0); } /** Gets the IFD stored at the given offset. */ public IFD getIFD(long offset) throws IOException { if (offset < 0 || offset >= in.length()) return null; IFD ifd = new IFD(); // save little-endian flag to internal LITTLE_ENDIAN tag ifd.put(new Integer(IFD.LITTLE_ENDIAN), new Boolean(in.isLittleEndian())); ifd.put(new Integer(IFD.BIG_TIFF), new Boolean(bigTiff)); // read in directory entries for this IFD LOGGER.trace("getIFDs: seeking IFD at {}", offset); in.seek(offset); long numEntries = bigTiff ? in.readLong() : in.readUnsignedShort(); LOGGER.trace("getIFDs: {} directory entries to read", numEntries); if (numEntries == 0 || numEntries == 1) return ifd; int bytesPerEntry = bigTiff ? TiffConstants.BIG_TIFF_BYTES_PER_ENTRY : TiffConstants.BYTES_PER_ENTRY; int baseOffset = bigTiff ? 8 : 2; for (int i=0; i<numEntries; i++) { in.seek(offset + baseOffset + bytesPerEntry * i); TiffIFDEntry entry = null; try { entry = readTiffIFDEntry(); } catch (EnumException e) { LOGGER.debug("", e); } if (entry == null) break; int count = entry.getValueCount(); int tag = entry.getTag(); long pointer = entry.getValueOffset(); int bpe = entry.getType().getBytesPerElement(); if (count < 0 || bpe <= 0) { // invalid data in.skipBytes(bytesPerEntry - 4 - (bigTiff ? 8 : 4)); continue; } Object value = null; long inputLen = in.length(); if (count * bpe + pointer > inputLen) { int oldCount = count; count = (int) ((inputLen - pointer) / bpe); LOGGER.trace("getIFDs: truncated {} array elements for tag {}", (oldCount - count), tag); if (count < 0) count = oldCount; } if (count < 0 || count > in.length()) break; if (pointer != in.getFilePointer() && !doCaching) { value = entry; } else value = getIFDValue(entry); if (value != null && !ifd.containsKey(new Integer(tag))) { ifd.put(new Integer(tag), value); } } long newOffset =offset + baseOffset + bytesPerEntry * numEntries; if (newOffset < in.length()) { in.seek(newOffset); } else { in.seek(in.length()); } return ifd; } /** Fill in IFD entries that are stored at an arbitrary offset. */ public void fillInIFD(IFD ifd) throws IOException { HashSet<TiffIFDEntry> entries = new HashSet<TiffIFDEntry>(); for (Object key : ifd.keySet()) { if (ifd.get(key) instanceof TiffIFDEntry) { entries.add((TiffIFDEntry) ifd.get(key)); } } for (TiffIFDEntry entry : entries) { if ((entry.getValueCount() < 10 * 1024 * 1024 || entry.getTag() < 32768) && entry.getTag() != IFD.COLOR_MAP) { ifd.put(new Integer(entry.getTag()), getIFDValue(entry)); } } } /** Retrieve the value corresponding to the given TiffIFDEntry. */ public Object getIFDValue(TiffIFDEntry entry) throws IOException { IFDType type = entry.getType(); int count = entry.getValueCount(); long offset = entry.getValueOffset(); LOGGER.trace("Reading entry {} from {}; type={}, count={}", new Object[] {entry.getTag(), offset, type, count}); if (offset >= in.length()) { return null; } if (offset != in.getFilePointer()) { if (fakeBigTiff && (offset < 0 || offset > in.getFilePointer())) { offset &= 0xffffffffL; offset += 0x100000000L; } in.seek(offset); } if (type == IFDType.BYTE) { // 8-bit unsigned integer if (count == 1) return new Short(in.readByte()); byte[] bytes = new byte[count]; in.readFully(bytes); // bytes are unsigned, so use shorts short[] shorts = new short[count]; for (int j=0; j<count; j++) shorts[j] = (short) (bytes[j] & 0xff); return shorts; } else if (type == IFDType.ASCII) { // 8-bit byte that contain a 7-bit ASCII code; // the last byte must be NUL (binary zero) byte[] ascii = new byte[count]; in.read(ascii); // count number of null terminators int nullCount = 0; for (int j=0; j<count; j++) { if (ascii[j] == 0 || j == count - 1) nullCount++; } // convert character array to array of strings String[] strings = nullCount == 1 ? null : new String[nullCount]; String s = null; int c = 0, ndx = -1; for (int j=0; j<count; j++) { if (ascii[j] == 0) { s = new String(ascii, ndx + 1, j - ndx - 1, Constants.ENCODING); ndx = j; } else if (j == count - 1) { // handle non-null-terminated strings s = new String(ascii, ndx + 1, j - ndx, Constants.ENCODING); } else s = null; if (strings != null && s != null) strings[c++] = s; } return strings == null ? (Object) s : strings; } else if (type == IFDType.SHORT) { // 16-bit (2-byte) unsigned integer if (count == 1) return new Integer(in.readUnsignedShort()); int[] shorts = new int[count]; for (int j=0; j<count; j++) { shorts[j] = in.readUnsignedShort(); } return shorts; } else if (type == IFDType.LONG || type == IFDType.IFD) { // 32-bit (4-byte) unsigned integer if (count == 1) return new Long(in.readInt()); long[] longs = new long[count]; for (int j=0; j<count; j++) { if (in.getFilePointer() + 4 <= in.length()) { longs[j] = in.readInt(); } } return longs; } else if (type == IFDType.LONG8 || type == IFDType.SLONG8 || type == IFDType.IFD8) { if (count == 1) return new Long(in.readLong()); long[] longs = null; if (equalStrips && (entry.getTag() == IFD.STRIP_BYTE_COUNTS || entry.getTag() == IFD.TILE_BYTE_COUNTS)) { longs = new long[1]; longs[0] = in.readLong(); } else if (entry.getTag() == IFD.STRIP_OFFSETS || entry.getTag() == IFD.TILE_OFFSETS || entry.getTag() == IFD.STRIP_BYTE_COUNTS || entry.getTag() == IFD.TILE_BYTE_COUNTS) { OnDemandLongArray offsets = new OnDemandLongArray(in); offsets.setSize(count); return offsets; } else { longs = new long[count]; for (int j=0; j<count; j++) longs[j] = in.readLong(); } return longs; } else if (type == IFDType.RATIONAL || type == IFDType.SRATIONAL) { // Two LONGs or SLONGs: the first represents the numerator // of a fraction; the second, the denominator if (count == 1) return new TiffRational(in.readInt(), in.readInt()); TiffRational[] rationals = new TiffRational[count]; for (int j=0; j<count; j++) { rationals[j] = new TiffRational(in.readInt(), in.readInt()); } return rationals; } else if (type == IFDType.SBYTE || type == IFDType.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 new Byte(in.readByte()); byte[] sbytes = new byte[count]; in.read(sbytes); return sbytes; } else if (type == IFDType.SSHORT) { // A 16-bit (2-byte) signed (twos-complement) integer if (count == 1) return new Short(in.readShort()); short[] sshorts = new short[count]; for (int j=0; j<count; j++) sshorts[j] = in.readShort(); return sshorts; } else if (type == IFDType.SLONG) { // A 32-bit (4-byte) signed (twos-complement) integer if (count == 1) return new Integer(in.readInt()); int[] slongs = new int[count]; for (int j=0; j<count; j++) slongs[j] = in.readInt(); return slongs; } else if (type == IFDType.FLOAT) { // Single precision (4-byte) IEEE format if (count == 1) return new Float(in.readFloat()); float[] floats = new float[count]; for (int j=0; j<count; j++) floats[j] = in.readFloat(); return floats; } else if (type == IFDType.DOUBLE) { // Double precision (8-byte) IEEE format if (count == 1) return new Double(in.readDouble()); double[] doubles = new double[count]; for (int j=0; j<count; j++) { doubles[j] = in.readDouble(); } return doubles; } return null; } /** Convenience method for obtaining a stream's first ImageDescription. */ public String getComment() throws IOException { IFD firstIFD = getFirstIFD(); if (firstIFD == null) { return null; } fillInIFD(firstIFD); return firstIFD.getComment(); } /** * Retrieve the color map associated with the given IFD. */ public int[] getColorMap(IFD ifd) throws IOException { Object map = ifd.get(IFD.COLOR_MAP); if (map == null) { return null; } int[] colorMap = null; if (map instanceof TiffIFDEntry) { colorMap = (int[]) getIFDValue((TiffIFDEntry) map); } else if (map instanceof int[]) { colorMap = (int[]) map; } return colorMap; } // -- TiffParser methods - image reading -- public byte[] getTile(IFD ifd, byte[] buf, int row, int col) throws FormatException, IOException { byte[] jpegTable = (byte[]) ifd.getIFDValue(IFD.JPEG_TABLES); codecOptions.interleaved = true; codecOptions.littleEndian = ifd.isLittleEndian(); long tileWidth = ifd.getTileWidth(); long tileLength = ifd.getTileLength(); int samplesPerPixel = ifd.getSamplesPerPixel(); int planarConfig = ifd.getPlanarConfiguration(); TiffCompression compression = ifd.getCompression(); long numTileCols = ifd.getTilesPerRow(); int pixel = ifd.getBytesPerSample()[0]; int effectiveChannels = planarConfig == 2 ? 1 : samplesPerPixel; if (ifd.get(IFD.STRIP_BYTE_COUNTS) instanceof OnDemandLongArray) { OnDemandLongArray counts = (OnDemandLongArray) ifd.get(IFD.STRIP_BYTE_COUNTS); if (counts != null && counts.getStream() == null) { counts.setStream(in); } } if (ifd.get(IFD.TILE_BYTE_COUNTS) instanceof OnDemandLongArray) { OnDemandLongArray counts = (OnDemandLongArray) ifd.get(IFD.TILE_BYTE_COUNTS); if (counts != null && counts.getStream() == null) { counts.setStream(in); } } long[] stripByteCounts = ifd.getStripByteCounts(); long[] rowsPerStrip = ifd.getRowsPerStrip(); int offsetIndex = (int) (row * numTileCols + col); int countIndex = offsetIndex; if (equalStrips) { countIndex = 0; } if (stripByteCounts[countIndex] == (rowsPerStrip[0] * tileWidth) && pixel > 1) { stripByteCounts[countIndex] *= pixel; } else if (stripByteCounts[countIndex] < 0 && countIndex > 0) { LOGGER.debug("byte count #{} was {}; correcting to {}", countIndex, stripByteCounts[countIndex], stripByteCounts[countIndex - 1]); stripByteCounts[countIndex] = stripByteCounts[countIndex - 1]; } long stripOffset = 0; long nStrips = 0; if (ifd.getOnDemandStripOffsets() != null) { OnDemandLongArray stripOffsets = ifd.getOnDemandStripOffsets(); if (stripOffsets.getStream() == null) { stripOffsets.setStream(in); } stripOffset = stripOffsets.get(offsetIndex); nStrips = stripOffsets.size(); } else { long[] stripOffsets = ifd.getStripOffsets(); stripOffset = stripOffsets[offsetIndex]; nStrips = stripOffsets.length; } int size = (int) (tileWidth * tileLength * pixel * effectiveChannels); if (buf == null) buf = new byte[size]; if (stripByteCounts[countIndex] == 0 || stripOffset >= in.length()) { return buf; } byte[] tile = new byte[(int) stripByteCounts[countIndex]]; LOGGER.debug("Reading tile Length {} Offset {}", tile.length, stripOffset); in.seek(stripOffset); in.read(tile); codecOptions.maxBytes = (int) Math.max(size, tile.length); codecOptions.ycbcr = ifd.getPhotometricInterpretation() == PhotoInterp.Y_CB_CR && ifd.getIFDIntValue(IFD.Y_CB_CR_SUB_SAMPLING) == 1 && ycbcrCorrection; if (jpegTable != null) { byte[] q = new byte[jpegTable.length + tile.length - 4]; System.arraycopy(jpegTable, 0, q, 0, jpegTable.length - 2); System.arraycopy(tile, 2, q, jpegTable.length - 2, tile.length - 2); tile = compression.decompress(q, codecOptions); } else tile = compression.decompress(tile, codecOptions); TiffCompression.undifference(tile, ifd); unpackBytes(buf, 0, tile, ifd); if (planarConfig == 2 && !ifd.isTiled() && ifd.getSamplesPerPixel() > 1) { int channel = (int) (row % nStrips); if (channel < ifd.getBytesPerSample().length) { int realBytes = ifd.getBytesPerSample()[channel]; if (realBytes != pixel) { // re-pack pixels to account for differing bits per sample boolean littleEndian = ifd.isLittleEndian(); int[] samples = new int[buf.length / pixel]; for (int i=0; i<samples.length; i++) { samples[i] = DataTools.bytesToInt(buf, i * realBytes, realBytes, littleEndian); } for (int i=0; i<samples.length; i++) { DataTools.unpackBytes( samples[i], buf, i * pixel, pixel, littleEndian); } } } } return buf; } public byte[] getSamples(IFD ifd, byte[] buf) throws FormatException, IOException { long width = ifd.getImageWidth(); long length = ifd.getImageLength(); return getSamples(ifd, buf, 0, 0, width, length); } public byte[] getSamples(IFD ifd, byte[] buf, int x, int y, long width, long height) throws FormatException, IOException { return getSamples(ifd, buf, x, y, width, height, 0, 0); } public byte[] getSamples(IFD ifd, byte[] buf, int x, int y, long width, long height, int overlapX, int overlapY) throws FormatException, IOException { LOGGER.trace("parsing IFD entries"); // get internal non-IFD entries boolean littleEndian = ifd.isLittleEndian(); in.order(littleEndian); // get relevant IFD entries int samplesPerPixel = ifd.getSamplesPerPixel(); long tileWidth = ifd.getTileWidth(); long tileLength = ifd.getTileLength(); if (tileLength <= 0) { LOGGER.trace("Tile length is {}; setting it to {}", tileLength, height); tileLength = height; } long numTileRows = ifd.getTilesPerColumn(); long numTileCols = ifd.getTilesPerRow(); PhotoInterp photoInterp = ifd.getPhotometricInterpretation(); int planarConfig = ifd.getPlanarConfiguration(); int pixel = ifd.getBytesPerSample()[0]; int effectiveChannels = planarConfig == 2 ? 1 : samplesPerPixel; if (LOGGER.isTraceEnabled()) { ifd.printIFD(); } if (width * height > Integer.MAX_VALUE) { throw new FormatException("Sorry, ImageWidth x ImageLength > " + Integer.MAX_VALUE + " is not supported (" + width + " x " + height + ")"); } if (width * height * effectiveChannels * pixel > Integer.MAX_VALUE) { throw new FormatException("Sorry, ImageWidth x ImageLength x " + "SamplesPerPixel x BitsPerSample > " + Integer.MAX_VALUE + " is not supported (" + width + " x " + height + " x " + samplesPerPixel + " x " + (pixel * 8) + ")"); } // casting to int is safe because we have already determined that // width * height is less than Integer.MAX_VALUE int numSamples = (int) (width * height); // read in image strips LOGGER.trace("reading image data (samplesPerPixel={}; numSamples={})", samplesPerPixel, numSamples); TiffCompression compression = ifd.getCompression(); if (compression == TiffCompression.JPEG_2000 || compression == TiffCompression.JPEG_2000_LOSSY) { codecOptions = compression.getCompressionCodecOptions(ifd, codecOptions); } else codecOptions = compression.getCompressionCodecOptions(ifd); codecOptions.interleaved = true; codecOptions.littleEndian = ifd.isLittleEndian(); long imageLength = ifd.getImageLength(); long[] stripOffsets = null; if (ifd.getOnDemandStripOffsets() != null) { OnDemandLongArray offsets = ifd.getOnDemandStripOffsets(); if (offsets.getStream() == null) { offsets.setStream(in); } stripOffsets = offsets.toArray(); } else { stripOffsets = ifd.getStripOffsets(); } if (ifd.get(IFD.STRIP_BYTE_COUNTS) instanceof OnDemandLongArray) { OnDemandLongArray counts = (OnDemandLongArray) ifd.get(IFD.STRIP_BYTE_COUNTS); if (counts != null && counts.getStream() == null) { counts.setStream(in); } } if (ifd.get(IFD.TILE_BYTE_COUNTS) instanceof OnDemandLongArray) { OnDemandLongArray counts = (OnDemandLongArray) ifd.get(IFD.TILE_BYTE_COUNTS); if (counts != null && counts.getStream() == null) { counts.setStream(in); } } long[] stripByteCounts = ifd.getStripByteCounts(); // special case: if we only need one tile, and that tile doesn't need // any special handling, then we can just read it directly and return if (effectiveChannels == 1 && (ifd.getBitsPerSample()[0] % 8) == 0 && photoInterp != PhotoInterp.WHITE_IS_ZERO && photoInterp != PhotoInterp.CMYK && photoInterp != PhotoInterp.Y_CB_CR && compression == TiffCompression.UNCOMPRESSED && numTileRows * numTileCols == 1 && stripOffsets != null && stripByteCounts != null && in.length() >= stripOffsets[0] + stripByteCounts[0]) { long column = x / tileWidth; int firstTile = (int) ((y / tileLength) * numTileCols + column); int lastTile = (int) (((y + height) / tileLength) * numTileCols + column); lastTile = (int) Math.min(lastTile, stripOffsets.length - 1); if (planarConfig == 2) { lastTile = stripOffsets.length - 1; } int offset = 0; for (int tile=firstTile; tile<=lastTile; tile++) { long byteCount = equalStrips ? stripByteCounts[0] : stripByteCounts[tile]; if (byteCount == numSamples && pixel > 1) { byteCount *= pixel; } if (stripOffsets[tile] < in.length()) { in.seek(stripOffsets[tile]); } else { continue; } if (width == tileWidth && height == imageLength) { // we want to entire tile, so just read the whole thing directly int len = (int) Math.min(buf.length - offset, byteCount); in.read(buf, offset, len); offset += len; } else { // we only want a piece of the tile, so read each row separately // this is especially necessary for large single-tile images int bpp = ifd.getBitsPerSample()[0] / 8; in.skipBytes((int) (y * bpp * tileWidth)); for (int row=0; row<height; row++) { in.skipBytes(x * bpp); int len = (int) Math.min(buf.length - offset, width * bpp); if (len > 0) { in.read(buf, offset, len); offset += len; int skip = (int) (bpp * (tileWidth - x - width)); if (skip + in.getFilePointer() < in.length()) { in.skipBytes(skip); } } else { break; } } } } return buf; } long nrows = numTileRows; if (planarConfig == 2) numTileRows *= samplesPerPixel; Region imageBounds = new Region(x, y, (int) width, (int) height); int endX = (int) width + x; int endY = (int) height + y; long w = tileWidth; long h = tileLength; int rowLen = pixel * (int) w;//tileWidth; int tileSize = (int) (rowLen * h);//tileLength); int planeSize = (int) (width * height * pixel); int outputRowLen = (int) (pixel * width); int bufferSizeSamplesPerPixel = samplesPerPixel; if (ifd.getPlanarConfiguration() == 2) bufferSizeSamplesPerPixel = 1; int bpp = ifd.getBytesPerSample()[0]; int bufferSize = (int) tileWidth * (int) tileLength * bufferSizeSamplesPerPixel * bpp; cachedTileBuffer = new byte[bufferSize]; Region tileBounds = new Region(0, 0, (int) tileWidth, (int) tileLength); for (int row=0; row<numTileRows; row++) { // make the first row shorter to account for row overlap if (row == 0) { tileBounds.height = (int) (tileLength - overlapY); } for (int col=0; col<numTileCols; col++) { // make the first column narrower to account for column overlap if (col == 0) { tileBounds.width = (int) (tileWidth - overlapX); } tileBounds.x = col * (int) (tileWidth - overlapX); tileBounds.y = row * (int) (tileLength - overlapY); if (planarConfig == 2) { tileBounds.y = (int) ((row % nrows) * (tileLength - overlapY)); } if (!imageBounds.intersects(tileBounds)) continue; getTile(ifd, cachedTileBuffer, row, col); // adjust tile bounds, if necessary int tileX = (int) Math.max(tileBounds.x, x); int tileY = (int) Math.max(tileBounds.y, y); int realX = tileX % (int) (tileWidth - overlapX); int realY = tileY % (int) (tileLength - overlapY); int twidth = (int) Math.min(endX - tileX, tileWidth - realX); if (twidth <= 0) { twidth = (int) Math.max(endX - tileX, tileWidth - realX); } int theight = (int) Math.min(endY - tileY, tileLength - realY); if (theight <= 0) { theight = (int) Math.max(endY - tileY, tileLength - realY); } // copy appropriate portion of the tile to the output buffer int copy = pixel * twidth; realX *= pixel; realY *= rowLen; for (int q=0; q<effectiveChannels; q++) { int src = (int) (q * tileSize) + realX + realY; int dest = (int) (q * planeSize) + pixel * (tileX - x) + outputRowLen * (tileY - y); if (planarConfig == 2) dest += (planeSize * (row / nrows)); // copying the tile directly will only work if there is no overlap; // otherwise, we may be overwriting a previous tile // (or the current tile may be overwritten by a subsequent tile) if (rowLen == outputRowLen && overlapX == 0 && overlapY == 0) { System.arraycopy(cachedTileBuffer, src, buf, dest, copy * theight); } else { for (int tileRow=0; tileRow<theight; tileRow++) { System.arraycopy(cachedTileBuffer, src, buf, dest, copy); src += rowLen; dest += outputRowLen; } } } } } return buf; } // -- Utility methods - byte stream decoding -- /** * Extracts pixel information from the given byte array according to the * bits per sample, photometric interpretation and color map IFD directory * entry values, and the specified byte ordering. * No error checking is performed. */ public static void unpackBytes(byte[] samples, int startIndex, byte[] bytes, IFD ifd) throws FormatException { boolean planar = ifd.getPlanarConfiguration() == 2; TiffCompression compression = ifd.getCompression(); PhotoInterp photoInterp = ifd.getPhotometricInterpretation(); if (compression == TiffCompression.JPEG) photoInterp = PhotoInterp.RGB; int[] bitsPerSample = ifd.getBitsPerSample(); int nChannels = bitsPerSample.length; int sampleCount = (int) (((long) 8 * bytes.length) / bitsPerSample[0]); if (photoInterp == PhotoInterp.Y_CB_CR) sampleCount *= 3; if (planar) { nChannels = 1; } else { sampleCount /= nChannels; } LOGGER.trace( "unpacking {} samples (startIndex={}; totalBits={}; numBytes={})", new Object[] {sampleCount, startIndex, nChannels * bitsPerSample[0], bytes.length}); long imageWidth = ifd.getImageWidth(); long imageHeight = ifd.getImageLength(); int bps0 = bitsPerSample[0]; int numBytes = ifd.getBytesPerSample()[0]; int nSamples = samples.length / (nChannels * numBytes); boolean noDiv8 = bps0 % 8 != 0; boolean bps8 = bps0 == 8; boolean bps16 = bps0 == 16; boolean littleEndian = ifd.isLittleEndian(); // Hyper optimisation that takes any 8-bit or 16-bit data, where there is // only one channel, the source byte buffer's size is less than or equal to // that of the destination buffer and for which no special unpacking is // required and performs a simple array copy. Over the course of reading // semi-large datasets this can save **billions** of method calls. // Wed Aug 5 19:04:59 BST 2009 // Chris Allan <callan@glencoesoftware.com> if ((bps8 || bps16) && bytes.length <= samples.length && nChannels == 1 && photoInterp != PhotoInterp.WHITE_IS_ZERO && photoInterp != PhotoInterp.CMYK && photoInterp != PhotoInterp.Y_CB_CR) { System.arraycopy(bytes, 0, samples, 0, bytes.length); return; } long maxValue = (long) Math.pow(2, bps0) - 1; if (photoInterp == PhotoInterp.CMYK) maxValue = Integer.MAX_VALUE; int skipBits = (int) (8 - ((imageWidth * bps0 * nChannels) % 8)); if (skipBits == 8 || (bytes.length * 8 < bps0 * (nChannels * imageWidth + imageHeight))) { skipBits = 0; } // set up YCbCr-specific values float lumaRed = PhotoInterp.LUMA_RED; float lumaGreen = PhotoInterp.LUMA_GREEN; float lumaBlue = PhotoInterp.LUMA_BLUE; int[] reference = ifd.getIFDIntArray(IFD.REFERENCE_BLACK_WHITE); if (reference == null) { reference = new int[] {0, 0, 0, 0, 0, 0}; } int[] subsampling = ifd.getIFDIntArray(IFD.Y_CB_CR_SUB_SAMPLING); TiffRational[] coefficients = (TiffRational[]) ifd.getIFDValue(IFD.Y_CB_CR_COEFFICIENTS); if (coefficients != null) { lumaRed = coefficients[0].floatValue(); lumaGreen = coefficients[1].floatValue(); lumaBlue = coefficients[2].floatValue(); } int subX = subsampling == null ? 2 : subsampling[0]; int subY = subsampling == null ? 2 : subsampling[1]; int block = subX * subY; int nTiles = (int) (imageWidth / subX); RandomAccessInputStream bb = null; try { bb = new RandomAccessInputStream(new ByteArrayHandle(bytes)); // unpack pixels for (int sample=0; sample<sampleCount; sample++) { int ndx = startIndex + sample; if (ndx >= nSamples) break; for (int channel=0; channel<nChannels; channel++) { int index = numBytes * (sample * nChannels + channel); int outputIndex = (channel * nSamples + ndx) * numBytes; // unpack non-YCbCr samples if (photoInterp != PhotoInterp.Y_CB_CR) { long value = 0; if (noDiv8) { // bits per sample is not a multiple of 8 if ((channel == 0 && photoInterp == PhotoInterp.RGB_PALETTE) || (photoInterp != PhotoInterp.CFA_ARRAY && photoInterp != PhotoInterp.RGB_PALETTE)) { try { value = bb.readBits(bps0) & 0xffff; } catch (ArrayIndexOutOfBoundsException e) { // leave the value at 0 if there aren't enough bytes // to cover the total number of samples } if ((ndx % imageWidth) == imageWidth - 1) { bb.skipBits(skipBits); } } } else { value = DataTools.bytesToLong(bytes, index, numBytes, littleEndian); } if (photoInterp == PhotoInterp.WHITE_IS_ZERO || photoInterp == PhotoInterp.CMYK) { value = maxValue - value; } if (outputIndex + numBytes <= samples.length) { DataTools.unpackBytes(value, samples, outputIndex, numBytes, littleEndian); } } else { // unpack YCbCr samples; these need special handling, as each of // the RGB components depends upon two or more of the YCbCr components if (channel == nChannels - 1) { int lumaIndex = sample + (2 * (sample / block)); int chromaIndex = (sample / block) * (block + 2) + block; if (chromaIndex + 1 >= bytes.length) break; int tile = ndx / block; int pixel = ndx % block; long r = subY * (tile / nTiles) + (pixel / subX); long c = subX * (tile % nTiles) + (pixel % subX); int idx = (int) (r * imageWidth + c); if (idx < nSamples) { int y = (bytes[lumaIndex] & 0xff) - reference[0]; int cb = (bytes[chromaIndex] & 0xff) - reference[2]; int cr = (bytes[chromaIndex + 1] & 0xff) - reference[4]; int red = (int) (cr * (2 - 2 * lumaRed) + y); int blue = (int) (cb * (2 - 2 * lumaBlue) + y); int green = (int) ((y - lumaBlue * blue - lumaRed * red) / lumaGreen); samples[idx] = (byte) (red & 0xff); samples[nSamples + idx] = (byte) (green & 0xff); samples[2*nSamples + idx] = (byte) (blue & 0xff); } } } } } } catch (IOException e) { throw new FormatException(e); } finally { if (bb != null) { try { bb.close(); } catch (IOException e) { throw new FormatException(e); } } } } /** * 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. */ long getNextOffset(long previous) throws IOException { if (bigTiff || fakeBigTiff) { return in.readLong(); } long 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; } TiffIFDEntry readTiffIFDEntry() throws IOException { int entryTag = in.readUnsignedShort(); // Parse the entry's "Type" IFDType entryType; try { entryType = IFDType.get(in.readUnsignedShort()); } catch (EnumException e) { LOGGER.error("Error reading IFD type at: {}", in.getFilePointer()); throw e; } // Parse the entry's "ValueCount" int valueCount = bigTiff ? (int) in.readLong() : in.readInt(); if (valueCount < 0) { throw new RuntimeException("Count of '" + valueCount + "' unexpected."); } int nValueBytes = valueCount * entryType.getBytesPerElement(); int threshhold = bigTiff ? 8 : 4; long offset = nValueBytes > threshhold ? getNextOffset(0) : in.getFilePointer(); return new TiffIFDEntry(entryTag, entryType, valueCount, offset); } }