/* * #%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.in; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import loci.common.DataTools; import loci.common.RandomAccessInputStream; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; import loci.formats.MetadataTools; import loci.formats.codec.JPEG2000CodecOptions; import loci.formats.meta.MetadataStore; import loci.formats.tiff.IFD; import loci.formats.tiff.IFDList; import loci.formats.tiff.PhotoInterp; import loci.formats.tiff.TiffCompression; import loci.formats.tiff.TiffIFDEntry; import loci.formats.tiff.TiffParser; /** * MinimalTiffReader is the superclass for file format readers compatible with * or derived from the TIFF 6.0 file format. * * @author Melissa Linkert melissa at glencoesoftware.com */ public class MinimalTiffReader extends FormatReader { // -- Constants -- /** Logger for this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(MinimalTiffReader.class); // -- Fields -- /** List of IFDs for the current TIFF. */ protected IFDList ifds; /** List of thumbnail IFDs for the current TIFF. */ protected IFDList thumbnailIFDs; /** * List of sub-resolution IFDs for each IFD in the current TIFF with the * same order as <code>ifds</code>. */ protected List<IFDList> subResolutionIFDs; protected transient TiffParser tiffParser; protected boolean equalStrips = false; protected boolean use64Bit = false; protected int lastPlane = 0; protected boolean noSubresolutions = false; /** Number of JPEG 2000 resolution levels. */ private Integer resolutionLevels; /** Codec options to use when decoding JPEG 2000 data. */ private JPEG2000CodecOptions j2kCodecOptions; // -- Constructors -- /** Constructs a new MinimalTiffReader. */ public MinimalTiffReader() { this("Minimal TIFF", new String[] {"tif", "tiff"}); } /** Constructs a new MinimalTiffReader. */ public MinimalTiffReader(String name, String suffix) { this(name, new String[] {suffix}); } /** Constructs a new MinimalTiffReader. */ public MinimalTiffReader(String name, String[] suffixes) { super(name, suffixes); domains = new String[] {FormatTools.GRAPHICS_DOMAIN}; suffixNecessary = false; } // -- MinimalTiffReader API methods -- /** Gets the list of IFDs associated with the current TIFF's image planes. */ public IFDList getIFDs() { return ifds; } /** Gets the list of IFDs associated with the current TIFF's thumbnails. */ public IFDList getThumbnailIFDs() { return thumbnailIFDs; } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */ @Override public boolean isThisType(RandomAccessInputStream stream) throws IOException { return new TiffParser(stream).isValidHeader(); } /* @see loci.formats.IFormatReader#get8BitLookupTable() */ @Override public byte[][] get8BitLookupTable() throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); if (ifds == null || lastPlane < 0 || lastPlane >= ifds.size()) return null; IFD lastIFD = ifds.get(lastPlane); int[] bits = lastIFD.getBitsPerSample(); if (bits[0] <= 8) { int[] colorMap = tiffParser.getColorMap(lastIFD); if (colorMap == null) { // it's possible that the LUT is only present in the first IFD if (lastPlane != 0) { lastIFD = ifds.get(0); colorMap = tiffParser.getColorMap(lastIFD); if (colorMap == null) return null; } else return null; } byte[][] table = new byte[3][colorMap.length / 3]; int next = 0; for (int j=0; j<table.length; j++) { for (int i=0; i<table[0].length; i++) { if (colorMap[next] > 255) { table[j][i] = (byte) ((colorMap[next++] >> 8) & 0xff); } else { table[j][i] = (byte) (colorMap[next++] & 0xff); } } } return table; } return null; } /* @see loci.formats.IFormatReader#get16BitLookupTable() */ @Override public short[][] get16BitLookupTable() throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); if (ifds == null || lastPlane < 0 || lastPlane >= ifds.size()) return null; IFD lastIFD = ifds.get(lastPlane); int[] bits = lastIFD.getBitsPerSample(); if (bits[0] <= 16 && bits[0] > 8) { int[] colorMap = tiffParser.getColorMap(lastIFD); if (colorMap == null || colorMap.length < 65536 * 3) { // it's possible that the LUT is only present in the first IFD if (lastPlane != 0) { lastIFD = ifds.get(0); colorMap = tiffParser.getColorMap(lastIFD); if (colorMap == null || colorMap.length < 65536 * 3) return null; } else return null; } short[][] table = new short[3][colorMap.length / 3]; int next = 0; for (int i=0; i<table.length; i++) { for (int j=0; j<table[0].length; j++) { table[i][j] = (short) (colorMap[next++] & 0xffff); } } return table; } return null; } /* @see loci.formats.FormatReader#getThumbSizeX() */ @Override public int getThumbSizeX() { if (thumbnailIFDs != null && thumbnailIFDs.size() > 0) { try { return (int) thumbnailIFDs.get(0).getImageWidth(); } catch (FormatException e) { LOGGER.debug("Could not retrieve thumbnail width", e); } } return super.getThumbSizeX(); } /* @see loci.formats.FormatReader#getThumbSizeY() */ @Override public int getThumbSizeY() { if (thumbnailIFDs != null && thumbnailIFDs.size() > 0) { try { return (int) thumbnailIFDs.get(0).getImageLength(); } catch (FormatException e) { LOGGER.debug("Could not retrieve thumbnail height", e); } } return super.getThumbSizeY(); } /* @see loci.formats.FormatReader#openThumbBytes(int) */ @Override public byte[] openThumbBytes(int no) throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); if (thumbnailIFDs == null || thumbnailIFDs.size() <= no) { return super.openThumbBytes(no); } if (tiffParser == null) { initTiffParser(); } tiffParser.fillInIFD(thumbnailIFDs.get(no)); int[] bps = null; try { bps = thumbnailIFDs.get(no).getBitsPerSample(); } catch (FormatException e) { } if (bps == null) { return super.openThumbBytes(no); } int b = bps[0]; while ((b % 8) != 0) b++; b /= 8; if (b != FormatTools.getBytesPerPixel(getPixelType()) || bps.length != getRGBChannelCount()) { return super.openThumbBytes(no); } byte[] buf = new byte[getThumbSizeX() * getThumbSizeY() * getRGBChannelCount() * FormatTools.getBytesPerPixel(getPixelType())]; return tiffParser.getSamples(thumbnailIFDs.get(no), buf); } /** * @see loci.formats.FormatReader#openBytes(int, byte[], int, int, int, int) */ @Override public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws FormatException, IOException { FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h); IFD firstIFD = ifds.get(0); lastPlane = no; IFD ifd = ifds.get(no); if ((firstIFD.getCompression() == TiffCompression.JPEG_2000 || firstIFD.getCompression() == TiffCompression.JPEG_2000_LOSSY) && resolutionLevels != null) { if (getCoreIndex() > 0) { ifd = subResolutionIFDs.get(no).get(getCoreIndex() - 1); } setResolutionLevel(ifd); } if (tiffParser == null) { initTiffParser(); } tiffParser.getSamples(ifd, buf, x, y, w, h); boolean float16 = getPixelType() == FormatTools.FLOAT && firstIFD.getBitsPerSample()[0] == 16; boolean float24 = getPixelType() == FormatTools.FLOAT && firstIFD.getBitsPerSample()[0] == 24; if (float16 || float24) { int nPixels = w * h * getRGBChannelCount(); int nBytes = float16 ? 2 : 3; int mantissaBits = float16 ? 10 : 16; int exponentBits = float16 ? 5 : 7; int maxExponent = (int) Math.pow(2, exponentBits) - 1; int bits = (nBytes * 8) - 1; byte[] newBuf = new byte[buf.length]; for (int i=0; i<nPixels; i++) { int v = DataTools.bytesToInt(buf, i * nBytes, nBytes, isLittleEndian()); int sign = v >> bits; int exponent = (v >> mantissaBits) & (int) (Math.pow(2, exponentBits) - 1); int mantissa = v & (int) (Math.pow(2, mantissaBits) - 1); if (exponent == 0) { if (mantissa != 0) { while ((mantissa & (int) Math.pow(2, mantissaBits)) == 0) { mantissa <<= 1; exponent--; } exponent++; mantissa &= (int) (Math.pow(2, mantissaBits) - 1); exponent += 127 - (Math.pow(2, exponentBits - 1) - 1); } } else if (exponent == maxExponent) { exponent = 255; } else { exponent += 127 - (Math.pow(2, exponentBits - 1) - 1); } mantissa <<= (23 - mantissaBits); int value = (sign << 31) | (exponent << 23) | mantissa; DataTools.unpackBytes(value, newBuf, i * 4, 4, isLittleEndian()); } System.arraycopy(newBuf, 0, buf, 0, newBuf.length); } return buf; } /* @see loci.formats.IFormatReader#reopenFile() */ @Override public void reopenFile() throws IOException { initTiffParser(); } /* @see loci.formats.IFormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (!fileOnly) { if (ifds != null) { for (IFD ifd : ifds) { try { if (ifd.getOnDemandStripOffsets() != null) { ifd.getOnDemandStripOffsets().close(); } } catch (FormatException e) { LOGGER.debug("", e); } } } ifds = null; thumbnailIFDs = null; subResolutionIFDs = null; lastPlane = 0; tiffParser = null; resolutionLevels = null; j2kCodecOptions = null; } } /* @see loci.formats.IFormatReader#getOptimalTileWidth() */ @Override public int getOptimalTileWidth() { FormatTools.assertId(currentId, true, 1); try { return (int) ifds.get(0).getTileWidth(); } catch (FormatException e) { LOGGER.debug("Could not retrieve tile width", e); } return super.getOptimalTileWidth(); } /* @see loci.formats.IFormatReader#getOptimalTileHeight() */ @Override public int getOptimalTileHeight() { FormatTools.assertId(currentId, true, 1); try { int height = (int) ifds.get(0).getTileLength(); if (height <= 0) { height = getSizeY(); } // Some TIFF files only store a single tile, even if the image is // very, very large. In those cases, we don't want to open the whole // tile if we can avoid it. if (DataTools.safeMultiply32(height, getOptimalTileWidth()) > 10 * 1024 * 1024) { return super.getOptimalTileHeight(); } if (height > 1) { return height; } } catch (FormatException e) { LOGGER.debug("Could not retrieve tile height", e); } return super.getOptimalTileHeight(); } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ @Override protected void initFile(String id) throws FormatException, IOException { super.initFile(id); in = new RandomAccessInputStream(id, 16); initTiffParser(); Boolean littleEndian = tiffParser.checkHeader(); if (littleEndian == null) { throw new FormatException("Invalid TIFF file: " + id); } boolean little = littleEndian.booleanValue(); in.order(little); LOGGER.info("Reading IFDs"); IFDList allIFDs = tiffParser.getIFDs(); if (allIFDs == null || allIFDs.size() == 0) { throw new FormatException("No IFDs found"); } ifds = new IFDList(); thumbnailIFDs = new IFDList(); subResolutionIFDs = new ArrayList<IFDList>(); for (IFD ifd : allIFDs) { Number subfile = (Number) ifd.getIFDValue(IFD.NEW_SUBFILE_TYPE); int subfileType = subfile == null ? 0 : subfile.intValue(); if (subfileType != 1 || allIFDs.size() <= 1) { ifds.add(ifd); } else if (subfileType == 1) { thumbnailIFDs.add(ifd); } } LOGGER.info("Populating metadata"); CoreMetadata ms0 = core.get(0); ms0.imageCount = ifds.size(); tiffParser.setAssumeEqualStrips(equalStrips); for (IFD ifd : ifds) { tiffParser.fillInIFD(ifd); if ((ifd.getCompression() == TiffCompression.JPEG_2000 || ifd.getCompression() == TiffCompression.JPEG_2000_LOSSY) && ifd.getImageWidth() == ifds.get(0).getImageWidth()) { LOGGER.debug("Found IFD with JPEG 2000 compression"); long[] stripOffsets = ifd.getStripOffsets(); long[] stripByteCounts = ifd.getStripByteCounts(); if (stripOffsets.length > 0) { long stripOffset = stripOffsets[0]; in.seek(stripOffset); JPEG2000MetadataParser metadataParser = new JPEG2000MetadataParser(in, stripOffset + stripByteCounts[0]); resolutionLevels = metadataParser.getResolutionLevels(); if (resolutionLevels != null && !noSubresolutions) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format( "Original resolution IFD Levels %d %dx%d Tile %dx%d", resolutionLevels, ifd.getImageWidth(), ifd.getImageLength(), ifd.getTileWidth(), ifd.getTileLength())); } IFDList theseSubResolutionIFDs = new IFDList(); subResolutionIFDs.add(theseSubResolutionIFDs); for (int level = 1; level <= resolutionLevels; level++) { IFD newIFD = new IFD(ifd); long imageWidth = ifd.getImageWidth(); long imageLength = ifd.getImageLength(); long tileWidth = ifd.getTileWidth(); long tileLength = ifd.getTileLength(); long factor = (long) Math.pow(2, level); long newTileWidth = Math.round((double) tileWidth / factor); newTileWidth = newTileWidth < 1? 1 : newTileWidth; long newTileLength = Math.round((double) tileLength / factor); newTileLength = newTileLength < 1? 1 : newTileLength; long evenTilesPerRow = imageWidth / tileWidth; long evenTilesPerColumn = imageLength / tileLength; double remainingWidth = ((double) (imageWidth - (evenTilesPerRow * tileWidth))) / factor; remainingWidth = remainingWidth < 1? Math.ceil(remainingWidth) : Math.round(remainingWidth); double remainingLength = ((double) (imageLength - (evenTilesPerColumn * tileLength))) / factor; remainingLength = remainingLength < 1? Math.ceil(remainingLength) : Math.round(remainingLength); long newImageWidth = (long) ((evenTilesPerRow * newTileWidth) + remainingWidth); long newImageLength = (long) ((evenTilesPerColumn * newTileLength) + remainingLength); int resolutionLevel = Math.abs(level - resolutionLevels); newIFD.put(IFD.IMAGE_WIDTH, newImageWidth); newIFD.put(IFD.IMAGE_LENGTH, newImageLength); newIFD.put(IFD.TILE_WIDTH, newTileWidth); newIFD.put(IFD.TILE_LENGTH, newTileLength); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format( "Added JPEG 2000 sub-resolution IFD Level %d %dx%d " + "Tile %dx%d", resolutionLevel, newImageWidth, newImageLength, newTileWidth, newTileLength)); } theseSubResolutionIFDs.add(newIFD); } } } else { LOGGER.warn("IFD has no strip offsets!"); } } } IFD firstIFD = ifds.get(0); PhotoInterp photo = firstIFD.getPhotometricInterpretation(); int samples = firstIFD.getSamplesPerPixel(); ms0.rgb = samples > 1 || photo == PhotoInterp.RGB; ms0.interleaved = false; ms0.littleEndian = firstIFD.isLittleEndian(); ms0.sizeX = (int) firstIFD.getImageWidth(); ms0.sizeY = (int) firstIFD.getImageLength(); ms0.sizeZ = 1; ms0.sizeC = isRGB() ? samples : 1; ms0.sizeT = ifds.size(); ms0.pixelType = firstIFD.getPixelType(); ms0.metadataComplete = true; ms0.indexed = photo == PhotoInterp.RGB_PALETTE && (get8BitLookupTable() != null || get16BitLookupTable() != null); if (isIndexed()) { ms0.sizeC = 1; ms0.rgb = false; for (IFD ifd : ifds) { ifd.putIFDValue(IFD.PHOTOMETRIC_INTERPRETATION, PhotoInterp.RGB_PALETTE); } } if (getSizeC() == 1 && !isIndexed()) ms0.rgb = false; ms0.dimensionOrder = "XYCZT"; ms0.bitsPerPixel = firstIFD.getBitsPerSample()[0]; // New core metadata now that we know how many sub-resolutions we have. if (resolutionLevels != null && subResolutionIFDs.size() > 0) { IFDList ifds = subResolutionIFDs.get(0); int seriesCount = ifds.size() + 1; if (!hasFlattenedResolutions()) { ms0.resolutionCount = seriesCount; } ms0.sizeT = subResolutionIFDs.size(); ms0.imageCount = ms0.sizeT; if (ms0.sizeT <= 0) { ms0.sizeT = 1; } if (ms0.imageCount <= 0) { ms0.imageCount = 1; } for (IFD ifd : ifds) { CoreMetadata ms = new CoreMetadata(this, 0); core.add(ms); ms.sizeX = (int) ifd.getImageWidth(); ms.sizeY = (int) ifd.getImageLength(); ms.sizeT = ms0.sizeT; ms.imageCount = ms0.imageCount; ms.thumbnail = true; ms.resolutionCount = 1; } } MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this); } /** * Sets the resolution level when we have JPEG 2000 compressed data. * @param ifd The active IFD that is being used in our current * <code>openBytes()</code> calling context. It will be the sub-resolution * IFD if <code>currentSeries > 0</code>. */ protected void setResolutionLevel(IFD ifd) { if (tiffParser == null) { initTiffParser(); } if (j2kCodecOptions == null) { j2kCodecOptions = new JPEG2000CodecOptions(); } j2kCodecOptions.resolution = Math.abs(getCoreIndex() - resolutionLevels); LOGGER.debug("Using JPEG 2000 resolution level {}", j2kCodecOptions.resolution); tiffParser.setCodecOptions(j2kCodecOptions); } /** Reinitialize the underlying TiffParser. */ protected void initTiffParser() { if (in == null) { try { in = new RandomAccessInputStream(getCurrentFile(), 16); } catch (IOException e) { LOGGER.error("Could not initialize stream", e); } } tiffParser = new TiffParser(in); tiffParser.setDoCaching(false); tiffParser.setUse64BitOffsets(use64Bit); } }