//
// FluoviewReader.java
//
/*
LOCI Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ Melissa Linkert, Curtis Rueden, Chris Allan,
Eric Kjellman and Brian Loranger.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Library 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 Library General Public License for more details.
You should have received a copy of the GNU Library 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.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import loci.formats.*;
/**
* FluoviewReader is the file format reader for
* Olympus Fluoview TIFF files AND Andor Bio-imaging Division (ABD) TIFF files.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/in/FluoviewReader.java">Trac</a>,
* <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/in/FluoviewReader.java">SVN</a></dd></dl>
*
* @author Eric Kjellman egkjellman at wisc.edu
* @author Melissa Linkert linkert at wisc.edu
* @author Curtis Rueden ctrueden at wisc.edu
*/
public class FluoviewReader extends BaseTiffReader {
// -- Constants --
/** Maximum number of bytes to check for Fluoview header information. */
private static final int BLOCK_CHECK_LEN = 16384;
/** String identifying a Fluoview file. */
private static final String FLUOVIEW_MAGIC_STRING = "FLUOVIEW";
/** Private TIFF tags */
private static final int MMHEADER = 34361;
private static final int MMSTAMP = 34362;
// -- Fields --
/** Pixel dimensions for this file. */
private float voxelX = 0f, voxelY = 0f, voxelZ = 0f, voxelC = 0f, voxelT = 0f;
/** First image. */
private BufferedImage zeroImage = null;
// -- Constructor --
/** Constructs a new Fluoview TIFF reader. */
public FluoviewReader() {
super("Olympus Fluoview/ABD TIFF", new String[] {"tif", "tiff"});
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(byte[]) */
public boolean isThisType(byte[] block) {
if (!TiffTools.isValidHeader(block)) return false;
if (block.length < 3) return false;
if (block.length < 8) return true;
String test = new String(block);
if (test.indexOf(FLUOVIEW_MAGIC_STRING) != -1) {
return true;
}
int ifdlocation = DataTools.bytesToInt(block, 4, true);
if (ifdlocation < 0 || ifdlocation + 1 > block.length) return false;
else {
int ifdnumber = DataTools.bytesToInt(block, ifdlocation, 2, true);
for (int i=0; i<ifdnumber; i++) {
if (ifdlocation + 3 + (i*12) > block.length) return false;
else {
int ifdtag = DataTools.bytesToInt(block, ifdlocation + 2 + (i*12),
2, true);
if (ifdtag == MMHEADER || ifdtag == MMSTAMP) return true;
}
}
}
return false;
}
/* @see loci.formats.IFormatReader#openBytes(int) */
public byte[] openBytes(int no) throws FormatException, IOException {
if (core.sizeY[0] == TiffTools.getImageLength(ifds[0])) {
return super.openBytes(no);
}
return openBytes(no, new byte[core.sizeX[0] *
FormatTools.getBytesPerPixel(core.pixelType[0])]);
}
/* @see loci.formats.IFormatReader#openBytes(int, byte[]) */
public byte[] openBytes(int no, byte[] buf)
throws FormatException, IOException
{
if (core.sizeY[0] == TiffTools.getImageLength(ifds[0])) {
return super.openBytes(no, buf);
}
FormatTools.assertId(currentId, true, 1);
FormatTools.checkPlaneNumber(this, no);
FormatTools.checkBufferSize(this, buf.length);
byte[] b = new byte[core.sizeX[0] *
(int) TiffTools.getImageLength(ifds[0]) *
getRGBChannelCount() * FormatTools.getBytesPerPixel(core.pixelType[0])];
super.openBytes(0, b);
System.arraycopy(b, 0, buf, 0, buf.length);
return buf;
}
/* @see loci.formats.IFormatReader#openImage(int) */
public BufferedImage openImage(int no) throws FormatException, IOException {
if (core.sizeY[0] == TiffTools.getImageLength(ifds[0])) {
return super.openImage(no);
}
if (zeroImage == null) zeroImage = super.openImage(0);
return zeroImage.getSubimage(0, no, core.sizeX[0], 1);
}
/* @see loci.formats.IFormatReader#close() */
public void close() throws IOException {
super.close();
zeroImage = null;
}
// -- IFormatHandler API methods --
/* @see loci.formats.IFormatHandler#isThisType(String, boolean) */
public boolean isThisType(String name, boolean open) {
if (!super.isThisType(name, open)) return false; // check extension
// just checking the filename isn't enough to differentiate between
// Fluoview and regular TIFF; open the file and check more thoroughly
return open ? checkBytes(name, BLOCK_CHECK_LEN) : true;
}
// -- Internal BaseTiffReader API methods --
/* @see loci.formats.BaseTiffReader#initStandardMetadata() */
protected void initStandardMetadata() throws FormatException, IOException {
super.initStandardMetadata();
// First, we want to determine whether this file is a Fluoview TIFF.
// Originally, Andor TIFF had its own reader; however, the two formats are
// very similar, so it made more sense to merge the two formats into one
// reader.
byte[] buf = new byte[BLOCK_CHECK_LEN];
in.seek(0);
in.read(buf);
short[] s = TiffTools.getIFDShortArray(ifds[0], MMHEADER, true);
byte[] mmheader = new byte[s.length];
for (int i=0; i<mmheader.length; i++) {
mmheader[i] = (byte) s[i];
if (mmheader[i] < 0) mmheader[i]++;
}
RandomAccessStream ras = new RandomAccessStream(mmheader);
ras.order(isLittleEndian());
put("Header Flag", ras.readShort());
put("Image Type", ras.readChar());
put("Image name", ras.readString(257));
ras.skipBytes(4); // skip pointer to data field
put("Number of colors", ras.readInt());
ras.skipBytes(4); // skip pointer to palette field
ras.skipBytes(4); // skip pointer to other palette field
put("Comment size", ras.readInt());
ras.skipBytes(4); // skip pointer to comment field
// read dimension information
String[] names = new String[10];
int[] sizes = new int[10];
double[] resolutions = new double[10];
for (int i=0; i<10; i++) {
names[i] = ras.readString(16);
sizes[i] = ras.readInt();
double origin = ras.readDouble();
resolutions[i] = ras.readDouble();
put("Dimension " + (i+1) + " Name", names[i]);
put("Dimension " + (i+1) + " Size", sizes[i]);
put("Dimension " + (i+1) + " Origin", origin);
put("Dimension " + (i+1) + " Resolution", resolutions[i]);
put("Dimension " + (i+1) + " Units", ras.readString(64));
}
ras.skipBytes(4); // skip pointer to spatial position data
put("Map type", ras.readShort());
put("Map min", ras.readDouble());
put("Map max", ras.readDouble());
put("Min value", ras.readDouble());
put("Max value", ras.readDouble());
ras.skipBytes(4); // skip pointer to map data
put("Gamma", ras.readDouble());
put("Offset", ras.readDouble());
// read gray channel data
put("Gray Channel Name", ras.readString(16));
put("Gray Channel Size", ras.readInt());
put("Gray Channel Origin", ras.readDouble());
put("Gray Channel Resolution", ras.readDouble());
put("Gray Channel Units", ras.readString(64));
ras.skipBytes(4); // skip pointer to thumbnail data
put("Voice field", ras.readInt());
ras.skipBytes(4); // skip pointer to voice field
// now we need to read the MMSTAMP data to determine dimension order
double[][] stamps = new double[8][ifds.length];
for (int i=0; i<ifds.length; i++) {
s = TiffTools.getIFDShortArray(ifds[i], MMSTAMP, true);
byte[] stamp = new byte[s.length];
for (int j=0; j<s.length; j++) {
stamp[j] = (byte) s[j];
if (stamp[j] < 0) stamp[j]++;
}
ras = new RandomAccessStream(stamp);
// each stamp is 8 doubles, representing the position on dimensions 3-10
for (int j=0; j<8; j++) {
stamps[j][i] = ras.readDouble();
}
}
// calculate the dimension order and axis sizes
core.sizeZ[0] = core.sizeC[0] = core.sizeT[0] = 1;
core.currentOrder[0] = "XY";
core.metadataComplete[0] = true;
for (int i=0; i<10; i++) {
String name = names[i];
int size = sizes[i];
float voxel = (float) resolutions[i];
if (name == null || size == 0) continue;
name = name.toLowerCase().trim();
if (name.length() == 0) continue;
if (name.equals("x")) {
if (core.sizeX[0] == 0) core.sizeX[0] = size;
voxelX = voxel;
}
else if (name.equals("y")) {
core.sizeY[0] = size;
voxelY = voxel;
}
else if (name.equals("z") || name.equals("event")) {
core.sizeZ[0] *= size;
if (core.currentOrder[0].indexOf("Z") == -1) {
core.currentOrder[0] += "Z";
}
voxelZ = voxel;
}
else if (name.equals("ch") || name.equals("wavelength")) {
core.sizeC[0] *= size;
if (core.currentOrder[0].indexOf("C") == -1) {
core.currentOrder[0] += "C";
}
voxelC = voxel;
}
else {
core.sizeT[0] *= size;
if (core.currentOrder[0].indexOf("T") == -1) {
core.currentOrder[0] += "T";
}
voxelT = voxel;
}
}
if (core.currentOrder[0].indexOf("Z") == -1) core.currentOrder[0] += "Z";
if (core.currentOrder[0].indexOf("T") == -1) core.currentOrder[0] += "T";
if (core.currentOrder[0].indexOf("C") == -1) core.currentOrder[0] += "C";
core.imageCount[0] = ifds.length;
if (core.imageCount[0] == 1 && (core.sizeT[0] == core.sizeY[0] ||
core.sizeZ[0] == core.sizeY[0]) && (core.sizeT[0] > core.imageCount[0] ||
core.sizeZ[0] > core.imageCount[0]))
{
core.sizeY[0] = 1;
core.imageCount[0] = core.sizeZ[0] * core.sizeT[0] * core.sizeC[0];
}
// cut up the comment, if necessary
String comment = (String) getMeta("Comment");
if (comment != null && comment.startsWith("[")) {
int start = comment.indexOf("[Acquisition Parameters]");
int end = comment.indexOf("[Acquisition Parameters End]");
if (start != -1 && end != -1 && end > start) {
String parms = comment.substring(start + 24, end).trim();
// this is an INI-style comment, with one key/value pair per line
StringTokenizer st = new StringTokenizer(parms, "\n");
while (st.hasMoreTokens()) {
String token = st.nextToken();
int eq = token.indexOf("=");
if (eq != -1) {
String key = token.substring(0, eq);
String value = token.substring(eq + 1);
addMeta(key, value);
}
}
}
start = comment.indexOf("[Version Info]");
end = comment.indexOf("[Version Info End]");
if (start != -1 && end != -1 && end > start) {
comment = comment.substring(start + 14, end).trim();
start = comment.indexOf("=") + 1;
end = comment.indexOf("\n");
if (end > start) comment = comment.substring(start, end).trim();
else comment = comment.substring(start).trim();
}
else comment = "";
}
addMeta("Comment", comment);
}
/* @see loci.formats.in.BaseTiffReader#initMetadataStore() */
protected void initMetadataStore() {
super.initMetadataStore();
MetadataStore store = getMetadataStore();
store.setDimensions(new Float(voxelX), new Float(voxelY),
new Float(voxelZ), new Float(voxelC), new Float(voxelT), null);
Double gamma = (Double) getMeta("Gamma");
for (int i=0; i<core.sizeC[0]; i++) {
store.setDisplayChannel(new Integer(i), null, null,
gamma == null ? null : new Float(gamma.floatValue()), null);
String gain = (String) getMeta("Gain Ch" + (i+1));
String voltage = (String) getMeta("PMT Voltage Ch" + (i+1));
String offset = (String) getMeta("Offset Ch" + (i+1));
if (gain != null || voltage != null || offset != null) {
store.setDetector((String) getMeta("System Configuration"), null,
null, null, gain == null ? null : new Float(gain),
voltage == null ? null : new Float(voltage),
offset == null ? null : new Float(offset), null, new Integer(i));
}
}
String mag = (String) getMeta("Magnification");
if (mag != null && mag.toLowerCase().endsWith("x")) {
mag = mag.substring(0, mag.length() - 1);
}
else if (mag == null) mag = "1";
store.setObjective((String) getMeta("Objective Lens"), null, null, null,
new Float(mag), null, null);
}
}