//
// ZeissLSMReader.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.Hashtable;
import loci.formats.*;
/**
* ZeissLSMReader is the file format reader for Zeiss LSM files.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/in/ZeissLSMReader.java">Trac</a>,
* <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/in/ZeissLSMReader.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 ZeissLSMReader extends BaseTiffReader {
// -- Constants --
/** Tag identifying a Zeiss LSM file. */
private static final int ZEISS_ID = 34412;
// -- Constructor --
/** Constructs a new Zeiss LSM reader. */
public ZeissLSMReader() { super("Zeiss Laser-Scanning Microscopy", "lsm"); }
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(byte[]) */
public boolean isThisType(byte[] block) {
if (block.length < 3) return false;
if (block[0] != TiffTools.LITTLE) return false; // denotes little-endian
if (block[1] != TiffTools.LITTLE) return false;
if (block[2] != TiffTools.MAGIC_NUMBER) return false; // denotes TIFF
if (block.length < 8) return true; // we have no way of verifying
int ifdlocation = DataTools.bytesToInt(block, 4, true);
if (ifdlocation + 1 > block.length) {
// no way of verifying this is a Zeiss file; it is at least a TIFF
return true;
}
else {
int ifdnumber = DataTools.bytesToInt(block, ifdlocation, 2, true);
for (int i=0; i<ifdnumber; i++) {
if (ifdlocation + 3 + (i * 12) > block.length) return true;
else {
int ifdtag = DataTools.bytesToInt(block,
ifdlocation + 2 + (i * 12), 2, true);
if (ifdtag == ZEISS_ID) return true; // absolutely a valid file
}
}
return false; // we went through the IFD; the ID wasn't found.
}
}
/* @see loci.formats.IFormatReader#openBytes(int, byte[]) */
public byte[] openBytes(int no, byte[] buf)
throws FormatException, IOException
{
FormatTools.assertId(currentId, true, 1);
FormatTools.checkPlaneNumber(this, no);
FormatTools.checkBufferSize(this, buf.length);
ifds = TiffTools.getIFDs(in);
TiffTools.getSamples(ifds[2*no], in, buf);
return swapIfRequired(buf);
}
/* @see loci.formats.IFormatReader#openThumbImage(int) */
public BufferedImage openThumbImage(int no)
throws FormatException, IOException
{
FormatTools.assertId(currentId, true, 1);
FormatTools.checkPlaneNumber(this, no);
if (2*no + 1 < ifds.length) return TiffTools.getImage(ifds[2*no + 1], in);
return super.openThumbImage(no);
}
// -- Internal BaseTiffReader API methods --
/* @see BaseTiffReader#initMetadata() */
protected void initMetadata() {
Hashtable ifd = ifds[0];
try {
boolean little = TiffTools.isLittleEndian(ifd);
in.order(little);
super.initMetadata();
// get TIF_CZ_LSMINFO structure
short[] s = TiffTools.getIFDShortArray(ifd, ZEISS_ID, true);
byte[] cz = new byte[s.length];
for (int i=0; i<s.length; i++) {
cz[i] = (byte) s[i];
if (cz[i] < 0) cz[i]++; // account for byte->short conversion
}
RandomAccessStream ras = new RandomAccessStream(cz);
ras.order(little);
put("MagicNumber", ras.readInt());
put("StructureSize", ras.readInt());
put("DimensionX", ras.readInt());
put("DimensionY", ras.readInt());
core.sizeZ[0] = ras.readInt();
int c = ras.readInt();
core.sizeT[0] = ras.readInt();
if (c > core.sizeC[0] || c != 1) core.sizeC[0] = c;
if (core.sizeC[0] == 0) core.sizeC[0]++;
while (core.imageCount[0] > core.sizeZ[0] * core.sizeC[0] * core.sizeT[0])
{
if (core.sizeZ[0] > core.sizeT[0]) core.sizeZ[0]++;
else core.sizeT[0]++;
}
while (core.imageCount[0] > core.sizeZ[0] * core.sizeT[0] *
getEffectiveSizeC())
{
core.imageCount[0]--;
}
put("DimensionZ", core.sizeZ[0]);
put("DimensionChannels", core.sizeC[0]);
put("DimensionTime", core.sizeT[0]);
int dataType = ras.readInt();
switch (dataType) {
case 1:
put("DataType", "8 bit unsigned integer");
core.pixelType[0] = FormatTools.UINT8;
break;
case 2:
put("DataType", "12 bit unsigned integer");
core.pixelType[0] = FormatTools.UINT16;
break;
case 5:
put("DataType", "32 bit float");
core.pixelType[0] = FormatTools.FLOAT;
break;
case 0:
put("DataType", "varying data types");
core.pixelType[0] = -1;
break;
default:
put("DataType", "8 bit unsigned integer");
core.pixelType[0] = -1;
}
if (core.pixelType[0] == -1) {
int[] bps = TiffTools.getBitsPerSample(ifd);
switch (bps[0]) {
case 16:
core.pixelType[0] = FormatTools.UINT16;
break;
case 32:
core.pixelType[0] = FormatTools.FLOAT;
break;
default:
core.pixelType[0] = FormatTools.UINT8;
}
}
put("ThumbnailX", ras.readInt());
put("ThumbnailY", ras.readInt());
put("VoxelSizeX", ras.readDouble());
put("VoxelSizeY", ras.readDouble());
put("VoxelSizeZ", ras.readDouble());
put("OriginX", ras.readDouble());
put("OriginY", ras.readDouble());
put("OriginZ", ras.readDouble());
int scanType = ras.readShort();
switch (scanType) {
case 0:
put("ScanType", "x-y-z scan");
core.currentOrder[0] = "XYZCT";
break;
case 1:
put("ScanType", "z scan (x-z plane)");
core.currentOrder[0] = "XYZCT";
break;
case 2:
put("ScanType", "line scan");
core.currentOrder[0] = "XYZCT";
break;
case 3:
put("ScanType", "time series x-y");
core.currentOrder[0] = "XYTCZ";
break;
case 4:
put("ScanType", "time series x-z");
core.currentOrder[0] = "XYZTC";
break;
case 5:
put("ScanType", "time series 'Mean of ROIs'");
core.currentOrder[0] = "XYTCZ";
break;
case 6:
put("ScanType", "time series x-y-z");
core.currentOrder[0] = "XYZTC";
break;
case 7:
put("ScanType", "spline scan");
core.currentOrder[0] = "XYCTZ";
break;
case 8:
put("ScanType", "spline scan x-z");
core.currentOrder[0] = "XYCZT";
break;
case 9:
put("ScanType", "time series spline plane x-z");
core.currentOrder[0] = "XYTCZ";
break;
case 10:
put("ScanType", "point mode");
core.currentOrder[0] = "XYZCT";
break;
default:
put("ScanType", "x-y-z scan");
core.currentOrder[0] = "XYZCT";
}
MetadataStore store = getMetadataStore();
FormatTools.populatePixels(store, this);
for (int i=0; i<core.sizeC[0]; i++) {
store.setLogicalChannel(i, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null);
}
int spectralScan = ras.readShort();
switch (spectralScan) {
case 0:
put("SpectralScan", "no spectral scan");
break;
case 1:
put("SpectralScan", "acquired with spectral scan");
break;
default:
put("SpectralScan", "no spectral scan");
}
long type = ras.readInt();
switch ((int) type) {
case 0:
put("DataType2", "original scan data");
break;
case 1:
put("DataType2", "calculated data");
break;
case 2:
put("DataType2", "animation");
break;
default:
put("DataType2", "original scan data");
}
long overlayOffset = ras.readInt();
long inputLUTOffset = ras.readInt();
long outputLUTOffset = ras.readInt();
long channelColorsOffset = ras.readInt();
put("TimeInterval", ras.readDouble());
ras.skipBytes(12);
long timeStampOffset = ras.readInt();
long eventListOffset = ras.readInt();
long roiOffset = ras.readInt();
long bleachRoiOffset = ras.readInt();
ras.skipBytes(4);
put("DisplayAspectX", ras.readDouble());
put("DisplayAspectY", ras.readDouble());
put("DisplayAspectZ", ras.readDouble());
put("DisplayAspectTime", ras.readDouble());
long meanOfRoisOverlayOffset = ras.readInt();
long topoIsolineOverlayOffset = ras.readInt();
long topoProfileOverlayOffset = ras.readInt();
long linescanOverlayOffset = ras.readInt();
put("ToolbarFlags", ras.readInt());
ras.skipBytes(20);
// read referenced structures
if (overlayOffset != 0) {
parseOverlays(overlayOffset, "OffsetVectorOverlay", little);
}
if (inputLUTOffset != 0) {
parseSubBlocks(inputLUTOffset, "OffsetInputLut", little);
}
if (outputLUTOffset != 0) {
parseSubBlocks(outputLUTOffset, "OffsetOutputLut", little);
}
if (channelColorsOffset != 0) {
in.seek(channelColorsOffset + 4);
int numColors = in.readInt();
int numNames = in.readInt();
if (numColors > core.sizeC[0]) {
in.seek(channelColorsOffset - 2);
in.order(!little);
in.readInt();
numColors = in.readInt();
numNames = in.readInt();
}
long namesOffset = in.readInt() + channelColorsOffset;
in.skipBytes(4);
// read in the intensity value for each color
if (namesOffset >= 0) {
in.seek(namesOffset);
for (int i=0; i<numColors; i++) put("Intensity" + i, in.readInt());
}
// read in the channel names
for (int i=0; i<numNames; i++) {
// we want to read until we find a null char
StringBuffer sb = new StringBuffer();
char current = (char) in.read();
while (current != 0) {
if (current < 128) sb.append(current);
current = (char) in.read();
}
if (sb.length() <= 128) put("ChannelName" + i, sb.toString());
else put("ChannelName" + i, "");
}
}
if (timeStampOffset != 0) {
in.seek(timeStampOffset + 4);
int numberOfStamps = in.readInt();
for (int i=0; i<numberOfStamps; i++) {
put("TimeStamp" + i, in.readDouble());
}
}
if (eventListOffset != 0) {
in.seek(eventListOffset);
in.skipBytes(4); // skipping the block size
int numEvents = in.readInt();
for (int i=0; i<numEvents; i++) {
int size = in.readInt();
double eventTime = in.readDouble();
int eventType = in.readInt();
byte[] b = new byte[size - 16];
in.read(b);
put("Event" + i + " Time", eventTime);
put("Event" + i + " Type", eventType);
put("Event" + i + " Description", new String(b));
}
}
if (roiOffset != 0) parseOverlays(roiOffset, "ROIOffset", little);
if (bleachRoiOffset != 0) {
parseOverlays(bleachRoiOffset, "BleachROIOffset", little);
}
if (meanOfRoisOverlayOffset != 0) {
parseOverlays(meanOfRoisOverlayOffset,
"OffsetMeanOfRoisOverlay", little);
}
if (topoIsolineOverlayOffset != 0) {
parseOverlays(topoIsolineOverlayOffset,
"OffsetTopoIsolineOverlay", little);
}
if (topoProfileOverlayOffset != 0) {
parseOverlays(topoProfileOverlayOffset,
"OffsetTopoProfileOverlay", little);
}
if (linescanOverlayOffset != 0) {
parseOverlays(linescanOverlayOffset, "OffsetLinescanOverlay", little);
}
}
catch (FormatException exc) {
if (debug) trace(exc);
}
catch (IOException exc) {
if (debug) trace(exc);
}
Object pixelSizeX = getMeta("VoxelSizeX");
Object pixelSizeY = getMeta("VoxelSizeY");
Object pixelSizeZ = getMeta("VoxelSizeZ");
Float pixX = new Float(pixelSizeX == null ? "0" : pixelSizeX.toString());
Float pixY = new Float(pixelSizeY == null ? "0" : pixelSizeY.toString());
Float pixZ = new Float(pixelSizeZ == null ? "0" : pixelSizeZ.toString());
MetadataStore store = getMetadataStore();
store.setDimensions(pixX, pixY, pixZ, null, null, null);
// see if we have an associated MDB file
Location dir = new Location(currentId).getAbsoluteFile().getParentFile();
String[] dirList = dir.list();
for (int i=0; i<dirList.length; i++) {
if (dirList[i].toLowerCase().endsWith(".mdb")) {
try {
MDBParser.parseDatabase((new Location(dir.getPath(),
dirList[i])).getAbsolutePath(), metadata);
}
catch (FormatException exc) {
if (debug) trace(exc);
}
i = dirList.length;
}
}
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
if (debug) debug("ZeissLSMReader.initFile(" + id + ")");
super.initFile(id);
// go through the IFD hashtable array and
// remove anything with NEW_SUBFILE_TYPE = 1
// NEW_SUBFILE_TYPE = 1 indicates that the IFD
// contains a thumbnail image
status("Removing thumbnails");
int numThumbs = 0;
long prevOffset = 0;
byte[] b = new byte[48];
byte[] c = new byte[48];
for (int i=0; i<ifds.length; i++) {
long subFileType = TiffTools.getIFDLongValue(ifds[i],
TiffTools.NEW_SUBFILE_TYPE, true, 0);
long[] offsets = TiffTools.getStripOffsets(ifds[i]);
if (subFileType == 1) {
ifds[i] = null;
numThumbs++;
}
else if (i > 0) {
// make sure that we don't grab the thumbnail by accident
// there's probably a better way to do this
in.seek(prevOffset);
in.read(b);
in.seek(offsets[0]);
in.read(c);
boolean equal = true;
for (int j=0; j<48; j++) {
if (b[j] != c[j]) {
equal = false;
j = 48;
}
}
if (equal) {
offsets[0] += (offsets[0] - prevOffset);
TiffTools.putIFDValue(ifds[i], TiffTools.STRIP_OFFSETS, offsets);
}
}
prevOffset = offsets[0];
}
// now copy ifds to a temp array so that we can get rid of
// any null entries
int ifdPointer = 0;
Hashtable[] tempIFDs = new Hashtable[ifds.length - numThumbs];
for (int i=0; i<tempIFDs.length; i++) {
if (ifds[ifdPointer] != null) {
tempIFDs[i] = ifds[ifdPointer];
ifdPointer++;
}
else {
while ((ifds[ifdPointer] == null) && ifdPointer < ifds.length) {
ifdPointer++;
}
tempIFDs[i] = ifds[ifdPointer];
ifdPointer++;
}
}
// reset numImages and ifds
core.imageCount[0] = tempIFDs.length;
ifds = tempIFDs;
initMetadata();
ifds = TiffTools.getIFDs(in);
if (ifds.length > 1) {
core.thumbSizeX[0] = TiffTools.getIFDIntValue(ifds[1],
TiffTools.IMAGE_WIDTH, false, 1);
core.thumbSizeY[0] = TiffTools.getIFDIntValue(ifds[1],
TiffTools.IMAGE_LENGTH, false, 1);
}
}
// -- Helper methods --
/** Parses overlay-related fields. */
protected void parseOverlays(long data, String suffix, boolean little)
throws IOException
{
if (data == 0) return;
in.seek(data);
int nde = in.readInt();
put("NumberDrawingElements-" + suffix, nde);
int size = in.readInt();
int idata = in.readInt();
put("LineWidth-" + suffix, idata);
idata = in.readInt();
put("Measure-" + suffix, idata);
in.readDouble();
put("ColorRed-" + suffix, in.read());
put("ColorGreen-" + suffix, in.read());
put("ColorBlue-" + suffix, in.read());
in.read();
put("Valid-" + suffix, in.readInt());
put("KnotWidth-" + suffix, in.readInt());
put("CatchArea-" + suffix, in.readInt());
// some fields describing the font
put("FontHeight-" + suffix, in.readInt());
put("FontWidth-" + suffix, in.readInt());
put("FontEscapement-" + suffix, in.readInt());
put("FontOrientation-" + suffix, in.readInt());
put("FontWeight-" + suffix, in.readInt());
put("FontItalic-" + suffix, in.readInt());
put("FontUnderline-" + suffix, in.readInt());
put("FontStrikeOut-" + suffix, in.readInt());
put("FontCharSet-" + suffix, in.readInt());
put("FontOutPrecision-" + suffix, in.readInt());
put("FontClipPrecision-" + suffix, in.readInt());
put("FontQuality-" + suffix, in.readInt());
put("FontPitchAndFamily-" + suffix, in.readInt());
byte[] temp = new byte[64];
in.read(temp);
put("FontFaceName-" + suffix, new String(temp));
// some flags for measuring values of different drawing element types
put("ClosedPolyline-" + suffix, in.read());
put("OpenPolyline-" + suffix, in.read());
put("ClosedBezierCurve-" + suffix, in.read());
put("OpenBezierCurve-" + suffix, in.read());
put("ArrowWithClosedTip-" + suffix, in.read());
put("ArrowWithOpenTip-" + suffix, in.read());
put("Ellipse-" + suffix, in.read());
put("Circle-" + suffix, in.read());
put("Rectangle-" + suffix, in.read());
put("Line-" + suffix, in.read());
try {
int drawingEl = (size - 194) / nde;
for (int i=0; i<nde; i++) {
byte[] draw = new byte[drawingEl];
in.read(draw);
put("DrawingElement" + i + "-" + suffix, new String(draw));
}
}
catch (ArithmeticException exc) {
if (debug) trace(exc);
}
}
/** Parses subblock-related fields. */
protected void parseSubBlocks(long data, String suffix, boolean little)
throws IOException, FormatException
{
if (data == 0) return;
in.seek((int) data);
in.order(little);
long size = in.readInt();
if (size < 0) size += 4294967296L;
long numSubBlocks = in.readInt();
if (numSubBlocks < 0) numSubBlocks += 4294967296L;
put("NumSubBlocks-" + suffix, numSubBlocks);
long numChannels = in.readInt();
if (numChannels < 0) numChannels += 4294967296L;
put("NumChannels-" + suffix, numChannels);
data = in.readInt();
if (data < 0) data += 4294967296L;
put("LutType-" + suffix, data);
data = in.readInt();
if (data < 0) data += 4294967296L;
put("Advanced-" + suffix, data);
data = in.readInt();
if (data < 0) data += 4294967296L;
put("CurrentChannel-" + suffix, data);
in.skipBytes(36);
if (numSubBlocks > 100) numSubBlocks = 20;
for (int i=0; i<numSubBlocks; i++) {
data = in.readInt();
if (data < 0) data += 4294967296L;
put("Type" + i + "-" + suffix, data);
put("Size" + i + "-" + suffix, in.readInt());
switch ((int) data) {
case 1:
for (int j=0; j<numChannels; j++) {
put("GammaChannel" + j + "-" + i + "-" + suffix, in.readDouble());
}
break;
case 2:
for (int j=0; j<numChannels; j++) {
put("BrightnessChannel" + j + "-" + i + "-" + suffix,
in.readDouble());
}
break;
case 3:
for (int j=0; j<numChannels; j++) {
put("ContrastChannel" + j + "-" + i + "-" + suffix,
in.readDouble());
}
break;
case 4:
for (int j=0; j<numChannels; j++) {
put("RampStartXChannel" + j + "-" + i + "-" + suffix,
in.readDouble());
put("RampStartYChannel" + j + "-" + i + "-" + suffix,
in.readDouble());
put("RampEndXChannel" + j + "-" + i + "-" + suffix,
in.readDouble());
put("RampEndYChannel" + j + "-" + i + "-" + suffix,
in.readDouble());
j += 4;
}
break;
case 5:
// the specs are unclear as to how
// this subblock should be read, so I'm
// skipping it for the present
break;
case 6:
// also skipping this block for
// the moment
break;
}
}
}
}