//
// LIFReader.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.io.*;
import java.util.*;
import javax.xml.parsers.*;
import loci.formats.*;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* LIFReader is the file format reader for Leica LIF files.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/in/LIFReader.java">Trac</a>,
* <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/in/LIFReader.java">SVN</a></dd></dl>
*
* @author Melissa Linkert linkert at wisc.edu
*/
public class LIFReader extends FormatReader {
// -- Constants --
/** Factory for generating SAX parsers. */
public static final SAXParserFactory SAX_FACTORY =
SAXParserFactory.newInstance();
// -- Fields --
/** Offsets to memory blocks, paired with their corresponding description. */
protected Vector offsets;
/** Bits per pixel. */
private int[] bitsPerPixel;
/** Extra dimensions. */
private int[] extraDimensions;
private int bpp;
private Vector xcal;
private Vector ycal;
private Vector zcal;
private Vector seriesNames;
private Vector containerNames;
private Vector containerCounts;
// -- Constructor --
/** Constructs a new Leica LIF reader. */
public LIFReader() { super("Leica Image File Format", "lif"); }
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(byte[]) */
public boolean isThisType(byte[] block) {
return block[0] == 0x70;
}
/* @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);
long offset = ((Long) offsets.get(series)).longValue();
in.seek(offset + core.sizeX[series] * core.sizeY[series] * no *
FormatTools.getBytesPerPixel(getPixelType()) * getRGBChannelCount());
in.read(buf);
return buf;
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
if (debug) debug("LIFReader.initFile(" + id + ")");
super.initFile(id);
in = new RandomAccessStream(id);
offsets = new Vector();
core.littleEndian[0] = true;
in.order(core.littleEndian[0]);
xcal = new Vector();
ycal = new Vector();
zcal = new Vector();
// read the header
status("Reading header");
byte checkOne = (byte) in.read();
in.skipBytes(2);
byte checkTwo = (byte) in.read();
if (checkOne != 0x70 && checkTwo != 0x70) {
throw new FormatException(id + " is not a valid Leica LIF file");
}
in.skipBytes(4);
// read and parse the XML description
if (in.read() != 0x2a) {
throw new FormatException("Invalid XML description");
}
// number of Unicode characters in the XML block
int nc = in.readInt();
String xml = DataTools.stripString(in.readString(nc * 2));
status("Finding image offsets");
while (in.getFilePointer() < in.length()) {
if (in.readInt() != 0x70) {
throw new FormatException("Invalid Memory Block");
}
in.skipBytes(4);
if (in.read() != 0x2a) {
throw new FormatException("Invalid Memory Description");
}
long blockLength = in.readInt();
if (in.read() != 0x2a) {
in.seek(in.getFilePointer() - 5);
blockLength = in.readLong();
if (in.read() != 0x2a) {
throw new FormatException("Invalid Memory Description");
}
}
int descrLength = in.readInt();
in.skipBytes(descrLength * 2);
if (blockLength > 0) {
offsets.add(new Long(in.getFilePointer()));
}
long skipped = 0;
while (skipped < blockLength) {
if (blockLength - skipped > 4096) {
skipped += in.skipBytes(4096);
}
else {
skipped += in.skipBytes((int) (blockLength - skipped));
}
}
}
initMetadata(xml);
}
// -- Helper methods --
/** Parses a string of XML and puts the values in a Hashtable. */
private void initMetadata(String xml) throws FormatException, IOException {
// parse raw key/value pairs - adapted from FlexReader
containerNames = new Vector();
containerCounts = new Vector();
seriesNames = new Vector();
LIFHandler handler = new LIFHandler();
xml = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><LEICA>" + xml +
"</LEICA>";
// strip out invalid characters
for (int i=0; i<xml.length(); i++) {
char c = xml.charAt(i);
if (Character.isISOControl(c) || !Character.isDefined(c)) {
xml = xml.replace(c, ' ');
}
}
try {
SAXParser parser = SAX_FACTORY.newSAXParser();
parser.parse(new ByteArrayInputStream(xml.getBytes()), handler);
}
catch (ParserConfigurationException exc) {
throw new FormatException(exc);
}
catch (SAXException exc) {
throw new FormatException(exc);
}
Vector elements = new Vector();
status("Populating native metadata");
// first parse each element in the XML string
StringTokenizer st = new StringTokenizer(xml, ">");
while (st.hasMoreTokens()) {
String token = st.nextToken();
elements.add(token.substring(1));
}
// the first element contains version information
String token = (String) elements.get(0);
String key = token.substring(0, token.indexOf("\""));
String value = token.substring(token.indexOf("\"") + 1, token.length()-1);
addMeta(key, value);
// what we have right now is a vector of XML elements, which need to
// be parsed into the appropriate image dimensions
int ndx = 1;
// the image data we need starts with the token "ElementName='blah'" and
// ends with the token "/ImageDescription"
int numDatasets = 0;
Vector widths = new Vector();
Vector heights = new Vector();
Vector zs = new Vector();
Vector ts = new Vector();
Vector channels = new Vector();
Vector bps = new Vector();
Vector extraDims = new Vector();
while (ndx < elements.size()) {
token = (String) elements.get(ndx);
// if the element contains a key/value pair, parse it and put it in
// the metadata hashtable
if (token.startsWith("ScannerSettingRecord")) {
if (token.indexOf("csScanMode") != -1) {
int index = token.indexOf("Variant") + 7;
String ordering = token.substring(index + 2,
token.indexOf("\"", index + 3));
ordering = ordering.toLowerCase();
if (ordering.indexOf("x") == -1 || ordering.indexOf("y") == -1 ||
ordering.indexOf("xy") == -1)
{
int xPos = ordering.indexOf("x");
int yPos = ordering.indexOf("y");
int zPos = ordering.indexOf("z");
int tPos = ordering.indexOf("t");
if (xPos < 0) xPos = 0;
if (yPos < 0) yPos = 1;
if (zPos < 0) zPos = 2;
if (tPos < 0) tPos = 3;
int x = ((Integer) widths.get(widths.size() - 1)).intValue();
int y = ((Integer) heights.get(widths.size() - 1)).intValue();
int z = ((Integer) zs.get(widths.size() - 1)).intValue();
int t = ((Integer) ts.get(widths.size() - 1)).intValue();
int[] dimensions = {x, y, z, t};
x = dimensions[xPos];
y = dimensions[yPos];
z = dimensions[zPos];
t = dimensions[tPos];
widths.setElementAt(new Integer(x), widths.size() - 1);
heights.setElementAt(new Integer(y), heights.size() - 1);
zs.setElementAt(new Integer(z), zs.size() - 1);
ts.setElementAt(new Integer(t), ts.size() - 1);
}
}
else if (token.indexOf("dblVoxel") != -1) {
int index = token.indexOf("Variant") + 7;
String size = token.substring(index + 2,
token.indexOf("\"", index + 3));
float cal = Float.parseFloat(size) * 1000000;
if (token.indexOf("X") != -1) xcal.add(new Float(cal));
else if (token.indexOf("Y") != -1) ycal.add(new Float(cal));
else if (token.indexOf("Z") != -1) zcal.add(new Float(cal));
}
}
else if (token.startsWith("Element Name")) {
// loop until we find "/ImageDescription"
numDatasets++;
int numChannels = 0;
int extras = 1;
while (token.indexOf("/ImageDescription") == -1) {
if (token.indexOf("=") != -1) {
// create a small hashtable to store just this element's data
if (token.startsWith("Element Name")) {
// hack to override first series name
int idx = numDatasets - 1;
if (idx >= seriesNames.size()) {
numDatasets = seriesNames.size();
idx = numDatasets - 1;
}
}
Hashtable tmp = new Hashtable();
while (token.length() > 2) {
key = token.substring(0, token.indexOf("\"") - 1);
value = token.substring(token.indexOf("\"") + 1,
token.indexOf("\"", token.indexOf("\"") + 1));
token = token.substring(key.length() + value.length() + 3);
key = key.trim();
value = value.trim();
tmp.put(key, value);
}
if (tmp.get("ChannelDescription DataType") != null) {
// found channel description block
numChannels++;
if (numChannels == 1) {
bps.add(new Integer((String) tmp.get("Resolution")));
}
}
else if (tmp.get("DimensionDescription DimID") != null) {
// found dimension description block
int w = Integer.parseInt((String) tmp.get("NumberOfElements"));
int id = Integer.parseInt((String)
tmp.get("DimensionDescription DimID"));
switch (id) {
case 1:
widths.add(new Integer(w));
break;
case 2:
heights.add(new Integer(w));
break;
case 3:
zs.add(new Integer(w));
break;
case 4:
ts.add(new Integer(w));
break;
default:
extras *= w;
}
}
}
ndx++;
if (elements != null && ndx < elements.size()) {
token = (String) elements.get(ndx);
}
else break;
}
extraDims.add(new Integer(extras));
if (numChannels == 0) numChannels++;
channels.add(new Integer(numChannels));
if (widths.size() < numDatasets && heights.size() < numDatasets) {
numDatasets--;
}
else {
if (widths.size() < numDatasets) widths.add(new Integer(1));
if (heights.size() < numDatasets) heights.add(new Integer(1));
if (zs.size() < numDatasets) zs.add(new Integer(1));
if (ts.size() < numDatasets) ts.add(new Integer(1));
if (bps.size() < numDatasets) bps.add(new Integer(8));
}
}
ndx++;
}
numDatasets = widths.size();
bitsPerPixel = new int[numDatasets];
extraDimensions = new int[numDatasets];
// Populate metadata store
status("Populating metadata");
// The metadata store we're working with.
MetadataStore store = getMetadataStore();
core = new CoreMetadata(numDatasets);
Arrays.fill(core.orderCertain, true);
for (int i=0; i<numDatasets; i++) {
core.sizeX[i] = ((Integer) widths.get(i)).intValue();
core.sizeY[i] = ((Integer) heights.get(i)).intValue();
core.sizeZ[i] = ((Integer) zs.get(i)).intValue();
core.sizeC[i] = ((Integer) channels.get(i)).intValue();
core.sizeT[i] = ((Integer) ts.get(i)).intValue();
core.currentOrder[i] =
(core.sizeZ[i] > core.sizeT[i]) ? "XYCZT" : "XYCTZ";
bitsPerPixel[i] = ((Integer) bps.get(i)).intValue();
extraDimensions[i] = ((Integer) extraDims.get(i)).intValue();
if (extraDimensions[i] > 1) {
if (core.sizeZ[i] == 1) core.sizeZ[i] = extraDimensions[i];
else core.sizeT[i] *= extraDimensions[i];
extraDimensions[i] = 1;
}
core.metadataComplete[i] = true;
core.littleEndian[i] = true;
core.rgb[i] = false;
core.interleaved[i] = false;
core.imageCount[i] = core.sizeZ[i] * core.sizeT[i];
core.imageCount[i] *= core.sizeC[i];
core.indexed[i] = false;
core.falseColor[i] = false;
while (bitsPerPixel[i] % 8 != 0) bitsPerPixel[i]++;
switch (bitsPerPixel[i]) {
case 8:
core.pixelType[i] = FormatTools.UINT8;
break;
case 16:
core.pixelType[i] = FormatTools.UINT16;
break;
case 32:
core.pixelType[i] = FormatTools.FLOAT;
break;
}
Integer ii = new Integer(i);
String seriesName = (String) seriesNames.get(i);
if (seriesName == null || seriesName.trim().length() == 0) {
seriesName = "Series " + (i + 1);
}
store.setImage(seriesName, null, null, ii);
FormatTools.populatePixels(store, this);
Float xf = i < xcal.size() ? (Float) xcal.get(i) : null;
Float yf = i < ycal.size() ? (Float) ycal.get(i) : null;
Float zf = i < zcal.size() ? (Float) zcal.get(i) : null;
store.setDimensions(xf, yf, zf, null, null, ii);
for (int j=0; j<core.sizeC[i]; j++) {
store.setLogicalChannel(j, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, ii);
}
String zoom = (String) getMeta(seriesName + " - dblZoom");
store.setDisplayOptions(zoom == null ? null : new Float(zoom),
new Boolean(core.sizeC[i] > 1), new Boolean(core.sizeC[i] > 1),
new Boolean(core.sizeC[i] > 2), new Boolean(isRGB()), null,
null, null, null, null, ii, null, null, null, null, null);
Enumeration keys = metadata.keys();
while (keys.hasMoreElements()) {
String k = (String) keys.nextElement();
if (k.startsWith((String) seriesNames.get(i) + " ")) {
core.seriesMetadata[i].put(k, metadata.get(k));
}
}
}
}
// -- Helper class --
/** SAX handler for parsing XML. */
class LIFHandler extends DefaultHandler {
private String series;
private String fullSeries;
private int count = 0;
private boolean firstElement = true;
private boolean dcroiOpen = false;
public void endElement(String uri, String localName, String qName) {
if (qName.equals("Element")) {
if (dcroiOpen) {
dcroiOpen = false;
return;
}
if (fullSeries.indexOf("/") != -1) {
fullSeries = fullSeries.substring(0, fullSeries.lastIndexOf("/"));
}
else fullSeries = "";
}
}
public void startElement(String uri, String localName, String qName,
Attributes attributes)
{
if (qName.equals("Element")) {
if (!attributes.getValue("Name").equals("DCROISet") && !firstElement) {
series = attributes.getValue("Name");
containerNames.add(series);
if (fullSeries == null || fullSeries.equals("")) fullSeries = series;
else fullSeries += "/" + series;
}
else if (firstElement) firstElement = false;
if (attributes.getValue("Name").equals("DCROISet")) {
dcroiOpen = true;
}
}
else if (qName.equals("Experiment")) {
for (int i=0; i<attributes.getLength(); i++) {
addMeta(attributes.getQName(i), attributes.getValue(i));
}
}
else if (qName.equals("Image")) {
containerNames.remove(series);
if (containerCounts.size() < containerNames.size()) {
containerCounts.add(new Integer(1));
}
else if (containerCounts.size() > 0) {
int ndx = containerCounts.size() - 1;
int n = ((Integer) containerCounts.get(ndx)).intValue();
containerCounts.setElementAt(new Integer(n + 1), ndx);
}
if (fullSeries == null || fullSeries.equals("")) fullSeries = series;
seriesNames.add(fullSeries);
}
else if (qName.equals("ChannelDescription")) {
String prefix = fullSeries + " - Channel " + count + " - ";
addMeta(prefix + "Min", attributes.getValue("Min"));
addMeta(prefix + "Max", attributes.getValue("Max"));
addMeta(prefix + "Resolution", attributes.getValue("Resolution"));
addMeta(prefix + "LUTName", attributes.getValue("LUTName"));
addMeta(prefix + "IsLUTInverted", attributes.getValue("IsLUTInverted"));
count++;
}
else if (qName.equals("DimensionDescription")) {
String prefix = fullSeries + " - Dimension " + count + " - ";
addMeta(prefix + "NumberOfElements",
attributes.getValue("NumberOfElements"));
addMeta(prefix + "Length", attributes.getValue("Length"));
addMeta(prefix + "Origin", attributes.getValue("Origin"));
addMeta(prefix + "DimID", attributes.getValue("DimID"));
}
else if (qName.equals("ScannerSettingRecord")) {
String key = attributes.getValue("Identifier") + " - " +
attributes.getValue("Description");
addMeta(fullSeries + " - " + key, attributes.getValue("Variant"));
}
else if (qName.equals("FilterSettingRecord")) {
String key = attributes.getValue("ObjectName") + " - " +
attributes.getValue("Description") + " - " +
attributes.getValue("Attribute");
addMeta(fullSeries + " - " + key, attributes.getValue("Variant"));
}
else if (qName.equals("ATLConfocalSettingDefinition")) {
if (fullSeries.endsWith(" - Master sequential setting")) {
fullSeries = fullSeries.replaceAll(" - Master sequential setting",
" - Sequential Setting 0");
}
if (fullSeries.indexOf("- Sequential Setting ") == -1) {
fullSeries += " - Master sequential setting";
}
else {
int ndx = fullSeries.indexOf(" - Sequential Setting ") + 22;
int n = Integer.parseInt(fullSeries.substring(ndx));
n++;
fullSeries = fullSeries.substring(0, ndx) + String.valueOf(n);
}
for (int i=0; i<attributes.getLength(); i++) {
addMeta(fullSeries + " - " + attributes.getQName(i),
attributes.getValue(i));
}
}
else if (qName.equals("Wheel")) {
String prefix = fullSeries + " - Wheel " + count + " - ";
addMeta(prefix + "Qualifier", attributes.getValue("Qualifier"));
addMeta(prefix + "FilterIndex", attributes.getValue("FilterIndex"));
addMeta(prefix + "FilterSpectrumPos",
attributes.getValue("FilterSpectrumPos"));
addMeta(prefix + "IsSpectrumTurnMode",
attributes.getValue("IsSpectrumTurnMode"));
addMeta(prefix + "IndexChanged", attributes.getValue("IndexChanged"));
addMeta(prefix + "SpectrumChanged",
attributes.getValue("SpectrumChanged"));
count++;
}
else if (qName.equals("WheelName")) {
String prefix = fullSeries + " - Wheel " + (count - 1) + " - WheelName ";
int ndx = 0;
while (getMeta(prefix + ndx) != null) ndx++;
addMeta(prefix + ndx, attributes.getValue("FilterName"));
}
else if (qName.equals("MultiBand")) {
String prefix = fullSeries + " - MultiBand Channel " +
attributes.getValue("Channel") + " - ";
addMeta(prefix + "LeftWorld", attributes.getValue("LeftWorld"));
addMeta(prefix + "RightWorld", attributes.getValue("RightWorld"));
addMeta(prefix + "DyeName", attributes.getValue("DyeName"));
}
else if (qName.equals("LaserLineSetting")) {
String prefix = fullSeries + " - LaserLine " +
attributes.getValue("LaserLine") + " - ";
addMeta(prefix + "IntensityDev", attributes.getValue("IntensityDev"));
addMeta(prefix + "IntensityLowDev",
attributes.getValue("IntensityLowDev"));
addMeta(prefix + "AOBSIntensityDev",
attributes.getValue("AOBSIntensityDev"));
addMeta(prefix + "AOBSIntensityLowDev",
attributes.getValue("AOBSIntensityLowDev"));
addMeta(prefix + "EnableDoubleMode",
attributes.getValue("EnableDoubleMode"));
addMeta(prefix + "LineIndex", attributes.getValue("LineIndex"));
addMeta(prefix + "Qualifier", attributes.getValue("Qualifier"));
addMeta(prefix + "SequenceIndex",
attributes.getValue("SequenceIndex"));
}
else if (qName.equals("Detector")) {
String prefix = fullSeries + " - Detector Channel " +
attributes.getValue("Channel") + " - ";
addMeta(prefix + "IsActive", attributes.getValue("IsActive"));
addMeta(prefix + "IsReferenceUnitActivatedForCorrection",
attributes.getValue("IsReferenceUnitActivatedForCorrection"));
addMeta(prefix + "Gain", attributes.getValue("Gain"));
addMeta(prefix + "Offset", attributes.getValue("Offset"));
}
else if (qName.equals("Laser")) {
String prefix = fullSeries + " Laser " +
attributes.getValue("LaserName") + " - ";
addMeta(prefix + "CanDoLinearOutputPower",
attributes.getValue("CanDoLinearOutputPower"));
addMeta(prefix + "OutputPower", attributes.getValue("OutputPower"));
addMeta(prefix + "Wavelength", attributes.getValue("Wavelength"));
}
else if (qName.equals("TimeStamp")) {
long high = Long.parseLong(attributes.getValue("HighInteger"));
long low = Long.parseLong(attributes.getValue("LowInteger"));
long stamp = 0;
high <<= 32;
if ((int) low < 0) {
low &= 0xffffffffL;
}
stamp = high + low;
long ms = stamp / 10000;
String n = String.valueOf(count);
while (n.length() < 4) n = "0" + n;
addMeta(fullSeries + " - TimeStamp " + n,
DataTools.convertDate(ms, DataTools.COBOL));
count++;
}
else if (qName.equals("ChannelScalingInfo")) {
String prefix = fullSeries + " - ChannelScalingInfo " + count + " - ";
addMeta(prefix + "WhiteValue", attributes.getValue("WhiteValue"));
addMeta(prefix + "BlackValue", attributes.getValue("BlackValue"));
addMeta(prefix + "GammaValue", attributes.getValue("GammaValue"));
addMeta(prefix + "Automatic", attributes.getValue("Automatic"));
}
else if (qName.equals("RelTimeStamp")) {
addMeta(fullSeries + " RelTimeStamp " + attributes.getValue("Frame"),
attributes.getValue("Time"));
}
else count = 0;
}
}
}