//
// MinimalTiffReader.java
//
/*
OME Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
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.TiffParser;
/**
* MinimalTiffReader is the superclass for file format readers compatible with
* or derived from the TIFF 6.0 file format.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/in/MinimalTiffReader.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/MinimalTiffReader.java;hb=HEAD">Gitweb</a></dd></dl>
*
* @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 TiffParser tiffParser;
protected boolean use64Bit = false;
private int lastPlane = 0;
/** 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) */
public boolean isThisType(RandomAccessInputStream stream) throws IOException {
return new TiffParser(stream).isValidHeader();
}
/* @see loci.formats.IFormatReader#get8BitLookupTable() */
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 = lastIFD.getIFDIntArray(IFD.COLOR_MAP);
if (colorMap == null) {
// it's possible that the LUT is only present in the first IFD
if (lastPlane != 0) {
lastIFD = ifds.get(0);
colorMap = lastIFD.getIFDIntArray(IFD.COLOR_MAP);
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() */
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 = lastIFD.getIFDIntArray(IFD.COLOR_MAP);
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 = lastIFD.getIFDIntArray(IFD.COLOR_MAP);
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() */
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() */
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) */
public byte[] openThumbBytes(int no) throws FormatException, IOException {
FormatTools.assertId(currentId, true, 1);
if (thumbnailIFDs == null || thumbnailIFDs.size() <= no) {
return super.openThumbBytes(no);
}
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)
*/
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 (series > 0) {
ifd = subResolutionIFDs.get(no).get(series - 1);
}
setResolutionLevel(ifd);
}
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#close(boolean) */
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
if (!fileOnly) {
ifds = null;
thumbnailIFDs = null;
subResolutionIFDs = new ArrayList<IFDList>();
lastPlane = 0;
tiffParser = null;
resolutionLevels = null;
j2kCodecOptions = JPEG2000CodecOptions.getDefaultOptions();
}
}
/* @see loci.formats.IFormatReader#getOptimalTileWidth() */
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() */
public int getOptimalTileHeight() {
FormatTools.assertId(currentId, true, 1);
try {
return (int) ifds.get(0).getTileLength();
}
catch (FormatException e) {
LOGGER.debug("Could not retrieve tile height", e);
}
return super.getOptimalTileHeight();
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
in = new RandomAccessInputStream(id);
tiffParser = new TiffParser(in);
tiffParser.setDoCaching(false);
tiffParser.setUse64BitOffsets(use64Bit);
Boolean littleEndian = tiffParser.checkHeader();
if (littleEndian == null) {
throw new FormatException("Invalid TIFF file");
}
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();
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");
core[0].imageCount = ifds.size();
for (IFD ifd : ifds) {
tiffParser.fillInIFD(ifd);
if (ifd.getCompression() == TiffCompression.JPEG_2000
|| ifd.getCompression() == TiffCompression.JPEG_2000_LOSSY) {
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) {
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();
core[0].rgb = samples > 1 || photo == PhotoInterp.RGB;
core[0].interleaved = false;
core[0].littleEndian = firstIFD.isLittleEndian();
core[0].sizeX = (int) firstIFD.getImageWidth();
core[0].sizeY = (int) firstIFD.getImageLength();
core[0].sizeZ = 1;
core[0].sizeC = isRGB() ? samples : 1;
core[0].sizeT = ifds.size();
core[0].pixelType = firstIFD.getPixelType();
core[0].metadataComplete = true;
core[0].indexed = photo == PhotoInterp.RGB_PALETTE &&
(get8BitLookupTable() != null || get16BitLookupTable() != null);
if (isIndexed()) {
core[0].sizeC = 1;
core[0].rgb = false;
for (IFD ifd : ifds) {
ifd.putIFDValue(IFD.PHOTOMETRIC_INTERPRETATION,
PhotoInterp.RGB_PALETTE);
}
}
if (getSizeC() == 1 && !isIndexed()) core[0].rgb = false;
core[0].dimensionOrder = "XYCZT";
core[0].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);
CoreMetadata[] newCore = new CoreMetadata[ifds.size() + 1];
newCore[0] = core[0];
int i = 1;
for (IFD ifd : ifds) {
newCore[i] = new CoreMetadata(this, 0);
newCore[i].sizeX = (int) ifd.getImageWidth();
newCore[i].sizeY = (int) ifd.getImageLength();
newCore[i].thumbnail = true;
i++;
}
core = newCore;
}
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) {
j2kCodecOptions.resolution = Math.abs(series - resolutionLevels);
LOGGER.debug("Using JPEG 2000 resolution level {}",
j2kCodecOptions.resolution);
tiffParser.setCodecOptions(j2kCodecOptions);
}
}